UNPKG

@parcel/core

Version:
795 lines (730 loc) • 27.8 kB
// @flow import type {ContentKey, NodeId} from '@parcel/graph'; import type {Meta, Symbol} from '@parcel/types'; import type {Diagnostic} from '@parcel/diagnostic'; import type { AssetNode, DependencyNode, InternalSourceLocation, ParcelOptions, } from './types'; import {type default as AssetGraph} from './AssetGraph'; import invariant from 'assert'; import nullthrows from 'nullthrows'; import {setEqual} from '@parcel/utils'; import logger from '@parcel/logger'; import {md, convertSourceLocationToHighlight} from '@parcel/diagnostic'; import {BundleBehavior} from './types'; import {fromProjectPathRelative, fromProjectPath} from './projectPath'; export function propagateSymbols({ options, assetGraph, changedAssetsPropagation, assetGroupsWithRemovedParents, previousErrors, }: {| options: ParcelOptions, assetGraph: AssetGraph, changedAssetsPropagation: Set<string>, assetGroupsWithRemovedParents: Set<NodeId>, previousErrors?: ?Map<NodeId, Array<Diagnostic>>, |}): Map<NodeId, Array<Diagnostic>> { let changedAssets = new Set( [...changedAssetsPropagation].map(id => assetGraph.getNodeIdByContentKey(id), ), ); // To reorder once at the end let changedDeps = new Set<DependencyNode>(); // For the down traversal, the nodes with `usedSymbolsDownDirty = true` are exactly // `changedAssetsPropagation` (= asset and therefore potentially dependencies changed) or the // asset children of `assetGroupsWithRemovedParents` (= fewer incoming dependencies causing less // used symbols). // // The up traversal has to consider all nodes that changed in the down traversal // (`useSymbolsUpDirtyDown = true`) which are listed in `changedDepsUsedSymbolsUpDirtyDown` // (more or less requested symbols) and in `changedAssetsPropagation` (changing an asset might // change exports). // The dependencies that changed in the down traversal causing an update in the up traversal. let changedDepsUsedSymbolsUpDirtyDown = new Set<ContentKey>(); // Propagate the requested symbols down from the root to the leaves propagateSymbolsDown( assetGraph, changedAssets, assetGroupsWithRemovedParents, (assetNode, incomingDeps, outgoingDeps) => { // exportSymbol -> identifier let assetSymbols: ?$ReadOnlyMap< Symbol, {|local: Symbol, loc: ?InternalSourceLocation, meta?: ?Meta|}, > = assetNode.value.symbols; // identifier -> exportSymbol let assetSymbolsInverse; if (assetSymbols) { assetSymbolsInverse = new Map<Symbol, Set<Symbol>>(); for (let [s, {local}] of assetSymbols) { let set = assetSymbolsInverse.get(local); if (!set) { set = new Set(); assetSymbolsInverse.set(local, set); } set.add(s); } } let hasNamespaceOutgoingDeps = outgoingDeps.some( d => d.value.symbols?.get('*')?.local === '*', ); // 1) Determine what the incomingDeps requests from the asset // ---------------------------------------------------------- let isEntry = false; let addAll = false; // Used symbols that are exported or reexported (symbol will be removed again later) by asset. assetNode.usedSymbols = new Set(); // Symbols that have to be namespace reexported by outgoingDeps. let namespaceReexportedSymbols = new Set<Symbol>(); if (incomingDeps.length === 0) { // Root in the runtimes Graph assetNode.usedSymbols.add('*'); namespaceReexportedSymbols.add('*'); } else { for (let incomingDep of incomingDeps) { if (incomingDep.value.symbols == null) { if (incomingDep.value.sourceAssetId == null) { // The root dependency on non-library builds isEntry = true; } else { // A regular dependency with cleared symbols addAll = true; } continue; } for (let exportSymbol of incomingDep.usedSymbolsDown) { if (exportSymbol === '*') { assetNode.usedSymbols.add('*'); namespaceReexportedSymbols.add('*'); } if ( !assetSymbols || assetSymbols.has(exportSymbol) || assetSymbols.has('*') ) { // An own symbol or a non-namespace reexport assetNode.usedSymbols.add(exportSymbol); } // A namespace reexport // (but only if we actually have namespace-exporting outgoing dependencies, // This usually happens with a reexporting asset with many namespace exports which means that // we cannot match up the correct asset with the used symbol at this level.) else if (hasNamespaceOutgoingDeps && exportSymbol !== 'default') { namespaceReexportedSymbols.add(exportSymbol); } } } } // Incomding dependency with cleared symbols, add everything if (addAll) { assetSymbols?.forEach((_, exportSymbol) => assetNode.usedSymbols.add(exportSymbol), ); } // 2) Distribute the symbols to the outgoing dependencies // ---------------------------------------------------------- for (let dep of outgoingDeps) { let depUsedSymbolsDownOld = dep.usedSymbolsDown; let depUsedSymbolsDown = new Set(); dep.usedSymbolsDown = depUsedSymbolsDown; if ( assetNode.value.sideEffects || // Incoming dependency with cleared symbols addAll || // For entries, we still need to add dep.value.symbols of the entry (which are "used" but not according to the symbols data) isEntry || // If not a single symbol is used, we can say the entire subgraph is not used. // This is e.g. needed when some symbol is imported and then used for a export which isn't used (= "semi-weak" reexport) // index.js: `import {bar} from "./lib"; ...` // lib/index.js: `export * from "./foo.js"; export * from "./bar.js";` // lib/foo.js: `import { data } from "./bar.js"; export const foo = data + " esm2";` assetNode.usedSymbols.size > 0 || namespaceReexportedSymbols.size > 0 ) { let depSymbols = dep.value.symbols; if (!depSymbols) continue; if (depSymbols.get('*')?.local === '*') { if (addAll) { depUsedSymbolsDown.add('*'); } else { for (let s of namespaceReexportedSymbols) { // We need to propagate the namespaceReexportedSymbols to all namespace dependencies (= even wrong ones because we don't know yet) depUsedSymbolsDown.add(s); } } } for (let [symbol, {local}] of depSymbols) { // Was already handled above if (local === '*') continue; if (!assetSymbolsInverse || !depSymbols.get(symbol)?.isWeak) { // Bailout or non-weak symbol (= used in the asset itself = not a reexport) depUsedSymbolsDown.add(symbol); } else { let reexportedExportSymbols = assetSymbolsInverse.get(local); if (reexportedExportSymbols == null) { // not reexported = used in asset itself depUsedSymbolsDown.add(symbol); } else if (assetNode.usedSymbols.has('*')) { // we need everything depUsedSymbolsDown.add(symbol); [...reexportedExportSymbols].forEach(s => assetNode.usedSymbols.delete(s), ); } else { let usedReexportedExportSymbols = [ ...reexportedExportSymbols, ].filter(s => assetNode.usedSymbols.has(s)); if (usedReexportedExportSymbols.length > 0) { // The symbol is indeed a reexport, so it's not used from the asset itself depUsedSymbolsDown.add(symbol); usedReexportedExportSymbols.forEach(s => assetNode.usedSymbols.delete(s), ); } } } } } else { depUsedSymbolsDown.clear(); } if (!setEqual(depUsedSymbolsDownOld, depUsedSymbolsDown)) { dep.usedSymbolsDownDirty = true; dep.usedSymbolsUpDirtyDown = true; changedDepsUsedSymbolsUpDirtyDown.add(dep.id); } if (dep.usedSymbolsUpDirtyDown) { // Set on node creation changedDepsUsedSymbolsUpDirtyDown.add(dep.id); } } }, ); const logFallbackNamespaceInsertion = ( assetNode, symbol: Symbol, depNode1, depNode2, ) => { if (options.logLevel === 'verbose') { logger.warn({ message: `${fromProjectPathRelative( assetNode.value.filePath, )} reexports "${symbol}", which could be resolved either to the dependency "${ depNode1.value.specifier }" or "${ depNode2.value.specifier }" at runtime. Adding a namespace object to fall back on.`, origin: '@parcel/core', }); } }; // Because namespace reexports introduce ambiguity, go up the graph from the leaves to the // root and remove requested symbols that aren't actually exported let errors = propagateSymbolsUp( assetGraph, changedAssets, changedDepsUsedSymbolsUpDirtyDown, previousErrors, (assetNode, incomingDeps, outgoingDeps) => { let assetSymbols: ?$ReadOnlyMap< Symbol, {|local: Symbol, loc: ?InternalSourceLocation, meta?: ?Meta|}, > = assetNode.value.symbols; let assetSymbolsInverse = null; if (assetSymbols) { assetSymbolsInverse = new Map<Symbol, Set<Symbol>>(); for (let [s, {local}] of assetSymbols) { let set = assetSymbolsInverse.get(local); if (!set) { set = new Set(); assetSymbolsInverse.set(local, set); } set.add(s); } } // 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<Symbol, DependencyNode>(); for (let outgoingDep of outgoingDeps) { let outgoingDepSymbols = outgoingDep.value.symbols; if (!outgoingDepSymbols) continue; let isExcluded = assetGraph.getNodeIdsConnectedFrom( assetGraph.getNodeIdByContentKey(outgoingDep.id), ).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((sResolved, s) => { if (s === 'default') { return; } // 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)) { 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, sResolved); reexportedSymbolsSource.set(s, outgoingDep); } }); } for (let [s, sResolved] of outgoingDep.usedSymbolsUp) { if (!outgoingDep.usedSymbolsDown.has(s)) { // usedSymbolsDown is a superset of usedSymbolsUp continue; } let local = outgoingDepSymbols.get(s)?.local; if (local == null) { // Caused by '*' => '*', already handled continue; } let reexported = assetSymbolsInverse?.get(local); if (reexported != null) { reexported.forEach(s => { // see same code above 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, sResolved); reexportedSymbolsSource.set(s, outgoingDep); } }); } } } let errors: Array<Diagnostic> = []; function usedSymbolsUpAmbiguous(old, current, s, value) { if (old.has(s)) { let valueOld = old.get(s); if ( valueOld !== value && !( valueOld?.asset === value.asset && valueOld?.symbol === value.symbol ) ) { // The dependency points to multiple assets (via an asset group). current.set(s, undefined); return; } } current.set(s, value); } for (let incomingDep of incomingDeps) { let incomingDepUsedSymbolsUpOld = incomingDep.usedSymbolsUp; incomingDep.usedSymbolsUp = new Map(); let incomingDepSymbols = incomingDep.value.symbols; if (!incomingDepSymbols) continue; let hasNamespaceReexport = incomingDepSymbols.get('*')?.local === '*'; for (let s of incomingDep.usedSymbolsDown) { if ( assetSymbols == null || // Assume everything could be provided if symbols are cleared assetNode.value.bundleBehavior === BundleBehavior.isolated || assetNode.value.bundleBehavior === BundleBehavior.inline || s === '*' || assetNode.usedSymbols.has(s) ) { usedSymbolsUpAmbiguous( incomingDepUsedSymbolsUpOld, incomingDep.usedSymbolsUp, s, { asset: assetNode.id, symbol: s, }, ); } else if (reexportedSymbols.has(s)) { let reexport = reexportedSymbols.get(s); let v = // Forward a reexport only if the current asset is side-effect free and not external !assetNode.value.sideEffects && reexport != null ? reexport : { asset: assetNode.id, symbol: s, }; usedSymbolsUpAmbiguous( incomingDepUsedSymbolsUpOld, incomingDep.usedSymbolsUp, s, v, ); } else if (!hasNamespaceReexport) { let loc = incomingDep.value.symbols?.get(s)?.loc; let [resolutionNodeId] = assetGraph.getNodeIdsConnectedFrom( assetGraph.getNodeIdByContentKey(incomingDep.id), ); let resolution = nullthrows(assetGraph.getNode(resolutionNodeId)); invariant( resolution && (resolution.type === 'asset_group' || resolution.type === 'asset'), ); errors.push({ message: md`${fromProjectPathRelative( resolution.value.filePath, )} does not export '${s}'`, origin: '@parcel/core', codeFrames: loc ? [ { filePath: fromProjectPath(options.projectRoot, loc?.filePath) ?? undefined, language: incomingDep.value.sourceAssetType ?? undefined, codeHighlights: [convertSourceLocationToHighlight(loc)], }, ] : undefined, }); } } if (!equalMap(incomingDepUsedSymbolsUpOld, incomingDep.usedSymbolsUp)) { changedDeps.add(incomingDep); incomingDep.usedSymbolsUpDirtyUp = true; } incomingDep.excluded = false; if ( incomingDep.value.symbols != null && incomingDep.usedSymbolsUp.size === 0 ) { let assetGroups = assetGraph.getNodeIdsConnectedFrom( assetGraph.getNodeIdByContentKey(incomingDep.id), ); if (assetGroups.length === 1) { let [assetGroupId] = assetGroups; let assetGroup = nullthrows(assetGraph.getNode(assetGroupId)); if ( assetGroup.type === 'asset_group' && assetGroup.value.sideEffects === false ) { incomingDep.excluded = true; } } else { invariant(assetGroups.length === 0); } } } return errors; }, ); // Sort usedSymbolsUp so they are a consistent order across builds. // 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 Map( [...dep.usedSymbolsUp].sort(([a], [b]) => a.localeCompare(b)), ); } return errors; } function propagateSymbolsDown( assetGraph: AssetGraph, changedAssets: Set<NodeId>, assetGroupsWithRemovedParents: Set<NodeId>, visit: ( assetNode: AssetNode, incoming: $ReadOnlyArray<DependencyNode>, outgoing: $ReadOnlyArray<DependencyNode>, ) => void, ) { if (changedAssets.size === 0 && assetGroupsWithRemovedParents.size === 0) { return; } // We care about changed assets and their changed dependencies. So start with the first changed // asset or dependency and continue while the symbols change. If the queue becomes empty, // continue with the next unvisited changed asset. // // In the end, nodes, which are neither listed in changedAssets nor in // assetGroupsWithRemovedParents nor reached via a dirty flag, don't have to be visited at all. // // In the worst case, some nodes have to be revisited because we don't want to sort the assets // into topological order. For example in a diamond graph where the join point is visited twice // via each parent (the numbers signifiying the order of re/visiting, `...` being unvisited). // However, this only continues as long as there are changes in the used symbols that influence // child nodes. // // | // ... // / \ // 1 4 // \ / // 2+5 // | // 3+6 // | // ... // | // let unreachedAssets = new Set([ ...changedAssets, ...assetGroupsWithRemovedParents, ]); let queue = new Set([setPop(unreachedAssets)]); while (queue.size > 0) { let queuedNodeId = setPop(queue); unreachedAssets.delete(queuedNodeId); let outgoing = assetGraph.getNodeIdsConnectedFrom(queuedNodeId); let node = nullthrows(assetGraph.getNode(queuedNodeId)); let wasNodeDirty = false; if (node.type === 'dependency' || node.type === 'asset_group') { wasNodeDirty = node.usedSymbolsDownDirty; node.usedSymbolsDownDirty = false; } else if (node.type === 'asset' && node.usedSymbolsDownDirty) { visit( node, assetGraph.getIncomingDependencies(node.value).map(d => { let dep = assetGraph.getNodeByContentKey(d.id); invariant(dep && dep.type === 'dependency'); return dep; }), outgoing.map(dep => { let depNode = nullthrows(assetGraph.getNode(dep)); invariant(depNode.type === 'dependency'); return depNode; }), ); node.usedSymbolsDownDirty = false; } for (let child of outgoing) { let childNode = nullthrows(assetGraph.getNode(child)); let childDirty = false; if ( (childNode.type === 'asset' || childNode.type === 'asset_group') && wasNodeDirty ) { childNode.usedSymbolsDownDirty = true; childDirty = true; } else if (childNode.type === 'dependency') { childDirty = childNode.usedSymbolsDownDirty; } if (childDirty) { queue.add(child); } } if (queue.size === 0 && unreachedAssets.size > 0) { queue.add(setPop(unreachedAssets)); } } } function propagateSymbolsUp( assetGraph: AssetGraph, changedAssets: Set<NodeId>, changedDepsUsedSymbolsUpDirtyDown: Set<ContentKey>, previousErrors: ?Map<NodeId, Array<Diagnostic>>, visit: ( assetNode: AssetNode, incoming: $ReadOnlyArray<DependencyNode>, outgoing: $ReadOnlyArray<DependencyNode>, ) => Array<Diagnostic>, ): Map<NodeId, Array<Diagnostic>> { // For graphs in general (so with cyclic dependencies), some nodes will have to be revisited. So // run a regular queue-based BFS for anything that's still dirty. // // (Previously, there was first a recursive post-order DFS, with the idea that all children of a // node should be processed first. With a tree, this would result in a minimal amount of work by // processing every asset exactly once and then the remaining cycles would have been handled // with the loop. This was slightly faster for initial builds but had O(project) instead of // O(changes).) let errors: Map<NodeId, Array<Diagnostic>> = previousErrors ? // Some nodes might have been removed since the last build new Map([...previousErrors].filter(([n]) => assetGraph.hasNode(n))) : new Map(); let changedDepsUsedSymbolsUpDirtyDownAssets = new Set([ ...[...changedDepsUsedSymbolsUpDirtyDown] .reverse() .flatMap(id => getDependencyResolution(assetGraph, id)), ...changedAssets, ]); // Do a more efficient full traversal (less recomputations) if more than half of the assets // changed. let runFullPass = // If there are n nodes in the graph, then the asset count is approximately // n/6 (for every asset, there are ~4 dependencies and ~1 asset_group). assetGraph.nodes.length * (1 / 6) * 0.5 < changedDepsUsedSymbolsUpDirtyDownAssets.size; let dirtyDeps; if (runFullPass) { dirtyDeps = new Set<NodeId>(); let rootNodeId = nullthrows( assetGraph.rootNodeId, 'A root node is required to traverse', ); const nodeVisitor = nodeId => { let node = nullthrows(assetGraph.getNode(nodeId)); let outgoing = assetGraph.getNodeIdsConnectedFrom(nodeId); for (let childId of outgoing) { let child = nullthrows(assetGraph.getNode(childId)); if (node.type === 'asset') { invariant(child.type === 'dependency'); if (child.usedSymbolsUpDirtyUp) { node.usedSymbolsUpDirty = true; child.usedSymbolsUpDirtyUp = false; } } } if (node.type === 'asset') { let incoming = assetGraph.getIncomingDependencies(node.value).map(d => { let n = assetGraph.getNodeByContentKey(d.id); invariant(n && n.type === 'dependency'); return n; }); for (let dep of incoming) { if (dep.usedSymbolsUpDirtyDown) { dep.usedSymbolsUpDirtyDown = false; node.usedSymbolsUpDirty = true; } } if (node.usedSymbolsUpDirty) { let e = visit( node, incoming, outgoing.map(depNodeId => { let depNode = nullthrows(assetGraph.getNode(depNodeId)); invariant(depNode.type === 'dependency'); return depNode; }), ); if (e.length > 0) { node.usedSymbolsUpDirty = true; errors.set(nodeId, e); } else { node.usedSymbolsUpDirty = false; errors.delete(nodeId); } } } else { if (node.type === 'dependency') { if (node.usedSymbolsUpDirtyUp) { dirtyDeps.add(nodeId); } else { dirtyDeps.delete(nodeId); } } } }; assetGraph.postOrderDfsFast(nodeVisitor, rootNodeId); } let queue = dirtyDeps ?? changedDepsUsedSymbolsUpDirtyDownAssets; while (queue.size > 0) { let queuedNodeId = setPop(queue); let node = nullthrows(assetGraph.getNode(queuedNodeId)); if (node.type === 'asset') { let incoming = assetGraph.getIncomingDependencies(node.value).map(dep => { let depNode = assetGraph.getNodeByContentKey(dep.id); invariant(depNode && depNode.type === 'dependency'); return depNode; }); for (let dep of incoming) { if (dep.usedSymbolsUpDirtyDown) { dep.usedSymbolsUpDirtyDown = false; node.usedSymbolsUpDirty = true; } } let outgoing = assetGraph .getNodeIdsConnectedFrom(queuedNodeId) .map(depNodeId => { let depNode = nullthrows(assetGraph.getNode(depNodeId)); invariant(depNode.type === 'dependency'); return depNode; }); for (let dep of outgoing) { if (dep.usedSymbolsUpDirtyUp) { node.usedSymbolsUpDirty = true; dep.usedSymbolsUpDirtyUp = false; } } if (node.usedSymbolsUpDirty) { let e = visit(node, incoming, outgoing); if (e.length > 0) { node.usedSymbolsUpDirty = true; errors.set(queuedNodeId, e); } else { node.usedSymbolsUpDirty = false; errors.delete(queuedNodeId); } } for (let i of incoming) { if (i.usedSymbolsUpDirtyUp) { queue.add(assetGraph.getNodeIdByContentKey(i.id)); } } } else { let connectedNodes = assetGraph.getNodeIdsConnectedTo(queuedNodeId); if (connectedNodes.length > 0) { queue.add(...connectedNodes); } } } return errors; } function getDependencyResolution( graph: AssetGraph, depId: ContentKey, ): Array<NodeId> { let depNodeId = graph.getNodeIdByContentKey(depId); let connected = graph.getNodeIdsConnectedFrom(depNodeId); invariant(connected.length <= 1); let child = connected[0]; if (child) { let childNode = nullthrows(graph.getNode(child)); if (childNode.type === 'asset_group') { return graph.getNodeIdsConnectedFrom(child); } else { return [child]; } } return []; } function equalMap<K>( a: $ReadOnlyMap<K, ?{|asset: ContentKey, symbol: ?Symbol|}>, b: $ReadOnlyMap<K, ?{|asset: ContentKey, symbol: ?Symbol|}>, ) { 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 setPop<T>(set: Set<T>): T { let v = nullthrows(set.values().next().value); set.delete(v); return v; }