diff --git a/packages/core/core/src/AssetGraph.js b/packages/core/core/src/AssetGraph.js index 5afb1f68e1e..72563b13b05 100644 --- a/packages/core/core/src/AssetGraph.js +++ b/packages/core/core/src/AssetGraph.js @@ -30,6 +30,15 @@ import {ContentGraph} from '@parcel/graph'; import {createDependency} from './Dependency'; import {type ProjectPath, fromProjectPathRelative} from './projectPath'; +export const assetGraphEdgeTypes = { + null: 1, + // In addition to the null edge, a dependency can be connected to the asset containing the symbols + // that the dependency requested (after reexports were skipped). + redirected: 2, +}; + +export type AssetGraphEdgeType = $Values; + type InitOpts = {| entries?: Array, targets?: Array, @@ -43,7 +52,7 @@ type AssetGraphOpts = {| |}; type SerializedAssetGraph = {| - ...SerializedContentGraph, + ...SerializedContentGraph, hash?: ?string, symbolPropagationRan: boolean, |}; @@ -56,10 +65,11 @@ export function nodeFromDep(dep: Dependency): DependencyNode { deferred: false, excluded: false, usedSymbolsDown: new Set(), - usedSymbolsUp: new Set(), + usedSymbolsUp: new Map(), usedSymbolsDownDirty: true, usedSymbolsUpDirtyDown: true, usedSymbolsUpDirtyUp: true, + symbolTarget: null, }; } @@ -109,7 +119,10 @@ export function nodeFromEntryFile(entry: Entry): EntryFileNode { }; } -export default class AssetGraph extends ContentGraph { +export default class AssetGraph extends ContentGraph< + AssetGraphNode, + AssetGraphEdgeType, +> { onNodeRemoved: ?(nodeId: NodeId) => mixed; hash: ?string; envCache: Map; diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index b0ab7d12864..93f5354a44d 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -37,6 +37,7 @@ import {Priority, BundleBehavior, SpecifierType} from './types'; import {getBundleGroupId, getPublicId} from './utils'; import {ISOLATED_ENVS} from './public/Environment'; import {fromProjectPath} from './projectPath'; +import {assetGraphEdgeTypes} from './AssetGraph'; export const bundleGraphEdgeTypes = { // A lack of an edge type indicates to follow the edge while traversing @@ -146,7 +147,8 @@ export default class BundleGraph { assetPublicIds: Set = new Set(), ): BundleGraph { let graph = new ContentGraph(); - let assetGroupIds = new Set(); + let assetGroupIds = new Map(); + let dependencyIds = new Set(); let assetGraphNodeIdToBundleGraphNodeId = new Map(); let assetGraphRootNode = @@ -155,63 +157,80 @@ export default class BundleGraph { : null; invariant(assetGraphRootNode != null && assetGraphRootNode.type === 'root'); - for (let [nodeId, node] of assetGraph.nodes) { - if (node.type === 'asset') { - let {id: assetId} = node.value; - // Generate a new, short public id for this asset to use. - // If one already exists, use it. - let publicId = publicIdByAssetId.get(assetId); - if (publicId == null) { - publicId = getPublicId(assetId, existing => - assetPublicIds.has(existing), - ); - publicIdByAssetId.set(assetId, publicId); - assetPublicIds.add(publicId); + assetGraph.dfs({ + visit: nodeId => { + let node = nullthrows(assetGraph.getNode(nodeId)); + if (node.type === 'asset') { + let {id: assetId} = node.value; + // Generate a new, short public id for this asset to use. + // If one already exists, use it. + let publicId = publicIdByAssetId.get(assetId); + if (publicId == null) { + publicId = getPublicId(assetId, existing => + assetPublicIds.has(existing), + ); + publicIdByAssetId.set(assetId, publicId); + assetPublicIds.add(publicId); + } } - } - // Don't copy over asset groups into the bundle graph. - if (node.type === 'asset_group') { - assetGroupIds.add(nodeId); - } else { - let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node); - if (node.id === assetGraphRootNode?.id) { - graph.setRootNodeId(bundleGraphNodeId); + // Don't copy over asset groups into the bundle graph. + if (node.type === 'asset_group') { + assetGroupIds.set( + nodeId, + assetGraph.getNodeIdsConnectedFrom( + nodeId, + assetGraphEdgeTypes.null, + ), + ); + } else { + if (node.type === 'dependency') { + dependencyIds.add(nodeId); + } + let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node); + if (node.id === assetGraphRootNode?.id) { + graph.setRootNodeId(bundleGraphNodeId); + } + assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId); } - assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId); - } - } + }, + startNodeId: null, + getChildren: nodeId => { + let children = assetGraph.getNodeIdsConnectedFrom( + nodeId, + assetGraphEdgeTypes.redirected, + ); + if (children.length > 0) { + return children; + } else { + return assetGraph.getNodeIdsConnectedFrom( + nodeId, + assetGraphEdgeTypes.null, + ); + } + }, + }); for (let edge of assetGraph.getAllEdges()) { - let fromIds; - if (assetGroupIds.has(edge.from)) { - fromIds = [ - ...assetGraph.getNodeIdsConnectedTo( - edge.from, - bundleGraphEdgeTypes.null, - ), - ]; - } else { - fromIds = [edge.from]; + if ( + dependencyIds.has(edge.from) && + edge.type === assetGraphEdgeTypes.null && + assetGraph.adjacencyList + .getOutboundEdgesByType(edge.from) + .some(n => n.type === assetGraphEdgeTypes.redirected) + ) { + // If there's a redirect edge for a dependency, ignore the normal edge and convert the + // redirect edge into a regular bundlegraph edge. + continue; } - for (let from of fromIds) { - if (assetGroupIds.has(edge.to)) { - for (let to of assetGraph.getNodeIdsConnectedFrom( - edge.to, - bundleGraphEdgeTypes.null, - )) { - graph.addEdge( - nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(from)), - nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(to)), - ); - } - } else { - graph.addEdge( - nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(from)), - nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.to)), - ); - } + let fromBundleGraph = assetGraphNodeIdToBundleGraphNodeId.get(edge.from); + if (fromBundleGraph == null) continue; // an asset group + for (let to of assetGroupIds.get(edge.to) ?? [edge.to]) { + graph.addEdge( + fromBundleGraph, + nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(to)), + ); } } @@ -858,6 +877,17 @@ export default class BundleGraph { }); } + getDependenciesWithSymbolTarget( + asset: Asset, + ): Array<[Dependency, ?Map]> { + let nodeId = this._graph.getNodeIdByContentKey(asset.id); + return this._graph.getNodeIdsConnectedFrom(nodeId).map(id => { + let node = nullthrows(this._graph.getNode(id)); + invariant(node.type === 'dependency'); + return [node.value, node.symbolTarget]; + }); + } + traverseAssets( bundle: Bundle, visit: GraphVisitor, @@ -1402,9 +1432,9 @@ export default class BundleGraph { let found = false; let nonStaticDependency = false; let skipped = false; - let deps = this.getDependencies(asset).reverse(); + let deps = this.getDependenciesWithSymbolTarget(asset).reverse(); let potentialResults = []; - for (let dep of deps) { + for (let [dep, symbolTarget] of deps) { let depSymbols = dep.symbols; if (!depSymbols) { nonStaticDependency = true; @@ -1414,7 +1444,10 @@ export default class BundleGraph { let symbolLookup = new Map( [...depSymbols].map(([key, val]) => [val.local, key]), ); - let depSymbol = symbolLookup.get(identifier); + let depSymbol = + identifier != null + ? symbolLookup.get(symbolTarget?.get(identifier) ?? identifier) + : undefined; if (depSymbol != null) { let resolved = this.getResolvedAsset(dep); if (!resolved || resolved.id === asset.id) { @@ -1737,16 +1770,23 @@ export default class BundleGraph { let node = this._graph.getNodeByContentKey(asset.id); invariant(node && node.type === 'asset'); return this._symbolPropagationRan - ? makeReadOnlySet(node.usedSymbols) + ? makeReadOnlySet(new Set(node.usedSymbols.keys())) : null; } getUsedSymbolsDependency(dep: Dependency): ?$ReadOnlySet { let node = this._graph.getNodeByContentKey(dep.id); invariant(node && node.type === 'dependency'); - return this._symbolPropagationRan - ? makeReadOnlySet(node.usedSymbolsUp) - : null; + let result = new Set(node.usedSymbolsUp.keys()); + if (node.symbolTarget) { + for (let [k, v] of node.symbolTarget) { + if (result.has(k)) { + result.delete(k); + result.add(v); + } + } + } + return this._symbolPropagationRan ? makeReadOnlySet(result) : null; } merge(other: BundleGraph) { @@ -1758,6 +1798,7 @@ export default class BundleGraph { let existingNode = nullthrows(this._graph.getNode(existingNodeId)); // Merge symbols, recompute dep.exluded based on that + if (existingNode.type === 'asset') { invariant(otherNode.type === 'asset'); existingNode.usedSymbols = new Set([ @@ -1770,7 +1811,7 @@ export default class BundleGraph { ...existingNode.usedSymbolsDown, ...otherNode.usedSymbolsDown, ]); - existingNode.usedSymbolsUp = new Set([ + existingNode.usedSymbolsUp = new Map([ ...existingNode.usedSymbolsUp, ...otherNode.usedSymbolsUp, ]); diff --git a/packages/core/core/src/dumpGraphToGraphViz.js b/packages/core/core/src/dumpGraphToGraphViz.js index 79519fa9183..b32d602925c 100644 --- a/packages/core/core/src/dumpGraphToGraphViz.js +++ b/packages/core/core/src/dumpGraphToGraphViz.js @@ -3,6 +3,8 @@ import type {Asset, BundleBehavior} from '@parcel/types'; import type {Graph} from '@parcel/graph'; import type {AssetGraphNode, BundleGraphNode, Environment} from './types'; +import type {AssetGraphEdgeType} from './AssetGraph'; +import {assetGraphEdgeTypes} from './AssetGraph'; import {bundleGraphEdgeTypes} from './BundleGraph'; import {requestGraphEdgeTypes} from './RequestTracker'; @@ -21,11 +23,15 @@ const COLORS = { }; const TYPE_COLORS = { + // bundle graph bundle: 'blue', contains: 'grey', internal_async: 'orange', references: 'red', sibling: 'green', + // asset graph + redirected: 'grey', + // request graph invalidated_by_create: 'green', invalidated_by_create_above: 'orange', invalidate_by_update: 'cyan', @@ -34,7 +40,7 @@ const TYPE_COLORS = { export default async function dumpGraphToGraphViz( graph: - | Graph + | Graph | Graph<{| assets: Set, sourceBundles: Set, @@ -42,7 +48,10 @@ export default async function dumpGraphToGraphViz( |}> | Graph, name: string, - edgeTypes?: typeof bundleGraphEdgeTypes | typeof requestGraphEdgeTypes, + edgeTypes?: + | typeof assetGraphEdgeTypes + | typeof bundleGraphEdgeTypes + | typeof requestGraphEdgeTypes, ): Promise { if ( process.env.PARCEL_BUILD_ENV === 'production' || @@ -80,8 +89,13 @@ export default async function dumpGraphToGraphViz( if (node.type === 'dependency') { label += node.value.specifier; let parts = []; - if (node.value.priority !== Priority.sync) - parts.push(node.value.priority); + if (node.value.priority !== Priority.sync) { + parts.push( + Object.entries(Priority).find( + ([, v]) => v === node.value.priority, + )?.[0], + ); + } if (node.value.isOptional) parts.push('optional'); if (node.value.specifierType === SpecifierType.url) parts.push('url'); if (node.hasDeferred) parts.push('deferred'); @@ -103,12 +117,25 @@ export default async function dumpGraphToGraphViz( label += '\\nweakSymbols: ' + weakSymbols.join(','); } if (node.usedSymbolsUp.size > 0) { - label += '\\nusedSymbolsUp: ' + [...node.usedSymbolsUp].join(','); + label += + '\\nusedSymbolsUp: ' + + [...node.usedSymbolsUp] + .map(([s, sAsset]) => + sAsset + ? `${s}(${sAsset.asset}.${sAsset.symbol ?? ''})` + : `${s}(external)`, + ) + .join(','); } if (node.usedSymbolsDown.size > 0) { label += '\\nusedSymbolsDown: ' + [...node.usedSymbolsDown].join(','); } + if (node.symbolTarget && node.symbolTarget?.size > 0) { + label += + '\\nsymbolTarget: ' + + [...node.symbolTarget].map(([a, b]) => `${a}:${b}`).join(','); + } } else { label += '\\nsymbols: cleared'; } diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index b42a7b68fed..3f1e07aeaf0 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -10,6 +10,7 @@ import type { ExportSymbolResolution, FilePath, GraphVisitor, + DependencySymbols as IDependencySymbols, Symbol, SymbolResolution, Target, @@ -27,6 +28,7 @@ import Dependency, {dependencyToInternalDependency} from './Dependency'; import {targetToInternalTarget} from './Target'; import {fromInternalSourceLocation} from '../utils'; import BundleGroup, {bundleGroupToInternalBundleGroup} from './BundleGroup'; +import {DependencySymbols} from './Symbols'; // Friendly access for other modules within this package that need access // to the internal bundle. @@ -308,6 +310,12 @@ export default class BundleGraph } } + getSymbols(dep: IDependency): IDependencySymbols { + let node = this.#graph._graph.getNodeByContentKey(dep.id); + invariant(node && node.type === 'dependency'); + return new DependencySymbols(this.#options, node.value, node.symbolTarget); + } + getEntryRoot(target: Target): FilePath { return this.#graph.getEntryRoot( this.#options.projectRoot, diff --git a/packages/core/core/src/public/Symbols.js b/packages/core/core/src/public/Symbols.js index 82ae6e3ae70..75130c0d885 100644 --- a/packages/core/core/src/public/Symbols.js +++ b/packages/core/core/src/public/Symbols.js @@ -4,6 +4,7 @@ import type { MutableAssetSymbols as IMutableAssetSymbols, AssetSymbols as IAssetSymbols, MutableDependencySymbols as IMutableDependencySymbols, + DependencySymbols as IDependencySymbols, SourceLocation, Meta, } from '@parcel/types'; @@ -27,7 +28,6 @@ const EMPTY_ITERATOR = { const inspect = Symbol.for('nodejs.util.inspect.custom'); let valueToSymbols: WeakMap = new WeakMap(); - export class AssetSymbols implements IAssetSymbols { /*:: @@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, meta?: ?Meta|}]> { return ({}: any); } @@ -192,6 +192,108 @@ export class MutableAssetSymbols implements IMutableAssetSymbols { } } +let valueToDependencySymbols: WeakMap = + new WeakMap(); +export class DependencySymbols implements IDependencySymbols { + /*:: + @@iterator(): Iterator<[ISymbol, {|local: ISymbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|}]> { return ({}: any); } + */ + #value: Dependency; + #options: ParcelOptions; + #symbolTarget: ?Map; + + constructor( + options: ParcelOptions, + dep: Dependency, + symbolTarget: ?Map, + ): DependencySymbols { + let existing = valueToDependencySymbols.get(dep); + if (existing != null) { + return existing; + } + this.#value = dep; + this.#options = options; + this.#symbolTarget = symbolTarget; + return this; + } + + #translateExportSymbol(exportSymbol: ISymbol): ISymbol { + return this.#symbolTarget?.get(exportSymbol) ?? exportSymbol; + } + + // immutable: + + hasExportSymbol(exportSymbol: ISymbol): boolean { + return Boolean( + this.#value.symbols?.has(this.#translateExportSymbol(exportSymbol)), + ); + } + + hasLocalSymbol(local: ISymbol): boolean { + if (this.#value.symbols) { + for (let s of this.#value.symbols.values()) { + if (local === s.local) return true; + } + } + return false; + } + + get( + exportSymbol: ISymbol, + ): ?{|local: ISymbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|} { + return fromInternalDependencySymbol( + this.#options.projectRoot, + nullthrows(this.#value.symbols).get( + this.#translateExportSymbol(exportSymbol), + ), + ); + } + + get isCleared(): boolean { + return this.#value.symbols == null; + } + + exportSymbols(): Iterable { + let symbols = this.#value.symbols; + if (symbols) { + let result = new Set(); + for (let s of symbols.keys()) { + result.add(this.#translateExportSymbol(s)); + } + return result; + } else { + // $FlowFixMe + return EMPTY_ITERABLE; + } + } + + // $FlowFixMe + [Symbol.iterator]() { + let symbols = this.#value.symbols; + if (symbols) { + let result = []; + for (let [s, v] of symbols) { + result.push([this.#translateExportSymbol(s), v]); + } + return result[Symbol.iterator](); + } else { + // $FlowFixMe + return EMPTY_ITERATOR; + } + } + + // $FlowFixMe + [inspect]() { + return `DependencySymbols(${ + this.#value.symbols + ? [...this.#value.symbols] + .map(([s, {local, isWeak}]) => `${s}:${local}${isWeak ? '?' : ''}`) + .join(', ') + : null + })`; + } +} + let valueToMutableDependencySymbols: WeakMap< Dependency, MutableDependencySymbols, diff --git a/packages/core/core/src/requests/AssetGraphRequest.js b/packages/core/core/src/requests/AssetGraphRequest.js index e9ebc26898b..f8db5a5d2c6 100644 --- a/packages/core/core/src/requests/AssetGraphRequest.js +++ b/packages/core/core/src/requests/AssetGraphRequest.js @@ -1,7 +1,7 @@ // @flow strict-local import type {Diagnostic} from '@parcel/diagnostic'; -import type {NodeId} from '@parcel/graph'; +import type {ContentKey, NodeId} from '@parcel/graph'; import type {Async, Symbol, Meta} from '@parcel/types'; import type {SharedReference} from '@parcel/workers'; import type { @@ -27,7 +27,7 @@ import {hashString} from '@parcel/hash'; import logger from '@parcel/logger'; import ThrowableDiagnostic, {md} from '@parcel/diagnostic'; import {BundleBehavior, Priority} from '../types'; -import AssetGraph from '../AssetGraph'; +import AssetGraph, {assetGraphEdgeTypes} from '../AssetGraph'; import {PARCEL_VERSION} from '../constants'; import createEntryRequest from './EntryRequest'; import createTargetRequest from './TargetRequest'; @@ -38,8 +38,7 @@ import { fromProjectPathRelative, fromProjectPath, } from '../projectPath'; - -import dumpToGraphViz from '../dumpGraphToGraphViz'; +import dumpGraphToGraphViz from '../dumpGraphToGraphViz'; type AssetGraphRequestInput = {| entries?: Array, @@ -209,14 +208,27 @@ export class AssetGraphBuilder { d => d.value.env.shouldScopeHoist, ); if (this.assetGraph.symbolPropagationRan) { + await dumpGraphToGraphViz( + this.assetGraph, + 'AssetGraph_' + this.name + '_before_prop', + assetGraphEdgeTypes, + ); try { this.propagateSymbols(); } catch (e) { - dumpToGraphViz(this.assetGraph, 'AssetGraph_' + this.name + '_failed'); + await dumpGraphToGraphViz( + this.assetGraph, + 'AssetGraph_' + this.name + '_failed', + assetGraphEdgeTypes, + ); throw e; } } - dumpToGraphViz(this.assetGraph, 'AssetGraph_' + this.name); + await dumpGraphToGraphViz( + this.assetGraph, + 'AssetGraph_' + this.name, + assetGraphEdgeTypes, + ); return { assetGraph: this.assetGraph, @@ -408,7 +420,7 @@ export class AssetGraphBuilder { const logFallbackNamespaceInsertion = ( assetNode, - symbol, + symbol: Symbol, depNode1, depNode2, ) => { @@ -426,6 +438,8 @@ export class AssetGraphBuilder { } }; + let replacements = new Map(); + // Because namespace reexports introduce ambiguity, go up the graph from the leaves to the // root and remove requested symbols that aren't actually exported this.propagateSymbolsUp((assetNode, incomingDeps, outgoingDeps) => { @@ -449,42 +463,61 @@ export class AssetGraphBuilder { } } - // the symbols that are reexport (not used in `asset`) -> the corresponding outgoingDep(s) - // There could be multiple dependencies with non-statically analyzable exports - let reexportedSymbols = new Map(); + // the symbols that are reexported (not used in `asset`) -> asset they resolved to + let reexportedSymbols = new Map< + Symbol, + ?{|asset: ContentKey, symbol: ?Symbol|}, + >(); + // the symbols that are reexported (not used in `asset`) -> the corresponding outgoingDep(s) + // To generate the diagnostic when there are multiple dependencies with non-statically + // analyzable exports + let reexportedSymbolsSource = new Map(); for (let outgoingDep of outgoingDeps) { let outgoingDepSymbols = outgoingDep.value.symbols; if (!outgoingDepSymbols) continue; - // excluded, assume everything that is requested exists - if ( + let isExcluded = this.assetGraph.getNodeIdsConnectedFrom( this.assetGraph.getNodeIdByContentKey(outgoingDep.id), - ).length === 0 - ) { - outgoingDep.usedSymbolsDown.forEach(s => - outgoingDep.usedSymbolsUp.add(s), + ).length === 0; + // excluded, assume everything that is requested exists + if (isExcluded) { + outgoingDep.usedSymbolsDown.forEach((_, s) => + outgoingDep.usedSymbolsUp.set(s, null), ); } if (outgoingDepSymbols.get('*')?.local === '*') { - outgoingDep.usedSymbolsUp.forEach(s => { + outgoingDep.usedSymbolsUp.forEach((sResolved, s) => { // If the symbol could come from multiple assets at runtime, assetNode's // namespace will be needed at runtime to perform the lookup on. - if (reexportedSymbols.has(s) && !assetNode.usedSymbols.has('*')) { - logFallbackNamespaceInsertion( - assetNode, + if (reexportedSymbols.has(s)) { + if (!assetNode.usedSymbols.has('*')) { + logFallbackNamespaceInsertion( + assetNode, + s, + nullthrows(reexportedSymbolsSource.get(s)), + outgoingDep, + ); + } + assetNode.usedSymbols.add('*'); + reexportedSymbols.set(s, {asset: assetNode.id, symbol: s}); + } else { + reexportedSymbols.set( s, - nullthrows(reexportedSymbols.get(s)), - outgoingDep, + // Forward a reexport only if the current asset is side-effect free. + !assetNode.value.sideEffects + ? sResolved + : {asset: assetNode.id, symbol: s}, ); - assetNode.usedSymbols.add('*'); + reexportedSymbolsSource.set(s, outgoingDep); } - reexportedSymbols.set(s, outgoingDep); }); } - for (let s of outgoingDep.usedSymbolsUp) { + let outgoingDepReplacements = new Map(); + let outgoingDepResolved = undefined; + for (let [s, sResolved] of outgoingDep.usedSymbolsUp) { if (!outgoingDep.usedSymbolsDown.has(s)) { // usedSymbolsDown is a superset of usedSymbolsUp continue; @@ -500,26 +533,66 @@ export class AssetGraphBuilder { if (reexported != null) { reexported.forEach(s => { // see same code above - if (reexportedSymbols.has(s) && !assetNode.usedSymbols.has('*')) { - logFallbackNamespaceInsertion( - assetNode, + if (reexportedSymbols.has(s)) { + if (!assetNode.usedSymbols.has('*')) { + logFallbackNamespaceInsertion( + assetNode, + s, + nullthrows(reexportedSymbolsSource.get(s)), + outgoingDep, + ); + } + assetNode.usedSymbols.add('*'); + reexportedSymbols.set(s, {asset: assetNode.id, symbol: s}); + } else { + reexportedSymbols.set( s, - nullthrows(reexportedSymbols.get(s)), - outgoingDep, + // Forward a reexport only if the current asset is side-effect free. + !assetNode.value.sideEffects + ? sResolved + : {asset: assetNode.id, symbol: s}, ); - assetNode.usedSymbols.add('*'); + reexportedSymbolsSource.set(s, outgoingDep); } - reexportedSymbols.set(s, outgoingDep); }); + } else if (sResolved) { + if (outgoingDepResolved === undefined) { + outgoingDepResolved = sResolved.asset; + } else { + if ( + outgoingDepResolved != null && + outgoingDepResolved != sResolved.asset + ) { + outgoingDepResolved = null; + } + } + outgoingDepReplacements.set(s, sResolved.symbol ?? s); } } + if ( + // TODO we currently can't replace async imports from + // (parcelRequire("4pwI8")).then(({ a: a })=>a); + // to + // (parcelRequire("4pwI8")).then((a)=>a); + // if symbolTarget == { a -> * } + outgoingDep.value.priority == Priority.sync && + outgoingDepResolved != null + ) { + replacements.set(outgoingDep.id, { + asset: outgoingDepResolved, + targets: outgoingDepReplacements, + }); + } else { + // Edge needs to be removed again + replacements.set(outgoingDep.id, null); + } } let errors: Array = []; for (let incomingDep of incomingDeps) { let incomingDepUsedSymbolsUpOld = incomingDep.usedSymbolsUp; - incomingDep.usedSymbolsUp = new Set(); + incomingDep.usedSymbolsUp = new Map(); let incomingDepSymbols = incomingDep.value.symbols; if (!incomingDepSymbols) continue; @@ -529,11 +602,23 @@ export class AssetGraphBuilder { assetSymbols == null || // Assume everything could be provided if symbols are cleared assetNode.value.bundleBehavior === BundleBehavior.isolated || assetNode.value.bundleBehavior === BundleBehavior.inline || - assetNode.usedSymbols.has(s) || - reexportedSymbols.has(s) || - s === '*' + s === '*' || + assetNode.usedSymbols.has(s) ) { - incomingDep.usedSymbolsUp.add(s); + incomingDep.usedSymbolsUp.set(s, { + asset: assetNode.id, + symbol: s, + }); + } else if (reexportedSymbols.has(s)) { + // Forward a reexport only if the current asset is side-effect free. + if (!assetNode.value.sideEffects) { + incomingDep.usedSymbolsUp.set(s, reexportedSymbols.get(s)); + } else { + incomingDep.usedSymbolsUp.set(s, { + asset: assetNode.id, + symbol: s, + }); + } } else if (!hasNamespaceReexport) { let loc = incomingDep.value.symbols?.get(s)?.loc; let [resolutionNodeId] = this.assetGraph.getNodeIdsConnectedFrom( @@ -571,7 +656,7 @@ export class AssetGraphBuilder { } } - if (!equalSet(incomingDepUsedSymbolsUpOld, incomingDep.usedSymbolsUp)) { + if (!equalMap(incomingDepUsedSymbolsUpOld, incomingDep.usedSymbolsUp)) { changedDeps.add(incomingDep); incomingDep.usedSymbolsUpDirtyUp = true; } @@ -604,7 +689,60 @@ export class AssetGraphBuilder { // This ensures a consistent ordering of these symbols when packaging. // See https://github.com/parcel-bundler/parcel/pull/8212 for (let dep of changedDeps) { - dep.usedSymbolsUp = new Set([...dep.usedSymbolsUp].sort()); + dep.usedSymbolsUp = new Map( + [...dep.usedSymbolsUp].sort(([a], [b]) => a.localeCompare(b)), + ); + } + + // Do after the fact to not disrupt traversal + for (let [dep, replacement] of replacements) { + if ( + process.env.PARCEL_BUILD_ENV !== 'production' && + // $FlowFixMe + process.env.PARCEL_SYMBOLS_CODESPLIT == false + ) { + break; + } + + let depNodeId = this.assetGraph.getNodeIdByContentKey(dep); + let depNode = nullthrows(this.assetGraph.getNode(depNodeId)); + invariant(depNode.type === 'dependency'); + if (replacement) { + let {asset, targets} = replacement; + let assetParents = this.assetGraph.getNodeIdsConnectedTo( + this.assetGraph.getNodeIdByContentKey(asset), + ); + let assetGroupId; + if (assetParents.length === 1) { + [assetGroupId] = assetParents; + let type = this.assetGraph.getNode(assetGroupId)?.type; + // Parent is either an asset group, or a dependency when transformer returned multiple + // connected assets. + if (type !== 'asset_group') { + invariant(type === 'dependency'); + assetGroupId = undefined; + } + } + + this.assetGraph.addEdge( + depNodeId, + assetGroupId ?? this.assetGraph.getNodeIdByContentKey(asset), + assetGraphEdgeTypes.redirected, + ); + depNode.symbolTarget = targets; + } else { + for (let n of this.assetGraph.getNodeIdsConnectedFrom( + depNodeId, + assetGraphEdgeTypes.redirected, + )) { + this.assetGraph.removeEdge( + depNodeId, + n, + assetGraphEdgeTypes.redirected, + ); + } + depNode.symbolTarget = null; + } } } @@ -893,6 +1031,19 @@ export class AssetGraphBuilder { } } +function equalMap( + a: $ReadOnlyMap, + b: $ReadOnlyMap, +) { + if (a.size !== b.size) return false; + for (let [k, v] of a) { + if (!b.has(k)) return false; + let vB = b.get(k); + if (vB?.asset !== v?.asset || vB?.symbol !== v?.symbol) return false; + } + return true; +} + function equalSet(a: $ReadOnlySet, b: $ReadOnlySet) { return a.size === b.size && [...a].every(i => b.has(i)); } diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 3acd4df21eb..e826f50f52a 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -307,7 +307,8 @@ export type DependencyNode = {| /** dependency was deferred (= no used symbols (in immediate parents) & side-effect free) */ hasDeferred?: boolean, usedSymbolsDown: Set, - usedSymbolsUp: Set, + // a requested symbol -> the asset it resolved to, and the potentially renamed export name (null if external) + usedSymbolsUp: Map, /** for the "down" pass, the dependency resolution asset needs to be updated */ usedSymbolsDownDirty: boolean, /** for the "up" pass, the parent asset needs to be updated */ @@ -316,6 +317,9 @@ export type DependencyNode = {| usedSymbolsUpDirtyDown: boolean, /** dependency was excluded (= no used symbols (globally) & side-effect free) */ excluded: boolean, + /** a dependency importing a single symbol is rewritten to point to the reexported target asset + * instead, this is the name of the export (might have been renamed by reexports) */ + symbolTarget: ?Map, |}; export type RootNode = {|id: ContentKey, +type: 'root', value: string | null|}; diff --git a/packages/core/integration-tests/test/integration/resolver-canDefer/index.js b/packages/core/integration-tests/test/integration/resolver-canDefer/index.js index f8f5ca007e9..381a65c3b7c 100644 --- a/packages/core/integration-tests/test/integration/resolver-canDefer/index.js +++ b/packages/core/integration-tests/test/integration/resolver-canDefer/index.js @@ -1,3 +1,3 @@ -import {a} from "./library"; +import {a, index} from "./library"; -output = a; +output = [a, index]; diff --git a/packages/core/integration-tests/test/integration/resolver-canDefer/library/index.js b/packages/core/integration-tests/test/integration/resolver-canDefer/library/index.js index cd00ec2d483..86922c23686 100644 --- a/packages/core/integration-tests/test/integration/resolver-canDefer/library/index.js +++ b/packages/core/integration-tests/test/integration/resolver-canDefer/library/index.js @@ -2,3 +2,4 @@ sideEffect("index"); export {a} from "./a.js"; export {b} from "./b.js"; export {c} from "./c.js"; +export const index = "Index"; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/index.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/index.js new file mode 100644 index 00000000000..3888e43d6b2 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/index.js @@ -0,0 +1,3 @@ +import { Context } from "./library"; + +output = [Context, () => import("./library/dynamic")]; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/a.js new file mode 100644 index 00000000000..9eda04b81ad --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/a.js @@ -0,0 +1,2 @@ +sideEffect("a"); +export const Ctx = 1; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/b.js new file mode 100644 index 00000000000..18b74695dea --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/b.js @@ -0,0 +1,2 @@ +sideEffect("b"); +export const Ctx = 2; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/dynamic.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/dynamic.js new file mode 100644 index 00000000000..74c81826df6 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/dynamic.js @@ -0,0 +1,4 @@ +sideEffect("dynamic"); +import { Ctx } from "./a.js"; +import { id } from "./index.js"; +export default [Ctx, id]; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/index.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/index.js new file mode 100644 index 00000000000..21b2f50e4ab --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/index.js @@ -0,0 +1,4 @@ +sideEffect("index"); +export { Ctx } from "./a.js"; +export { Ctx as Context } from "./b.js"; +export const id = 3; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/package.json b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/package.json new file mode 100644 index 00000000000..1b95642997c --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/library/package.json @@ -0,0 +1,3 @@ +{ + "sideEffects": false +} diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index 8cedde288af..348cc87216b 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -6246,7 +6246,7 @@ describe('javascript', function () { }, { type: 'js', - assets: ['index.js', 'a.js', 'b1.js'], + assets: ['index.js', 'b1.js'], }, { type: 'css', @@ -6291,7 +6291,7 @@ describe('javascript', function () { }, { type: 'js', - assets: ['index.js', 'a.js', 'b1.js'], + assets: ['index.js', 'b1.js'], }, ]); @@ -6320,7 +6320,21 @@ describe('javascript', function () { options, ); - assertDependencyWasExcluded(b, 'index.js', './message2.js'); + assertBundles(b, [ + { + type: 'js', + assets: usesSymbolPropagation + ? ['a.js', 'message1.js'] + : [ + 'a.js', + 'esmodule-helpers.js', + 'index.js', + 'message1.js', + 'message3.js', + ], + }, + ]); + if (usesSymbolPropagation) { // TODO this only excluded, but should be deferred. assert(!findAsset(b, 'message3.js')); @@ -6403,10 +6417,24 @@ describe('javascript', function () { options, ); + assertBundles(b, [ + { + type: 'js', + assets: usesSymbolPropagation + ? ['c.js', 'message3.js'] + : [ + 'c.js', + 'esmodule-helpers.js', + 'index.js', + 'message1.js', + 'message3.js', + ], + }, + ]); + if (usesSymbolPropagation) { assert(!findAsset(b, 'message1.js')); } - assertDependencyWasExcluded(b, 'index.js', './message2.js'); let calls = []; let res = await run( @@ -6707,11 +6735,9 @@ describe('javascript', function () { ); assert(!called, 'side effect called'); assert.deepEqual(res.output, 4); - assertDependencyWasExcluded( - bundleEvent.bundleGraph, - 'index.js', - './bar', - ); + if (usesSymbolPropagation) { + assert(!findAsset(bundleEvent.bundleGraph, 'index.js')); + } await overlayFS.mkdirp(path.join(testDir, 'node_modules/bar')); await overlayFS.copyFile( @@ -7082,10 +7108,7 @@ describe('javascript', function () { ); if (usesSymbolPropagation) { - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); + assert(!findAsset(b, 'index.js')); assert.deepStrictEqual( new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), new Set(['default']), @@ -7120,10 +7143,7 @@ describe('javascript', function () { ); if (usesSymbolPropagation) { - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); + assert(!findAsset(b, 'index.js')); assert.deepStrictEqual( new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), new Set(['bar']), @@ -7158,10 +7178,7 @@ describe('javascript', function () { ); if (usesSymbolPropagation) { - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); + assert(!findAsset(b, 'index.js')); assert.deepStrictEqual( new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), new Set(['default', 'bar']), @@ -7186,6 +7203,43 @@ describe('javascript', function () { assert.deepEqual(res.output, ['foo', 'bar']); }); + it('supports partially used reexporting index file', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-partially-used/index.js', + ), + options, + ); + + let calls = []; + let res = ( + await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ) + ).output; + + let [v, async] = res; + + assert.deepEqual(calls, shouldScopeHoist ? ['b'] : ['a', 'b', 'index']); + assert.deepEqual(v, 2); + + v = await async(); + assert.deepEqual( + calls, + shouldScopeHoist + ? ['b', 'a', 'index', 'dynamic'] + : ['a', 'b', 'index', 'dynamic'], + ); + assert.deepEqual(v.default, [1, 3]); + }); + it('supports deferring non-weak dependencies that are not used', async function () { let b = await bundle( path.join( @@ -7269,17 +7323,12 @@ describe('javascript', function () { if (usesSymbolPropagation) { assert(!findAsset(b, 'esm.js')); + assert(!findAsset(b, 'index.js')); assert.deepStrictEqual( new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'commonjs.js')))), // the exports object is used freely new Set(['*', 'message2']), ); - assert.deepEqual( - new Set( - b.getUsedSymbols(findDependency(b, 'index.js', './commonjs.js')), - ), - new Set(['message2']), - ); } let calls = []; diff --git a/packages/core/integration-tests/test/plugin.js b/packages/core/integration-tests/test/plugin.js index 5b86ca5fbe9..7472e28e8f8 100644 --- a/packages/core/integration-tests/test/plugin.js +++ b/packages/core/integration-tests/test/plugin.js @@ -123,10 +123,7 @@ parcel-transformer-b`, }, ); - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); + assert.deepStrictEqual(!findAsset(b, 'index.js')); assert.deepStrictEqual( new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'a.js')))), new Set(['a']), diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index ab0927df6f6..dd918b9242d 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -6,7 +6,6 @@ import {createWorkerFarm} from '@parcel/core'; import {md} from '@parcel/diagnostic'; import { assertBundles, - assertDependencyWasExcluded, bundle as _bundle, bundler as _bundler, distDir, @@ -2441,12 +2440,6 @@ describe('scope hoisting', function () { let output = await run(bundleEvent.bundleGraph); assert.deepEqual(output, [123]); - assertDependencyWasExcluded( - bundleEvent.bundleGraph, - 'a.js', - './c.js', - ); - await overlayFS.copyFile( path.join(testDir, 'index.2.js'), path.join(testDir, 'index.js'), @@ -2674,18 +2667,6 @@ describe('scope hoisting', function () { ), new Set(['gridSize']), ); - assert.deepStrictEqual( - new Set( - bundleEvent.bundleGraph.getUsedSymbols( - findDependency( - bundleEvent.bundleGraph, - 'theme.js', - './themeColors', - ), - ), - ), - new Set(), - ); assert(!findAsset(bundleEvent.bundleGraph, 'themeColors.js')); await overlayFS.copyFile( @@ -2725,6 +2706,7 @@ describe('scope hoisting', function () { ), new Set('*'), ); + assert(findAsset(bundleEvent.bundleGraph, 'themeColors.js')); await overlayFS.copyFile( path.join(testDir, 'index.1.js'), @@ -2748,18 +2730,7 @@ describe('scope hoisting', function () { ), new Set(['gridSize']), ); - assert.deepStrictEqual( - new Set( - bundleEvent.bundleGraph.getUsedSymbols( - findDependency( - bundleEvent.bundleGraph, - 'theme.js', - './themeColors', - ), - ), - ), - new Set(), - ); + assert(!findAsset(bundleEvent.bundleGraph, 'themeColors.js')); } finally { await subscription.unsubscribe(); } diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 29aa68ee9fd..feb993bd59e 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -465,6 +465,29 @@ export interface MutableDependencySymbols // eslint-disable-next-line no-undef delete(exportSymbol: Symbol): void; } +export interface DependencySymbols // eslint-disable-next-line no-undef + extends Iterable< + [ + Symbol, + {|local: Symbol, loc: ?SourceLocation, isWeak: boolean, meta?: ?Meta|}, + ], + > { + /** + * The symbols taht are imports are unknown, rather than just empty. + * This is the default state. + */ + +isCleared: boolean; + get(exportSymbol: Symbol): ?{| + local: Symbol, + loc: ?SourceLocation, + isWeak: boolean, + meta?: ?Meta, + |}; + hasExportSymbol(exportSymbol: Symbol): boolean; + hasLocalSymbol(local: Symbol): boolean; + exportSymbols(): Iterable; +} + export type DependencyPriority = 'sync' | 'parallel' | 'lazy'; export type SpecifierType = 'commonjs' | 'esm' | 'url' | 'custom'; @@ -1449,6 +1472,7 @@ export interface BundleGraph { * Returns null if symbol propagation didn't run (so the result is unknown). */ getUsedSymbols(Asset | Dependency): ?$ReadOnlySet; + getSymbols(Dependency): DependencySymbols; /** Returns the common root directory for the entry assets of a target. */ getEntryRoot(target: Target): FilePath; } diff --git a/packages/packagers/css/src/CSSPackager.js b/packages/packagers/css/src/CSSPackager.js index c602e9044bf..eafc307c3dc 100644 --- a/packages/packagers/css/src/CSSPackager.js +++ b/packages/packagers/css/src/CSSPackager.js @@ -79,7 +79,9 @@ export default (new Packager({ if (asset.meta.hasReferences) { let replacements = new Map(); for (let dep of asset.getDependencies()) { - for (let [exported, {local}] of dep.symbols) { + for (let [exported, {local}] of bundleGraph.getSymbols( + dep, + )) { let resolved = bundleGraph.getResolvedAsset(dep, bundle); if (resolved) { let resolution = bundleGraph.getSymbolResolution( @@ -216,9 +218,11 @@ async function processCSSModule( let defaultImport = null; if (usedSymbols.has('default')) { let incoming = bundleGraph.getIncomingDependencies(asset); - defaultImport = incoming.find(d => d.symbols.hasExportSymbol('default')); + defaultImport = incoming.find(d => + bundleGraph.getSymbols(d).hasExportSymbol('default'), + ); if (defaultImport) { - let loc = defaultImport.symbols.get('default')?.loc; + let loc = bundleGraph.getSymbols(defaultImport).get('default')?.loc; logger.warn({ message: 'CSS modules cannot be tree shaken when imported with a default specifier', diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index b990058b634..92f4afa399f 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -620,7 +620,7 @@ ${code} continue; } - for (let [imported, {local}] of dep.symbols) { + for (let [imported, {local}] of this.bundleGraph.getSymbols(dep)) { if (local === '*') { continue; } @@ -695,7 +695,7 @@ ${code} this.externals.set(dep.specifier, external); } - for (let [imported, {local}] of dep.symbols) { + for (let [imported, {local}] of this.bundleGraph.getSymbols(dep)) { // If already imported, just add the already renamed variable to the mapping. let renamed = external.get(imported); if (renamed && local !== '*' && replacements) { @@ -1041,7 +1041,7 @@ ${code} let isWrapped = resolved && resolved.meta.shouldWrap; - for (let [imported, {local}] of dep.symbols) { + for (let [imported, {local}] of this.bundleGraph.getSymbols(dep)) { if (imported === '*' && local === '*') { if (!resolved) { // Re-exporting an external module. This should have already been handled in buildReplacements. @@ -1202,7 +1202,7 @@ ${code} dep => this.bundle.hasDependency(dep) && // dep.meta.isES6Module && - dep.symbols.hasExportSymbol('default'), + this.bundleGraph.getSymbols(dep).hasExportSymbol('default'), ); }