diff --git a/packages/bundlers/default/src/DefaultBundler.js b/packages/bundlers/default/src/DefaultBundler.js index 65afe85250b..cafc643e017 100644 --- a/packages/bundlers/default/src/DefaultBundler.js +++ b/packages/bundlers/default/src/DefaultBundler.js @@ -1,6 +1,6 @@ // @flow strict-local -import type {Asset, Bundle, MutableBundleGraph} from '@parcel/types'; +import type {Asset, Bundle, MutableBundleGraph, Symbol} from '@parcel/types'; import invariant from 'assert'; import {Bundler} from '@parcel/plugin'; @@ -270,6 +270,53 @@ export default new Bundler({ deduplicateBundle(bundleGraph, sharedBundle); } }, + propagate({bundleGraph}) { + // TODO put this in a separate plugin + + let usedExports: Map> = new Map(); + let processedDeps = new Set(); + bundleGraph.traverseBundles({ + enter(bundle) { + setUsedExports(usedExports, processedDeps, bundle, bundleGraph); + }, + exit(bundle) { + bundle.traverse((node, shouldWrap) => { + switch (node.type) { + case 'dependency': + // Mark assets that should be wrapped, based on metadata in the incoming dependency tree + if (shouldWrap || Boolean(node.value.meta.shouldWrap)) { + let resolved = bundleGraph.getDependencyResolution( + node.value, + bundle, + ); + if (resolved) { + resolved.meta.shouldWrap = true; + } + return true; + } + break; + case 'asset': { + let assetUsedExports = usedExports.get(node.value.id); + if (assetUsedExports) { + let wildcardUsed = assetUsedExports.has('*'); + if (wildcardUsed) { + let assetExports = node.value.meta.exportsIdentifier; + invariant(typeof assetExports === 'string'); + node.value.exportedSymbols.set('*', assetExports); + } + for (let [symbol, id] of node.value.symbols) { + if (assetUsedExports.has(symbol)) { + node.value.exportedSymbols.set(symbol, id); + } + } + } + break; + } + } + }); + }, + }); + }, }); function deduplicateBundle(bundleGraph: MutableBundleGraph, bundle: Bundle) { @@ -297,3 +344,91 @@ function deduplicateBundle(bundleGraph: MutableBundleGraph, bundle: Bundle) { } }); } + +// ---------------------------------------------------------- + +function setUsedExports( + usedExports, + processedDeps, + bundle, + bundleGraph, +): Map> { + bundle.traverseAssets(asset => { + // TODO Entry assets should export eveything?! + for (let dep of bundleGraph.getIncomingDependencies(asset)) { + if (processedDeps.has(dep.id)) continue; + if (dep.isEntry) { + processedDeps.add(dep.id); + for (let {asset: symbolAsset, exportSymbol} of bundleGraph.getSymbols( + asset, + )) { + if (exportSymbol != null) { + markUsed(symbolAsset, exportSymbol); + markUsedEntry(asset, exportSymbol); // mark in the asset itself because it is an entry + } + } + } + } + + for (let dep of bundleGraph.getDependencies(asset)) { + if (processedDeps.has(dep.id)) continue; + processedDeps.add(dep.id); + + let resolvedAsset = bundleGraph.getDependencyResolution(dep, bundle); + if (!resolvedAsset) { + continue; + } + for (let [symbol, identifier] of dep.symbols) { + if (identifier === '*') { + continue; + } + + if (symbol === '*') { + for (let {asset: symbolAsset, exportSymbol} of bundleGraph.getSymbols( + resolvedAsset, + )) { + if (exportSymbol != null) { + markUsed(symbolAsset, exportSymbol); + } + } + } + + markUsed(resolvedAsset, symbol); + } + } + + // // If the asset is referenced by another bundle, include all exports. + // if (bundleGraph.isAssetReferencedByAnotherBundleOfType(asset, 'js')) { + // markUsed(asset, '*'); + // for (let {asset: a, symbol} of bundleGraph.getExportedSymbols(asset)) { + // if (symbol != null) { + // markUsed(a, symbol); + // } + // } + // } + }); + + // TODO merge these two functions + function markUsedEntry(asset, symbol) { + let used = usedExports.get(asset.id); + if (!used) { + used = new Set(); + usedExports.set(asset.id, used); + } + + used.add(symbol); + } + + function markUsed(asset, symbol) { + let resolved = bundleGraph.resolveSymbol(asset, symbol); + let used = usedExports.get(resolved.asset.id); + if (!used) { + used = new Set(); + usedExports.set(resolved.asset.id, used); + } + + used.add(resolved.exportSymbol); + } + + return usedExports; +} diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 59ec9704e37..93f98483631 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -39,6 +39,12 @@ type BundleGraphEdgeTypes = // Using this type prevents referenced assets from being traversed normally. | 'references'; +type ResolvedSymbol = {| + asset: Asset, + exportSymbol: Symbol, + symbol: ?Symbol, +|}; + export default class BundleGraph { // TODO: These hashes are being invalidated in mutative methods, but this._graph is not a private // property so it is possible to reach in and mutate the graph without invalidating these hashes. @@ -178,13 +184,13 @@ export default class BundleGraph { getDependencyAssets(dependency: Dependency): Array { let dependencyNode = nullthrows(this._graph.getNode(dependency.id)); - return this._graph - .getNodesConnectedFrom(dependencyNode) - .filter(node => node.type === 'asset') - .map(node => { - invariant(node.type === 'asset'); - return node.value; - }); + let assets = []; + this.traverseContents((node, _, traverse) => { + if (node.type === 'dependency') { + if (node.id !== dependency.id) traverse.skipChildren(); + } else assets.push(node.value); + }, dependencyNode); + return assets; } getDependencyResolution(dep: Dependency, bundle?: Bundle): ?Asset { @@ -204,6 +210,7 @@ export default class BundleGraph { firstAsset; // If a resolution still hasn't been found, return the first referenced asset. + // TODO still will never work? if (resolved == null) { this._graph.traverse( (node, _, traversal) => { @@ -401,11 +408,13 @@ export default class BundleGraph { traverseContents( visit: GraphVisitor, + startNode?: BundleGraphNode, ): ?TContext { return this._graph.filteredTraverse( node => node.type === 'asset' || node.type === 'dependency' ? node : null, visit, + startNode, ); } @@ -527,7 +536,7 @@ export default class BundleGraph { ); } - resolveSymbol(asset: Asset, symbol: Symbol) { + resolveSymbol(asset: Asset, symbol: Symbol): ResolvedSymbol { let identifier = asset.symbols.get(symbol); if (symbol === '*') { return {asset, exportSymbol: '*', symbol: identifier}; @@ -584,13 +593,41 @@ export default class BundleGraph { return {asset, exportSymbol: symbol, symbol: identifier}; } - getExportedSymbols(asset: Asset) { + // all symbols that this asset is (in)directly exporting + getSymbols(asset: Asset): Array { let symbols = []; for (let symbol of asset.symbols.keys()) { symbols.push(this.resolveSymbol(asset, symbol)); } + let deps = this.getDependencies(asset); + for (let dep of deps) { + if (dep.symbols.get('*') === '*') { + let resolved = nullthrows(this.getDependencyResolution(dep)); + let exported = this.getSymbols(resolved).filter( + s => s.exportSymbol !== 'default', + ); + symbols.push(...exported); + } + } + + return symbols; + } + + // the used symbols that this asset is exporting + getExportedSymbols(asset: Asset): Array { + let symbols = []; + + for (let [exportSymbol, symbol] of asset.exportedSymbols) { + //TODO ? + if (exportSymbol === '*') { + symbols.push({asset: asset, exportSymbol, symbol}); + } else { + symbols.push(this.resolveSymbol(asset, exportSymbol)); + } + } + let deps = this.getDependencies(asset); for (let dep of deps) { if (dep.symbols.get('*') === '*') { diff --git a/packages/core/core/src/BundlerRunner.js b/packages/core/core/src/BundlerRunner.js index ef1053b80cb..ae198b90b10 100644 --- a/packages/core/core/src/BundlerRunner.js +++ b/packages/core/core/src/BundlerRunner.js @@ -119,6 +119,18 @@ export default class BundlerRunner { assertSignalNotAborted(signal); await dumpGraphToGraphViz(bundleGraph, 'after_runtimes'); + try { + await bundler.propagate({ + bundleGraph: new BundleGraph(internalBundleGraph, this.options), + options: this.pluginOptions, + logger: new PluginLogger({origin: this.config.getBundlerName()}), + }); + } catch (e) { + throw new ThrowableDiagnostic({ + diagnostic: errorToDiagnostic(e, this.config.getBundlerName()), + }); + } + if (cacheKey != null) { await this.options.cache.set(cacheKey, internalBundleGraph); } diff --git a/packages/core/core/src/InternalAsset.js b/packages/core/core/src/InternalAsset.js index b57f3571bcc..fd2ed2fe1b4 100644 --- a/packages/core/core/src/InternalAsset.js +++ b/packages/core/core/src/InternalAsset.js @@ -61,6 +61,7 @@ type AssetOptions = {| pipeline?: ?string, stats: Stats, symbols?: Map, + exportedSymbols?: Map, sideEffects?: boolean, uniqueKey?: ?string, plugin?: PackageName, @@ -96,6 +97,7 @@ export function createAsset(options: AssetOptions): Asset { meta: options.meta || {}, stats: options.stats, symbols: options.symbols || new Map(), + exportedSymbols: options.exportedSymbols || new Map(), sideEffects: options.sideEffects != null ? options.sideEffects : true, uniqueKey: uniqueKey, plugin: options.plugin, diff --git a/packages/core/core/src/applyRuntimes.js b/packages/core/core/src/applyRuntimes.js index 19ef6dfdfe2..f8467147c81 100644 --- a/packages/core/core/src/applyRuntimes.js +++ b/packages/core/core/src/applyRuntimes.js @@ -15,7 +15,11 @@ import type PluginOptions from './public/PluginOptions'; import assert from 'assert'; import invariant from 'assert'; import nullthrows from 'nullthrows'; -import AssetGraph, {nodeFromAssetGroup} from './AssetGraph'; +import AssetGraph, {nodeFromAssetGroup, nodeFromDep} from './AssetGraph'; +import {createDependency} from './Dependency'; +import {dependencyToInternalDependency} from './public/Dependency'; +import {environmentToInternalEnvironment} from './public/Environment'; +import {targetToInternalTarget} from './public/Target'; import BundleGraph from './public/BundleGraph'; import {removeAssetGroups} from './BundleGraph'; import {NamedBundle} from './public/Bundle'; @@ -27,7 +31,8 @@ import {HASH_REF_PREFIX, HASH_REF_REGEX} from './constants'; type RuntimeConnection = {| bundle: InternalBundle, assetRequest: AssetRequestDesc, - dependency: ?Dependency, + dependencyFrom: ?Dependency, + dependencyReplace: ?Dependency, isEntry: ?boolean, |}; @@ -60,7 +65,13 @@ export default async function applyRuntimes({ if (applied) { let runtimeAssets = Array.isArray(applied) ? applied : [applied]; - for (let {code, dependency, filePath, isEntry} of runtimeAssets) { + for (let { + code, + dependencyFrom, + dependencyReplace, + filePath, + isEntry, + } of runtimeAssets) { let assetRequest = { code, filePath, @@ -69,7 +80,8 @@ export default async function applyRuntimes({ connections.push({ bundle, assetRequest, - dependency: dependency, + dependencyFrom, + dependencyReplace, isEntry, }); let hashRefs = code.match(HASH_REF_REGEX) ?? []; @@ -100,7 +112,13 @@ export default async function applyRuntimes({ // the node to it. bundleGraph._graph.merge(runtimesGraph); - for (let {bundle, assetRequest, dependency, isEntry} of connections) { + for (let { + bundle, + assetRequest, + dependencyFrom, + dependencyReplace, + isEntry, + } of connections) { let assetGroupNode = nodeFromAssetGroup(assetRequest); let assetGroupAssets = runtimesAssetGraph.getNodesConnectedFrom( assetGroupNode, @@ -147,15 +165,53 @@ export default async function applyRuntimes({ bundle.entryAssetIds.unshift(runtimeNode.id); } - if (dependency == null) { - // Verify this asset won't become an island - assert( - bundleGraph._graph.getNodesConnectedTo(runtimeNode).length > 0, - 'Runtime must have an inbound dependency or be an entry', + if (dependencyFrom != null) { + bundleGraph._graph.addEdge(dependencyFrom.id, runtimeNode.id); + } + + if (dependencyReplace != null) { + let [dst] = bundleGraph._graph.getNodesConnectedFrom( + nodeFromDep(dependencyToInternalDependency(dependencyReplace)), + ); + dependencyToInternalDependency(dependencyReplace).isAsync = false; + + bundleGraph._graph.removeEdge(dependencyReplace.id, dst.id); + + let newDep = bundleGraph._graph.addNode( + nodeFromDep( + createDependency({ + sourceAssetId: runtimeNode.id, + sourcePath: runtimeNode.value.filePath, + moduleSpecifier: dependencyReplace.moduleSpecifier, + env: environmentToInternalEnvironment(dependencyReplace.env), + target: + // $FlowFixMe + dependencyReplace.target && + targetToInternalTarget(dependencyReplace.target), + isAsync: dependencyReplace.isAsync, + isEntry: dependencyReplace.isEntry, + isOptional: dependencyReplace.isOptional, + isURL: dependencyReplace.isURL, + isWeak: dependencyReplace.isWeak, + // $FlowFixMe + loc: dependencyReplace.loc, + meta: dependencyReplace.meta, + symbols: dependencyReplace.symbols, + pipeline: dependencyReplace.pipeline, + }), + ), ); - } else { - bundleGraph._graph.addEdge(dependency.id, runtimeNode.id); + bundleGraph._graph.addEdge(bundle.id, newDep.id, 'contains'); + bundleGraph._graph.addEdge(dependencyReplace.id, runtimeNode.id); + bundleGraph._graph.addEdge(runtimeNode.id, newDep.id); + bundleGraph._graph.addEdge(newDep.id, dst.id); } + + // Verify this asset won't become an island + assert( + bundleGraph._graph.getNodesConnectedTo(runtimeNode).length > 0, + 'Runtime must have an inbound dependency or be an entry', + ); } for (let {from, to} of bundleReferences) { diff --git a/packages/core/core/src/dumpGraphToGraphViz.js b/packages/core/core/src/dumpGraphToGraphViz.js index bef511c6f16..e01f14cc68d 100644 --- a/packages/core/core/src/dumpGraphToGraphViz.js +++ b/packages/core/core/src/dumpGraphToGraphViz.js @@ -54,8 +54,13 @@ export default async function dumpGraphToGraphViz( if (node.value.isDeferred) parts.push('deferred'); if (parts.length) label += ' (' + parts.join(', ') + ')'; if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; + label += + '\n' + [...node.value.symbols].map(([a, b]) => `${a}:${b}`).join(', '); } else if (node.type === 'asset') { label += path.basename(node.value.filePath) + '#' + node.value.type; + label += + '\n' + + [...node.value.exportedSymbols].map(([a, b]) => `${a}:${b}`).join(', '); } else if (node.type === 'asset_group') { if (node.deferred) label += '(deferred)'; } else if (node.type === 'file') { diff --git a/packages/core/core/src/public/Asset.js b/packages/core/core/src/public/Asset.js index da018381fb8..a223e913163 100644 --- a/packages/core/core/src/public/Asset.js +++ b/packages/core/core/src/public/Asset.js @@ -112,6 +112,10 @@ class BaseAsset { return this.#asset.value.symbols; } + get exportedSymbols(): Map { + return this.#asset.value.exportedSymbols; + } + get uniqueKey(): ?string { return this.#asset.value.uniqueKey; } diff --git a/packages/core/core/src/public/Bundle.js b/packages/core/core/src/public/Bundle.js index 81e0c60e292..8bd75e883db 100644 --- a/packages/core/core/src/public/Bundle.js +++ b/packages/core/core/src/public/Bundle.js @@ -8,10 +8,10 @@ import type { BundleTraversable, Environment as IEnvironment, FilePath, + GraphVisitor, NamedBundle as INamedBundle, Stats, Target as ITarget, - GraphVisitor, } from '@parcel/types'; import type BundleGraph from '../BundleGraph'; diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index 9b1ee4d5573..d147f836e87 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -6,7 +6,7 @@ import type { BundleGraph as IBundleGraph, BundleGroup, Dependency as IDependency, - GraphTraversalCallback, + GraphVisitor, Symbol, SymbolResolution, } from '@parcel/types'; @@ -182,6 +182,15 @@ export default class BundleGraph implements IBundleGraph { }; } + getSymbols(asset: IAsset): Array { + let res = this.#graph.getSymbols(assetToInternalAsset(asset).value); + return res.map(e => ({ + asset: assetFromValue(e.asset, this.#options), + exportSymbol: e.exportSymbol, + symbol: e.symbol, + })); + } + getExportedSymbols(asset: IAsset): Array { let res = this.#graph.getExportedSymbols(assetToInternalAsset(asset).value); return res.map(e => ({ @@ -192,7 +201,7 @@ export default class BundleGraph implements IBundleGraph { } traverseBundles( - visit: GraphTraversalCallback, + visit: GraphVisitor, startBundle?: IBundle, ): ?TContext { return this.#graph.traverseBundles( diff --git a/packages/core/core/src/types.js b/packages/core/core/src/types.js index 723c4401758..783da8d3b7c 100644 --- a/packages/core/core/src/types.js +++ b/packages/core/core/src/types.js @@ -119,6 +119,7 @@ export type Asset = {| astKey: ?string, astGenerator: ?ASTGenerator, symbols: Map, + exportedSymbols: Map, sideEffects: boolean, uniqueKey?: ?string, configPath?: FilePath, diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-library/package.json b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-library/package.json index d8e91bfde3a..bae6f246fba 100644 --- a/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-library/package.json +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/side-effects-re-exports-library/package.json @@ -1,4 +1,4 @@ { - "main": "build.js", + "main": "dist/index.js", "sideEffects": false } diff --git a/packages/core/integration-tests/test/output-formats.js b/packages/core/integration-tests/test/output-formats.js index d107a785722..3fe5b7ae853 100644 --- a/packages/core/integration-tests/test/output-formats.js +++ b/packages/core/integration-tests/test/output-formats.js @@ -618,7 +618,7 @@ describe('output formats', function() { .getBundles() .find(b => b.name.startsWith('async1') && !index.includes(b.name)); let shared = await outputFS.readFile(sharedBundle.filePath, 'utf8'); - assert(shared.includes('export function $')); + assert(shared.includes('export var $')); let async1 = await outputFS.readFile( b @@ -859,7 +859,8 @@ describe('output formats', function() { assert(!entry.includes('Promise.all')); // not needed - esmodules will wait for shared bundle let shared = await outputFS.readFile(sharedBundle.filePath, 'utf8'); - assert(shared.includes('export function $')); + + assert(shared.includes('export var $')); let async1 = await outputFS.readFile(async1Bundle.filePath, 'utf8'); assert( @@ -896,7 +897,7 @@ describe('output formats', function() { 'utf8', ); - let exportName = dist1.match(/export function\s*([a-z0-9$]+)\(\)/)[1]; + let exportName = dist1.match(/export var ([a-z0-9$]+) =/)[1]; assert(exportName); assert.equal( diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index cbe1ed2ea06..0fb1dd0fc21 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -448,10 +448,7 @@ describe('scope hoisting', function() { ); let contents = await outputFS.readFile( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-library/build.js', - ), + b.getBundles()[0].filePath, 'utf8', ); assert(!contents.includes('console.log')); diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 10d83d17617..2ad62ea1906 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -312,6 +312,7 @@ export interface BaseAsset { +isSource: boolean; +type: string; +symbols: Map; + +exportedSymbols: Map; +sideEffects: boolean; +uniqueKey: ?string; +astGenerator: ?ASTGenerator; @@ -651,9 +652,10 @@ export interface BundleGraph { isAssetReferencedByAnotherBundleOfType(asset: Asset, type: string): boolean; hasParentBundleOfType(bundle: Bundle, type: string): boolean; resolveSymbol(asset: Asset, symbol: Symbol): SymbolResolution; + getSymbols(asset: Asset): Array; getExportedSymbols(asset: Asset): Array; traverseBundles( - visit: GraphTraversalCallback, + visit: GraphVisitor, startBundle?: Bundle, ): ?TContext; findBundlesWithAsset(Asset): Array; @@ -685,6 +687,11 @@ export type Bundler = {| options: PluginOptions, logger: PluginLogger, |}): Async, + propagate({| + bundleGraph: BundleGraph, + options: PluginOptions, + logger: PluginLogger, + |}): Async, |}; export type Namer = {| @@ -699,7 +706,8 @@ export type Namer = {| export type RuntimeAsset = {| +filePath: FilePath, +code: string, - +dependency?: Dependency, + +dependencyFrom?: Dependency, + +dependencyReplace?: Dependency, +isEntry?: boolean, |}; diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index 832e27fe2f2..928a98d8464 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -77,7 +77,7 @@ export default new Runtime({ ); } - let assets = []; + let assets: Array = []; for (let dependency of bundleGraph.getExternalDependencies(bundle)) { let bundleGroup = bundleGraph.resolveExternalDependency(dependency); if (bundleGroup == null) { @@ -89,7 +89,7 @@ export default new Runtime({ code: `module.exports = ${JSON.stringify( dependency.moduleSpecifier, )}`, - dependency, + dependencyFrom: dependency, }); } continue; @@ -102,7 +102,7 @@ export default new Runtime({ assets.push({ filePath: path.join(__dirname, `/bundles/${firstBundle.id}.js`), code: `module.exports = ${JSON.stringify(dependency.id)};`, - dependency, + dependencyFrom: dependency, }); continue; @@ -190,7 +190,7 @@ export default new Runtime({ assets.push({ filePath: __filename, code: `module.exports = ${loaders};`, - dependency, + dependencyReplace: dependency, }); } else { assert(externalBundles.length === 1); @@ -209,7 +209,6 @@ export default new Runtime({ isEntry: true, }); } - return assets; }, }); @@ -237,14 +236,14 @@ function getURLRuntime( return { filePath: __filename, code: `module.exports = require('./get-worker-url')(${relativePathExpr});`, - dependency, + dependencyFrom: dependency, }; } return { filePath: __filename, code: `module.exports = require('./bundle-url').getBundleURL() + ${relativePathExpr}`, - dependency, + dependencyFrom: dependency, }; } diff --git a/packages/shared/scope-hoisting/src/concat.js b/packages/shared/scope-hoisting/src/concat.js index 0ad5b524b33..d2a3c932b16 100644 --- a/packages/shared/scope-hoisting/src/concat.js +++ b/packages/shared/scope-hoisting/src/concat.js @@ -1,6 +1,6 @@ // @flow -import type {Bundle, Asset, Symbol, BundleGraph} from '@parcel/types'; +import type {Bundle, Asset, BundleGraph} from '@parcel/types'; import type {CallExpression, Identifier, Statement} from '@babel/types'; import {parse as babelParse} from '@babel/parser'; @@ -39,24 +39,8 @@ type TraversalContext = {| // eslint-disable-next-line no-unused-vars export async function concat(bundle: Bundle, bundleGraph: BundleGraph) { let queue = new PromiseQueue({maxConcurrent: 32}); - bundle.traverse((node, shouldWrap) => { - switch (node.type) { - case 'dependency': - // Mark assets that should be wrapped, based on metadata in the incoming dependency tree - if (shouldWrap || node.value.meta.shouldWrap) { - let resolved = bundleGraph.getDependencyResolution( - node.value, - bundle, - ); - if (resolved) { - resolved.meta.shouldWrap = true; - } - return true; - } - break; - case 'asset': - queue.add(() => processAsset(bundle, node.value)); - } + bundle.traverseAssets(asset => { + queue.add(() => processAsset(bundle, asset)); }); let outputs = new Map>(await queue.run()); @@ -65,13 +49,11 @@ export async function concat(bundle: Bundle, bundleGraph: BundleGraph) { result.unshift(...parse(PRELUDE, PRELUDE_PATH)); } - let usedExports = getUsedExports(bundle, bundleGraph); - // Node: for each asset, the order of `$parcel$require` calls and the corresponding // `asset.getDependencies()` must be the same! bundle.traverseAssets({ enter(asset, context) { - if (shouldExcludeAsset(asset, usedExports)) { + if (shouldExcludeAsset(asset)) { return context; } @@ -81,7 +63,7 @@ export async function concat(bundle: Bundle, bundleGraph: BundleGraph) { }; }, exit(asset, context) { - if (!context || shouldExcludeAsset(asset, usedExports)) { + if (!context || shouldExcludeAsset(asset)) { return; } @@ -158,82 +140,11 @@ function parse(code, filename) { return ast.program.body; } -function getUsedExports( - bundle: Bundle, - bundleGraph: BundleGraph, -): Map> { - let usedExports: Map> = new Map(); - - let entry = bundle.getMainEntry(); - if (entry) { - for (let {asset, symbol} of bundleGraph.getExportedSymbols(entry)) { - if (symbol) { - markUsed(asset, symbol); - } - } - } - - bundle.traverseAssets(asset => { - for (let dep of bundleGraph.getDependencies(asset)) { - let resolvedAsset = bundleGraph.getDependencyResolution(dep, bundle); - if (!resolvedAsset) { - continue; - } - - for (let [symbol, identifier] of dep.symbols) { - if (identifier === '*') { - continue; - } - - if (symbol === '*') { - for (let {asset, symbol} of bundleGraph.getExportedSymbols( - resolvedAsset, - )) { - if (symbol) { - markUsed(asset, symbol); - } - } - } - - markUsed(resolvedAsset, symbol); - } - } - - // If the asset is referenced by another bundle, include all exports. - if (bundleGraph.isAssetReferencedByAnotherBundleOfType(asset, 'js')) { - markUsed(asset, '*'); - for (let {asset: a, symbol} of bundleGraph.getExportedSymbols(asset)) { - if (symbol) { - markUsed(a, symbol); - } - } - } - }); - - function markUsed(asset, symbol) { - let resolved = bundleGraph.resolveSymbol(asset, symbol); - - let used = usedExports.get(resolved.asset.id); - if (!used) { - used = new Set(); - usedExports.set(resolved.asset.id, used); - } - - used.add(resolved.exportSymbol); - } - - return usedExports; -} - -function shouldExcludeAsset( - asset: Asset, - usedExports: Map>, -) { +function shouldExcludeAsset(asset: Asset) { return ( asset.sideEffects === false && !asset.meta.isCommonJS && - (!usedExports.has(asset.id) || - nullthrows(usedExports.get(asset.id)).size === 0) + asset.exportedSymbols.size === 0 ); } diff --git a/packages/shared/scope-hoisting/src/formats/commonjs.js b/packages/shared/scope-hoisting/src/formats/commonjs.js index 219e02a8984..f1993b9e49a 100644 --- a/packages/shared/scope-hoisting/src/formats/commonjs.js +++ b/packages/shared/scope-hoisting/src/formats/commonjs.js @@ -131,7 +131,10 @@ export function generateBundleImports( scope: Scope, ) { let specifiers: Array = [...assets].map(asset => { - let id = getIdentifier(asset, 'init'); + let id = asset.meta.shouldWrap + ? getIdentifier(asset, 'init') + : t.identifier(assertString(asset.meta.exportsIdentifier)); + return t.objectProperty(id, id, false, true); }); @@ -299,14 +302,25 @@ export function generateExports( let statements = []; for (let asset of referencedAssets) { - let id = getIdentifier(asset, 'init'); - exported.add(id.name); - statements.push( - EXPORT_TEMPLATE({ - NAME: id, - IDENTIFIER: id, - }), - ); + if (asset.meta.shouldWrap) { + let id = getIdentifier(asset, 'init'); + exported.add(id.name); + statements.push( + EXPORT_TEMPLATE({ + NAME: id, + IDENTIFIER: id, + }), + ); + } else { + let exportsId = assertString(asset.meta.exportsIdentifier); + exported.add(exportsId); + statements.push( + EXPORT_TEMPLATE({ + NAME: t.identifier(exportsId), + IDENTIFIER: t.identifier(exportsId), + }), + ); + } } let entry = bundle.getMainEntry(); diff --git a/packages/shared/scope-hoisting/src/formats/esmodule.js b/packages/shared/scope-hoisting/src/formats/esmodule.js index 9d89abf77eb..f78e3d5c8a3 100644 --- a/packages/shared/scope-hoisting/src/formats/esmodule.js +++ b/packages/shared/scope-hoisting/src/formats/esmodule.js @@ -18,7 +18,7 @@ import {relative} from 'path'; import {relativeBundlePath} from '@parcel/utils'; import ThrowableDiagnostic from '@parcel/diagnostic'; import rename from '../renamer'; -import {getName, getIdentifier} from '../utils'; +import {assertString, getName, getIdentifier} from '../utils'; export function generateBundleImports( from: Bundle, @@ -26,7 +26,10 @@ export function generateBundleImports( assets: Set, ) { let specifiers = [...assets].map(asset => { - let id = getIdentifier(asset, 'init'); + let id = asset.meta.shouldWrap + ? getIdentifier(asset, 'init') + : t.identifier(assertString(asset.meta.exportsIdentifier)); + return t.importSpecifier(id, id); }); @@ -125,7 +128,10 @@ export function generateExports( } for (let asset of referencedAssets) { - let exportsId = getName(asset, 'init'); + let exportsId = asset.meta.shouldWrap + ? getName(asset, 'init') + : assertString(asset.meta.exportsIdentifier); + exportedIdentifiers.set(exportsId, exportsId); } diff --git a/packages/shared/scope-hoisting/src/formats/global.js b/packages/shared/scope-hoisting/src/formats/global.js index d79bded3fa8..b9257f0cae7 100644 --- a/packages/shared/scope-hoisting/src/formats/global.js +++ b/packages/shared/scope-hoisting/src/formats/global.js @@ -52,7 +52,9 @@ export function generateBundleImports( for (let asset of assets) { statements.push( IMPORT_TEMPLATE({ - IDENTIFIER: getIdentifier(asset, 'init'), + IDENTIFIER: asset.meta.shouldWrap + ? getIdentifier(asset, 'init') + : t.identifier(assertString(asset.meta.exportsIdentifier)), ASSET_ID: t.stringLiteral(asset.id), }), ); @@ -77,7 +79,9 @@ export function generateExports( let statements = []; for (let asset of referencedAssets) { - let exportsId = getName(asset, 'init'); + let exportsId = asset.meta.shouldWrap + ? getName(asset, 'init') + : assertString(asset.meta.exportsIdentifier); exported.add(exportsId); statements.push( diff --git a/packages/shared/scope-hoisting/src/hoist.js b/packages/shared/scope-hoisting/src/hoist.js index d3b2e8dbc2a..0ff9dfd8646 100644 --- a/packages/shared/scope-hoisting/src/hoist.js +++ b/packages/shared/scope-hoisting/src/hoist.js @@ -16,8 +16,12 @@ import type { import * as t from '@babel/types'; import { + isCallExpression, isClassDeclaration, isExportDefaultSpecifier, + isFunction, + isObjectPattern, + isObjectProperty, isExportNamespaceSpecifier, isExportSpecifier, isExpression, @@ -35,7 +39,12 @@ import template from '@babel/template'; import nullthrows from 'nullthrows'; import invariant from 'assert'; import rename from './renamer'; -import {getName, getIdentifier, getExportIdentifier} from './utils'; +import { + getName, + getIdentifier, + getExportIdentifier, + isUnusedValue, +} from './utils'; const WRAPPER_TEMPLATE = template.statement< {|NAME: LVal, BODY: Array|}, @@ -447,8 +456,34 @@ const VISITOR: Visitor = { dep.meta.shouldWrap = true; } - dep.meta.isCommonJS = true; - dep.symbols.set('*', getName(asset, 'require', source)); + if (!isUnusedValue(path)) { + let {parent} = path; + if ( + isMemberExpression(parent, {object: path.node}) && + isIdentifier(parent.property, {name: 'then'}) && + isCallExpression(path.parentPath.parent, { + callee: parent, + }) && + path.parentPath.parent.arguments.length === 1 && + isFunction(path.parentPath.parent.arguments[0]) && + // $FlowFixMe + path.parentPath.parent.arguments[0].params.length === 1 && + isObjectPattern(path.parentPath.parent.arguments[0].params[0]) && + path.parentPath.parent.arguments[0].params[0].properties.every( + p => isObjectProperty(p) && isIdentifier(p.key), + ) + ) { + for (let imported of path.parentPath.parent.arguments[0].params[0].properties.map( + // $FlowFixMe + ({key}) => key.name, + )) { + dep.symbols.set(imported, getName(asset, 'import', imported)); + } + } else { + dep.meta.isCommonJS = true; + dep.symbols.set('*', getName(asset, 'require', source)); + } + } // Generate a variable name based on the current asset id and the module name to require. // This will be replaced by the final variable name of the resolved asset in the packager. diff --git a/packages/shared/scope-hoisting/src/link.js b/packages/shared/scope-hoisting/src/link.js index e1808f5ef35..eabbab2def1 100644 --- a/packages/shared/scope-hoisting/src/link.js +++ b/packages/shared/scope-hoisting/src/link.js @@ -25,16 +25,10 @@ import invariant from 'assert'; import {relative} from 'path'; import template from '@babel/template'; import * as t from '@babel/types'; -import { - isExpressionStatement, - isIdentifier, - isObjectPattern, - isSequenceExpression, - isStringLiteral, -} from '@babel/types'; +import {isIdentifier, isObjectPattern, isStringLiteral} from '@babel/types'; import traverse from '@babel/traverse'; import treeShake from './shake'; -import {assertString, getName, getIdentifier} from './utils'; +import {assertString, getName, getIdentifier, isUnusedValue} from './utils'; import OutputFormats from './formats/index.js'; const ESMODULE_TEMPLATE = template.statement< @@ -51,12 +45,6 @@ const DEFAULT_INTEROP_TEMPLATE = template.statement< const THROW_TEMPLATE = template.statement<{|MODULE: StringLiteral|}, Statement>( '$parcel$missingModule(MODULE);', ); -const FAKE_INIT_TEMPLATE = template.statement< - {|INIT: Identifier, EXPORTS: Identifier|}, - Statement, ->(`function INIT(){ - return EXPORTS; -}`); export function link({ bundle, @@ -235,17 +223,6 @@ export function link({ return path.isScope() ? path.parentPath.scope : path.scope; } - function isUnusedValue(path) { - let {parent} = path; - return ( - isExpressionStatement(parent) || - (isSequenceExpression(parent) && - ((Array.isArray(path.container) && - path.key !== path.container.length - 1) || - isUnusedValue(path.parentPath))) - ); - } - function addExternalModule(path, dep) { // Find an existing import for this specifier, or create a new one. let importedFile = importedFiles.get(dep.moduleSpecifier); @@ -320,7 +297,11 @@ export function link({ invariant(imported.assets != null); imported.assets.add(mod); - return t.callExpression(getIdentifier(mod, 'init'), []); + if (mod.meta.shouldWrap) { + return t.callExpression(getIdentifier(mod, 'init'), []); + } else { + return t.identifier(assertString(mod.meta.exportsIdentifier)); + } } } @@ -413,9 +394,8 @@ export function link({ // function, if statement, or conditional expression. if (mod.meta.shouldWrap) { node = t.callExpression(getIdentifier(mod, 'init'), []); - } - // Replace with nothing if the require call's result is not used. - else if (isValueUsed) { + } else if (isValueUsed) { + // Replace with nothing if the require call's result is not used. node = t.identifier(replacements.get(name) || name); } } else if (mod.type === 'js') { @@ -607,21 +587,6 @@ export function link({ path.scope.crawl(); } - // Insert fake init functions that will be imported in other bundles, - // because `asset.meta.shouldWrap` isn't set in a packager if `asset` is - // not in the current bundle: - path.pushContainer( - 'body', - [...referencedAssets] - .filter(a => !a.meta.shouldWrap) - .map(a => { - return FAKE_INIT_TEMPLATE({ - INIT: getIdentifier(a, 'init'), - EXPORTS: t.identifier(assertString(a.meta.exportsIdentifier)), - }); - }), - ); - // Generate exports let exported = format.generateExports( bundleGraph, diff --git a/packages/shared/scope-hoisting/src/utils.js b/packages/shared/scope-hoisting/src/utils.js index 9df90bb17c9..76863900816 100644 --- a/packages/shared/scope-hoisting/src/utils.js +++ b/packages/shared/scope-hoisting/src/utils.js @@ -1,7 +1,10 @@ // @flow import type {Asset, MutableAsset, Bundle, BundleGraph} from '@parcel/types'; +import type {NodePath} from '@babel/traverse'; +import type {Node} from '@babel/types'; import * as t from '@babel/types'; +import {isExpressionStatement, isSequenceExpression} from '@babel/types'; import invariant from 'assert'; export function getName( @@ -67,3 +70,14 @@ export function assertString(v: mixed): string { invariant(typeof v === 'string'); return v; } + +export function isUnusedValue(path: NodePath) { + let {parent} = path; + return ( + isExpressionStatement(parent) || + (isSequenceExpression(parent) && + ((Array.isArray(path.container) && + path.key !== path.container.length - 1) || + isUnusedValue(path.parentPath))) + ); +}