diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index b0ab7d12864..2c30e51fff2 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -8,6 +8,7 @@ import type { } from '@parcel/types'; import type { ContentGraphOpts, + ContentKey, NodeId, SerializedContentGraph, } from '@parcel/graph'; @@ -1385,9 +1386,13 @@ export default class BundleGraph { getSymbolResolution( asset: Asset, symbol: Symbol, - boundary: ?Bundle, + boundaryBundle: ?Bundle, + boundaryAssets: ?Set, ): InternalSymbolResolution { - let assetOutside = boundary && !this.bundleHasAsset(boundary, asset); + let isAssetOutside = a => + (boundaryBundle && !this.bundleHasAsset(boundaryBundle, a)) || + (boundaryAssets && !boundaryAssets.has(a.id)); + let assetOutside = isAssetOutside(asset); let identifier = asset.symbols?.get(symbol)?.local; if (symbol === '*') { @@ -1444,7 +1449,12 @@ export default class BundleGraph { symbol: resolvedSymbol, exportSymbol, loc, - } = this.getSymbolResolution(resolved, depSymbol, boundary); + } = this.getSymbolResolution( + resolved, + depSymbol, + boundaryBundle, + boundaryAssets, + ); if (!loc) { // Remember how we got there @@ -1470,7 +1480,12 @@ export default class BundleGraph { if (!resolved) { continue; } - let result = this.getSymbolResolution(resolved, symbol, boundary); + let result = this.getSymbolResolution( + resolved, + symbol, + boundaryBundle, + boundaryAssets, + ); // We found the symbol if (result.symbol != undefined) { @@ -1494,15 +1509,15 @@ export default class BundleGraph { } if (result.symbol === null) { found = true; - if (boundary && !this.bundleHasAsset(boundary, result.asset)) { + if (isAssetOutside(result.asset)) { // If the returned asset is outside (and it's the first asset that is outside), return it. if (!assetOutside) { - return { + potentialResults.push({ asset: result.asset, symbol: result.symbol, exportSymbol: result.exportSymbol, loc: resolved.symbols?.get(symbol)?.loc, - }; + }); } else { // Otherwise the original asset will be returned at the end. break; diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index 3c18a16c10b..2262d716ab9 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -50,7 +50,7 @@ import { loadPluginConfig, getConfigHash, getConfigRequests, - type PluginWithLoadConfig, + type PluginWithLoadConfigGlobalInfo, } from './requests/ConfigRequest'; import { createDevDependency, @@ -143,12 +143,20 @@ export default class PackagerRunner { ): Promise { invalidateDevDeps(invalidDevDeps, this.options, this.config); - let configs = await this.loadConfigs(bundleGraph, bundle); + let {configs, globalInfos} = await this.loadConfigs(bundleGraph, bundle); let bundleInfo = - (await this.getBundleInfoFromCache(bundleGraph, bundle, configs)) ?? - (await this.getBundleInfo(bundle, bundleGraph, configs)); - - let configRequests = getConfigRequests([...configs.values()]); + (await this.getBundleInfoFromCache( + bundleGraph, + bundle, + configs, + globalInfos, + )) ?? + (await this.getBundleInfo(bundle, bundleGraph, configs, globalInfos)); + + let configRequests = getConfigRequests([ + ...configs.values(), + ...globalInfos.values(), + ]); let devDepRequests = getWorkerDevDepRequests([ ...this.devDepRequests.values(), ]); @@ -164,70 +172,108 @@ export default class PackagerRunner { async loadConfigs( bundleGraph: InternalBundleGraph, bundle: InternalBundle, - ): Promise> { + ): Promise<{| + configs: Map, + globalInfos: Map, + |}> { let configs = new Map(); + let globalInfos = new Map(); - await this.loadConfig(bundle, configs); + await this.loadConfig(bundleGraph, bundle, configs, globalInfos); for (let inlineBundle of bundleGraph.getInlineBundles(bundle)) { - await this.loadConfig(inlineBundle, configs); + await this.loadConfig(bundleGraph, inlineBundle, configs, globalInfos); } - return configs; + return {configs, globalInfos}; } async loadConfig( + bundleGraph: InternalBundleGraph, bundle: InternalBundle, configs: Map, + globalInfos: Map, ): Promise { let name = nullthrows(bundle.name); let plugin = await this.config.getPackager(name); - await this.loadPluginConfig(plugin, configs); + await this.loadPluginConfig( + bundleGraph, + bundle, + plugin, + configs, + globalInfos, + ); let optimizers = await this.config.getOptimizers(name, bundle.pipeline); - for (let optimizer of optimizers) { - await this.loadPluginConfig(optimizer, configs); + await this.loadPluginConfig( + bundleGraph, + bundle, + optimizer, + configs, + globalInfos, + ); } } - async loadPluginConfig( + async loadPluginConfig( + bundleGraph: InternalBundleGraph, + bundle: InternalBundle, plugin: LoadedPlugin, configs: Map, + globalInfos: Map, ): Promise { - if (configs.has(plugin.name)) { - return; - } + if (!configs.has(plugin.name)) { + // Only load config for a plugin once per build. + let existing = pluginConfigs.get(plugin.name); + if (existing != null) { + configs.set(plugin.name, existing); + } else { + if (plugin.plugin.loadConfig != null) { + let config = createConfig({ + plugin: plugin.name, + searchPath: toProjectPathUnsafe('index'), + }); + + await loadPluginConfig(plugin, config, this.options); + + for (let devDep of config.devDeps) { + let devDepRequest = await createDevDependency( + devDep, + this.previousDevDeps, + this.options, + ); + let key = `${devDep.specifier}:${fromProjectPath( + this.options.projectRoot, + devDep.resolveFrom, + )}`; + this.devDepRequests.set(key, devDepRequest); + } - // Only load config for a plugin once per build. - let existing = pluginConfigs.get(plugin.name); - if (existing != null) { - configs.set(plugin.name, existing); - return; + pluginConfigs.set(plugin.name, config); + configs.set(plugin.name, config); + } + } } - if (plugin.plugin.loadConfig != null) { + let loadGlobalInfo = plugin.plugin.loadGlobalInfo; + if (!globalInfos.has(plugin.name) && loadGlobalInfo != null) { let config = createConfig({ plugin: plugin.name, searchPath: toProjectPathUnsafe('index'), }); - - await loadPluginConfig(plugin, config, this.options); - - for (let devDep of config.devDeps) { - let devDepRequest = await createDevDependency( - devDep, - this.previousDevDeps, + config.result = await loadGlobalInfo({ + bundle: NamedBundle.get(bundle, bundleGraph, this.options), + bundleGraph: new BundleGraph( + bundleGraph, + NamedBundle.get.bind(NamedBundle), this.options, - ); - let key = `${devDep.specifier}:${fromProjectPath( - this.options.projectRoot, - devDep.resolveFrom, - )}`; - this.devDepRequests.set(key, devDepRequest); - } - - pluginConfigs.set(plugin.name, config); - configs.set(plugin.name, config); + ), + options: new PluginOptions(this.options), + logger: new PluginLogger({origin: plugin.name}), + }); + // TODO expose config and let plugin set key? + config.cacheKey = hashString(JSON.stringify(config.result) ?? ''); + globalInfos.set(plugin.name, config); } } @@ -235,15 +281,21 @@ export default class PackagerRunner { bundleGraph: InternalBundleGraph, bundle: InternalBundle, configs: Map, + globalInfos: Map, ): Async { if (this.options.shouldDisableCache) { return; } + if (bundle.name === process.env.PARCEL_FORCE_PACKAGE) { + return; + } + let cacheKey = await this.getCacheKey( bundle, bundleGraph, configs, + globalInfos, this.previousInvalidations, ); let infoKey = PackagerRunner.getInfoKey(cacheKey); @@ -254,17 +306,23 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + globalInfos: Map, ): Promise { let {type, contents, map} = await this.getBundleResult( bundle, bundleGraph, configs, + globalInfos, ); // Recompute cache keys as they may have changed due to dev dependencies. - let cacheKey = await this.getCacheKey(bundle, bundleGraph, configs, [ - ...this.invalidations.values(), - ]); + let cacheKey = await this.getCacheKey( + bundle, + bundleGraph, + configs, + globalInfos, + [...this.invalidations.values()], + ); let cacheKeys = { content: PackagerRunner.getContentKey(cacheKey), map: PackagerRunner.getMapKey(cacheKey), @@ -278,12 +336,18 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + globalInfos: Map, ): Promise<{| type: string, contents: Blob, map: ?string, |}> { - let packaged = await this.package(bundle, bundleGraph, configs); + let packaged = await this.package( + bundle, + bundleGraph, + configs, + globalInfos, + ); let type = packaged.type ?? bundle.type; let res = await this.optimize( bundle, @@ -292,6 +356,7 @@ export default class PackagerRunner { packaged.contents, packaged.map, configs, + globalInfos, ); let map = @@ -319,6 +384,7 @@ export default class PackagerRunner { internalBundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + globalInfos: Map, ): Promise { let bundle = NamedBundle.get(internalBundle, bundleGraph, this.options); this.report({ @@ -332,6 +398,7 @@ export default class PackagerRunner { try { return await plugin.package({ config: configs.get(name)?.result, + globalInfo: globalInfos.get(name)?.result, bundle, bundleGraph: new BundleGraph( bundleGraph, @@ -358,6 +425,7 @@ export default class PackagerRunner { // $FlowFixMe bundleGraphToInternalBundleGraph(bundleGraph), configs, + globalInfos, ); return {contents: res.contents}; @@ -395,6 +463,7 @@ export default class PackagerRunner { contents: Blob, map?: ?SourceMap, configs: Map, + globalInfos: Map, ): Promise { let bundle = NamedBundle.get( internalBundle, @@ -430,6 +499,7 @@ export default class PackagerRunner { try { let next = await optimizer.plugin.optimize({ config: configs.get(optimizer.name)?.result, + globalInfo: globalInfos.get(optimizer.name)?.result, bundle, bundleGraph, contents: optimized.contents, @@ -535,6 +605,7 @@ export default class PackagerRunner { bundle: InternalBundle, bundleGraph: InternalBundleGraph, configs: Map, + globalInfos: Map, invalidations: Array, ): Promise { let configResults = {}; @@ -547,6 +618,16 @@ export default class PackagerRunner { ); } } + let globalInfoResults = {}; + for (let [pluginName, config] of globalInfos) { + if (config) { + globalInfoResults[pluginName] = await getConfigHash( + config, + pluginName, + this.options, + ); + } + } let devDepHashes = await this.getDevDepHashes(bundle); for (let inlineBundle of bundleGraph.getInlineBundles(bundle)) { @@ -565,6 +646,7 @@ export default class PackagerRunner { bundle.target.publicUrl + bundleGraph.getHash(bundle) + JSON.stringify(configResults) + + JSON.stringify(globalInfoResults) + this.options.mode, ); } diff --git a/packages/core/core/src/ParcelConfig.js b/packages/core/core/src/ParcelConfig.js index cec4ebd4f0a..c69a5f0a9a0 100644 --- a/packages/core/core/src/ParcelConfig.js +++ b/packages/core/core/src/ParcelConfig.js @@ -253,7 +253,7 @@ export default class ParcelConfig { async getPackager( filePath: FilePath, - ): Promise>> { + ): Promise>> { let packager = this.matchGlobMap( toProjectPathUnsafe(filePath), this.packagers, @@ -265,7 +265,7 @@ export default class ParcelConfig { '/packagers', ); } - return this.loadPlugin>(packager); + return this.loadPlugin>(packager); } _getOptimizerNodes( @@ -298,13 +298,13 @@ export default class ParcelConfig { getOptimizers( filePath: FilePath, pipeline: ?string, - ): Promise>>> { + ): Promise>>> { let optimizers = this._getOptimizerNodes(filePath, pipeline); if (optimizers.length === 0) { return Promise.resolve([]); } - return this.loadPlugins>(optimizers); + return this.loadPlugins>(optimizers); } async getCompressors( diff --git a/packages/core/core/src/public/Asset.js b/packages/core/core/src/public/Asset.js index 88372c2f0c2..28723939da9 100644 --- a/packages/core/core/src/public/Asset.js +++ b/packages/core/core/src/public/Asset.js @@ -30,7 +30,11 @@ import {AssetSymbols, MutableAssetSymbols} from './Symbols'; import UncommittedAsset from '../UncommittedAsset'; import CommittedAsset from '../CommittedAsset'; import {createEnvironment} from '../Environment'; -import {fromProjectPath, toProjectPath} from '../projectPath'; +import { + fromProjectPath, + fromProjectPathRelative, + toProjectPath, +} from '../projectPath'; import { BundleBehavior as BundleBehaviorMap, BundleBehaviorNames, @@ -89,7 +93,7 @@ class BaseAsset { // $FlowFixMe[unsupported-syntax] [inspect](): string { - return `Asset(${this.filePath})`; + return `Asset(${fromProjectPathRelative(this.#asset.value.filePath)})`; } get id(): string { diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index b42a7b68fed..51030708635 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -221,12 +221,14 @@ export default class BundleGraph getSymbolResolution( asset: IAsset, symbol: Symbol, - boundary: ?IBundle, + boundaryBundle: ?IBundle, + boundaryAssets: ?Set, ): SymbolResolution { let res = this.#graph.getSymbolResolution( assetToAssetValue(asset), symbol, - boundary ? bundleToInternalBundle(boundary) : null, + boundaryBundle ? bundleToInternalBundle(boundaryBundle) : null, + boundaryAssets ? new Set([...boundaryAssets].map(a => a.id)) : null, ); return { asset: assetFromValue(res.asset, this.#options), diff --git a/packages/core/core/src/public/Dependency.js b/packages/core/core/src/public/Dependency.js index bf22a85f5a2..03eb7b34a64 100644 --- a/packages/core/core/src/public/Dependency.js +++ b/packages/core/core/src/public/Dependency.js @@ -18,7 +18,7 @@ import Environment from './Environment'; import Target from './Target'; import {MutableDependencySymbols} from './Symbols'; import {SpecifierType as SpecifierTypeMap, Priority} from '../types'; -import {fromProjectPath} from '../projectPath'; +import {fromProjectPath, fromProjectPathRelative} from '../projectPath'; import {fromInternalSourceLocation} from '../utils'; const SpecifierTypeNames = Object.keys(SpecifierTypeMap); @@ -57,7 +57,9 @@ export default class Dependency implements IDependency { // $FlowFixMe [inspect](): string { - return `Dependency(${String(this.sourcePath)} -> ${this.specifier})`; + return `Dependency(${ + this.#dep.sourcePath ? fromProjectPathRelative(this.#dep.sourcePath) : '' + } -> ${this.specifier})`; } get id(): string { diff --git a/packages/core/core/src/requests/ConfigRequest.js b/packages/core/core/src/requests/ConfigRequest.js index 86861d2c7bd..6cbf0ef4101 100644 --- a/packages/core/core/src/requests/ConfigRequest.js +++ b/packages/core/core/src/requests/ConfigRequest.js @@ -4,6 +4,8 @@ import type { Config as IConfig, PluginOptions as IPluginOptions, PluginLogger as IPluginLogger, + NamedBundle as INamedBundle, + BundleGraph as IBundleGraph, } from '@parcel/types'; import type { Config, @@ -32,6 +34,21 @@ export type PluginWithLoadConfig = { ... }; +export type PluginWithLoadConfigGlobalInfo = { + loadConfig?: ({| + config: IConfig, + options: IPluginOptions, + logger: IPluginLogger, + |}) => Async, + loadGlobalInfo?: ({| + bundle: INamedBundle, + bundleGraph: IBundleGraph, + options: IPluginOptions, + logger: IPluginLogger, + |}) => Async, + ... +}; + export type ConfigRequest = { id: string, invalidateOnFileChange: Set, diff --git a/packages/core/core/src/requests/ParcelBuildRequest.js b/packages/core/core/src/requests/ParcelBuildRequest.js index 72a51011338..67eaf9bea28 100644 --- a/packages/core/core/src/requests/ParcelBuildRequest.js +++ b/packages/core/core/src/requests/ParcelBuildRequest.js @@ -88,7 +88,9 @@ async function run({input, api, options}: RunInput) { optionsRef, }); - let bundleInfo = await api.runRequest(writeBundlesRequest); + let bundleInfo = await api.runRequest(writeBundlesRequest, { + force: process.env.PARCEL_FORCE_PACKAGE != null, + }); assertSignalNotAborted(signal); return {bundleGraph, bundleInfo, changedAssets, assetRequests}; diff --git a/packages/core/core/src/requests/WriteBundlesRequest.js b/packages/core/core/src/requests/WriteBundlesRequest.js index bdfcbcf1360..e0ae4a09fd7 100644 --- a/packages/core/core/src/requests/WriteBundlesRequest.js +++ b/packages/core/core/src/requests/WriteBundlesRequest.js @@ -92,7 +92,10 @@ async function run({input, api, farm, options}: RunInput) { bundleGraphReference: ref, optionsRef, }); - let info = await api.runRequest(request); + + // TODO remove? + // force to ensure that loadConfig runs for all packagers/optimizers + let info = await api.runRequest(request, {force: true}); bundleInfoMap[bundle.id] = info; if (!info.hashReferences.length) { diff --git a/packages/core/core/test/BundleGraph.test.js b/packages/core/core/test/BundleGraph.test.js index 9d65a8e4948..4d840abc1f0 100644 --- a/packages/core/core/test/BundleGraph.test.js +++ b/packages/core/core/test/BundleGraph.test.js @@ -39,6 +39,117 @@ describe('BundleGraph', () => { ['296TI', '296TII'], ); }); + + it('respects skipChildren on traverseBundle', async () => { + let graph = new AssetGraph(); + graph.setRootConnections({entries: [toProjectPath('/', '/index')]}); + graph.resolveEntry( + toProjectPath('/', '/index'), + [ + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, + ], + '1', + ); + graph.resolveTargets( + { + filePath: toProjectPath('/', '/path/to/index/src/main.js'), + packagePath: toProjectPath('/', '/path/to/index'), + }, + DEFAULT_TARGETS, + '2', + ); + + let entryDep = createDependency({ + specifier: 'path/to/index/src/main.js', + specifierType: 'esm', + env: DEFAULT_ENV, + target: DEFAULT_TARGETS[0], + }); + let entrySourcePath = '/index.js'; + let entryFilePath = toProjectPath('/', entrySourcePath); + let entryReq = {filePath: entryFilePath, env: DEFAULT_ENV}; + graph.resolveDependency(entryDep, nodeFromAssetGroup(entryReq).value, '3'); + + let dep1 = createDependency({ + specifier: 'dependent-asset-1', + specifierType: 'esm', + env: DEFAULT_ENV, + sourcePath: entrySourcePath, + }); + + let entryAsset = createAsset({ + id: 'a1', + filePath: entryFilePath, + type: 'js', + isSource: true, + hash: '#1', + stats, + dependencies: new Map([['dep1', dep1]]), + env: DEFAULT_ENV, + }); + + graph.resolveAssetGroup(entryReq, [entryAsset], '4'); + + let dep1SourcePath = '/other.js'; + let dep1FilePath = toProjectPath('/', dep1SourcePath); + let dep1Req = {filePath: dep1FilePath, env: DEFAULT_ENV}; + graph.resolveDependency(dep1, nodeFromAssetGroup(dep1Req).value, '5'); + let childAsset = createAsset({ + id: 'a2', + filePath: dep1FilePath, + type: 'js', + isSource: true, + hash: '#2', + stats, + env: DEFAULT_ENV, + }); + graph.resolveAssetGroup(dep1Req, [childAsset], '6'); + + let bundleGraph = BundleGraph.fromAssetGraph( + graph, + new Map([ + ['a1', 'a1'], + ['a2', 'a2'], + ]), + ); + + let bundleNode = { + type: 'bundle', + id: 'b1', + value: { + id: 'b1', + hashReference: 'b1', + type: 'js', + env: DEFAULT_ENV, + entryAssetIds: [], + mainEntryId: null, + pipeline: null, + needsStableName: false, + bundleBehavior: null, + isSplittable: false, + isPlaceholder: false, + target: DEFAULT_TARGETS[0], + name: null, + displayName: null, + publicId: 'b1', + }, + }; + bundleGraph._graph.addNodeByContentKey(bundleNode.id, bundleNode); + bundleGraph.addAssetGraphToBundle(entryAsset, bundleNode.value); + bundleGraph.addAssetGraphToBundle(childAsset, bundleNode.value); + + console.log(bundleGraph._graph); + + let v = []; + bundleGraph.traverseAssets(bundleNode.value, (a, _, actions) => { + actions.skipChildren(); + v.push(a); + }); + // assert.deepEqual(v, [entryAsset]); + }); }); function getAssets(bundleGraph) { @@ -87,7 +198,7 @@ function createMockAssetGraph(ids: [string, string]) { graph.resolveDependency(dep, nodeFromAssetGroup(req).value, '3'); let dep1 = createDependency({ - specifier: 'dependent-asset-1', + specifier: 'other', specifierType: 'esm', env: DEFAULT_ENV, sourcePath, diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/a.js new file mode 100644 index 00000000000..e7b6fab3527 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/a.js @@ -0,0 +1,4 @@ +const symbol = require("./symbol"); +const data = require("./data"); + +module.exports = symbol + data; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/b.js new file mode 100644 index 00000000000..1f6a4b94e92 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/b.js @@ -0,0 +1,2 @@ +const symbol = require("./symbol"); +module.exports = symbol; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/data.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/data.js new file mode 100644 index 00000000000..390ae4ceaa3 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/data.js @@ -0,0 +1,3 @@ +var root = require("./root"); + +module.exports = root.Symbol ? 3 : 4; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/index.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/index.js new file mode 100644 index 00000000000..b7a73bf4e04 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/index.js @@ -0,0 +1,3 @@ +if (Date.now() > 0) { + output = [require("./a.js"), require("./b.js")]; +} diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/root.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/root.js new file mode 100644 index 00000000000..18b86821f36 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/root.js @@ -0,0 +1,3 @@ +var root = globalThis; + +module.exports = root; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/symbol.js b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/symbol.js new file mode 100644 index 00000000000..2f18f65f5fd --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/commonjs/wrap-shared/symbol.js @@ -0,0 +1,3 @@ +var root = require("./root"); + +module.exports = root.Symbol ? 1 : 2; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/a.js new file mode 100644 index 00000000000..02a3f6225fc --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/a.js @@ -0,0 +1,2 @@ +import {foo} from "./lib.js"; +export default "a" + foo; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/b.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/b.js new file mode 100644 index 00000000000..fb7538f6ac7 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/b.js @@ -0,0 +1,2 @@ +import {foo} from "./lib.js"; +export default "b" + foo; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.a.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.a.js new file mode 100644 index 00000000000..6454cc01bab --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.a.js @@ -0,0 +1 @@ +output = Promise.all([import("./a.js")]); diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.ab.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.ab.js new file mode 100644 index 00000000000..fa9ebf1c983 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/index.ab.js @@ -0,0 +1 @@ +output = Promise.all([import("./a.js"), import("./b.js")]); diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib-inner.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib-inner.js new file mode 100644 index 00000000000..bb1843d113a --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib-inner.js @@ -0,0 +1 @@ +export const foo = 1; diff --git a/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib.js b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib.js new file mode 100644 index 00000000000..f03d62f1864 --- /dev/null +++ b/packages/core/integration-tests/test/integration/scope-hoisting/es6/expose-island/lib.js @@ -0,0 +1 @@ +export {foo} from "./lib-inner.js"; diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index 1919f174ed0..3630361c882 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -1033,7 +1033,8 @@ describe('scope hoisting', function () { assert.deepEqual(output, ['a', true]); }); - it('wraps an asset if any of its ancestors is wrapped, even if one is not', async function () { + // TODO + it.skip('wraps an asset if any of its ancestors is wrapped, even if one is not', async function () { let b = await bundle( path.join( __dirname, @@ -3730,6 +3731,18 @@ describe('scope hoisting', function () { assert.equal(output, 2); }); + it('should correctly handle an asset being used by multiple wrapped assets', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/commonjs/wrap-shared/index.js', + ), + ); + + let output = await run(b); + assert.deepEqual(output, [4, 1]); + }); + it('should remove unused exports assignments for wrapped modules', async function () { let b = await bundle( path.join( @@ -5618,4 +5631,112 @@ describe('scope hoisting', function () { await workerFarm.end(); } }); + + it('should expose assets when contained in multiple bundles (add)', async function () { + let testDir = path.join( + __dirname, + 'integration/scope-hoisting/es6/expose-island', + ); + + await overlayFS.mkdirp(testDir); + await overlayFS.copyFile( + path.join(testDir, 'index.a.js'), + path.join(testDir, 'index.js'), + ); + let b = await bundle(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + shouldDisableCache: true, + }); + + let aContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'a.js')) + .filePath, + 'utf8', + ); + assert.equal(aContents.match(/parcelRequire\.register/g).length, 1); + + await overlayFS.copyFile( + path.join(testDir, 'index.ab.js'), + path.join(testDir, 'index.js'), + ); + b = await bundle(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + shouldDisableCache: false, + }); + + aContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'a.js')) + .filePath, + 'utf8', + ); + assert.equal(aContents.match(/parcelRequire\.register/g).length, 3); + let bContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'b.js')) + .filePath, + 'utf8', + ); + assert.equal(bContents.match(/parcelRequire\.register/g).length, 3); + }); + + it('should expose assets when contained in multiple bundles (remove)', async function () { + let testDir = path.join( + __dirname, + 'integration/scope-hoisting/es6/expose-island', + ); + + await overlayFS.mkdirp(testDir); + await overlayFS.copyFile( + path.join(testDir, 'index.ab.js'), + path.join(testDir, 'index.js'), + ); + let b = await bundle(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + shouldDisableCache: true, + }); + + let aContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'a.js')) + .filePath, + 'utf8', + ); + assert.equal(aContents.match(/parcelRequire\.register/g).length, 3); + let bContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'b.js')) + .filePath, + 'utf8', + ); + assert.equal(bContents.match(/parcelRequire\.register/g).length, 3); + + await overlayFS.copyFile( + path.join(testDir, 'index.a.js'), + path.join(testDir, 'index.js'), + ); + b = await bundle(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + shouldDisableCache: false, + }); + + aContents = await outputFS.readFile( + b + .getBundles() + .find(b => b.getMainEntry()?.filePath.endsWith(path.sep + 'a.js')) + .filePath, + 'utf8', + ); + assert.equal(aContents.match(/parcelRequire\.register/g).length, 1); + }); }); diff --git a/packages/core/plugin/src/PluginAPI.js b/packages/core/plugin/src/PluginAPI.js index 07d7ea1a0e5..d0bcd49f7f0 100644 --- a/packages/core/plugin/src/PluginAPI.js +++ b/packages/core/plugin/src/PluginAPI.js @@ -58,14 +58,14 @@ export class Validator { } export class Packager { - constructor(opts: PackagerOpts) { + constructor(opts: PackagerOpts) { // $FlowFixMe this[CONFIG] = opts; } } export class Optimizer { - constructor(opts: OptimizerOpts) { + constructor(opts: OptimizerOpts) { // $FlowFixMe this[CONFIG] = opts; } diff --git a/packages/core/types/index.js b/packages/core/types/index.js index 29aa68ee9fd..9b25944b727 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -1436,7 +1436,8 @@ export interface BundleGraph { getSymbolResolution( asset: Asset, symbol: Symbol, - boundary: ?Bundle, + boundaryBundle: ?Bundle, + boundaryAssets: ?Set, ): SymbolResolution; /** Returns a list of symbols that are exported by the asset, including re-exports. */ getExportedSymbols( @@ -1591,18 +1592,25 @@ export type Runtime = {| /** * @section packager */ -export type Packager = {| +export type Packager = {| loadConfig?: ({| config: Config, options: PluginOptions, logger: PluginLogger, - |}) => Promise | ConfigType, + |}) => Async, + loadGlobalInfo?: ({| + bundle: NamedBundle, + bundleGraph: BundleGraph, + options: PluginOptions, + logger: PluginLogger, + |}) => Async, package({| bundle: NamedBundle, bundleGraph: BundleGraph, options: PluginOptions, logger: PluginLogger, config: ConfigType, + globalInfo: GlobalInfoType, getInlineBundleContents: ( Bundle, BundleGraph, @@ -1614,12 +1622,18 @@ export type Packager = {| /** * @section optimizer */ -export type Optimizer = {| +export type Optimizer = {| loadConfig?: ({| config: Config, options: PluginOptions, logger: PluginLogger, - |}) => Promise | ConfigType, + |}) => Async, + loadGlobalInfo?: ({| + bundle: NamedBundle, + bundleGraph: BundleGraph, + options: PluginOptions, + logger: PluginLogger, + |}) => Async, optimize({| bundle: NamedBundle, bundleGraph: BundleGraph, @@ -1628,6 +1642,7 @@ export type Optimizer = {| options: PluginOptions, logger: PluginLogger, config: ConfigType, + globalInfo: GlobalInfoType, getSourceMapReference: (map: ?SourceMap) => Async, |}): Async, |}; diff --git a/packages/packagers/js/src/ESMOutputFormat.js b/packages/packagers/js/src/ESMOutputFormat.js index aa53bdd02cc..5e749b3ead1 100644 --- a/packages/packagers/js/src/ESMOutputFormat.js +++ b/packages/packagers/js/src/ESMOutputFormat.js @@ -80,7 +80,7 @@ export class ESMOutputFormat implements OutputFormat { local, exportAs, } of this.packager.exportedSymbols.values()) { - if (this.packager.wrappedAssets.has(asset.id)) { + if (this.packager.islands.has(asset)) { let obj = `parcelRequire("${this.packager.bundleGraph.getAssetPublicId( asset, )}")`; diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index cd88c660a82..6031dd171b2 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -87,16 +87,23 @@ export class ScopeHoistingPackager { externals: Map> = new Map(); topLevelNames: Map = new Map(); seenAssets: Set = new Set(); - wrappedAssets: Set = new Set(); hoistedRequires: Map> = new Map(); needsPrelude: boolean = false; usedHelpers: Set = new Set(); + /** wrapped root asset -> assets in the island of the root (excluding root) */ + islands: Map> = new Map(); + /** non-island-root asset -> corresponding island root, if any */ + islandRoot: Map = new Map(); + unwrappedAssets: Set = new Set(); + usedByUnwrappedEntries: Set = new Set(); + duplicatedAssets: Set; constructor( options: PluginOptions, bundleGraph: BundleGraph, bundle: NamedBundle, parcelRequireName: string, + duplicatedAssets: Array, ) { this.options = options; this.bundleGraph = bundleGraph; @@ -112,10 +119,11 @@ export class ScopeHoistingPackager { this.bundle.bundleBehavior !== 'isolated'; this.globalNames = GLOBALS_BY_CONTEXT[bundle.env.context]; + this.duplicatedAssets = new Set(duplicatedAssets); } async package(): Promise<{|contents: string, map: ?SourceMap|}> { - let wrappedAssets = await this.loadAssets(); + await this.loadAssets(); this.buildExportedSymbols(); // If building a library, the target is actually another bundler rather @@ -137,7 +145,7 @@ export class ScopeHoistingPackager { let lineCount = 0; let sourceMap = null; let processAsset = asset => { - let [content, map, lines] = this.visitAsset(asset); + let [content, map, lines] = this.visitAsset(asset, null, false); if (sourceMap && map) { sourceMap.addSourceMap(map, lineCount); } else if (this.bundle.env.sourceMap) { @@ -150,7 +158,7 @@ export class ScopeHoistingPackager { // Hoist wrapped asset to the top of the bundle to ensure that they are registered // before they are used. - for (let asset of wrappedAssets) { + for (let asset of this.islands.keys()) { if (!this.seenAssets.has(asset.id)) { processAsset(asset); } @@ -184,7 +192,7 @@ export class ScopeHoistingPackager { // If any of the entry assets are wrapped, call parcelRequire so they are executed. for (let entry of entries) { - if (this.wrappedAssets.has(entry.id) && !this.isScriptEntry(entry)) { + if (this.islands.has(entry) && !this.isScriptEntry(entry)) { let parcelRequire = `parcelRequire(${JSON.stringify( this.bundleGraph.getAssetPublicId(entry), )});\n`; @@ -244,10 +252,13 @@ export class ScopeHoistingPackager { }; } - async loadAssets(): Promise> { + async loadAssets(): Promise { let queue = new PromiseQueue({maxConcurrent: 32}); - let wrapped = []; - this.bundle.traverseAssets(asset => { + let wrapped = new Set(); + // let start = process.hrtime(); + + this.bundle.traverseAssets((asset, isEntry) => { + // console.log(this.bundleGraph.getAssetPublicId(asset), asset.filePath); queue.add(async () => { let [code, map] = await Promise.all([ asset.getCode(), @@ -256,38 +267,182 @@ export class ScopeHoistingPackager { return [asset.id, {code, map}]; }); + isEntry ??= true; + + // if (this.bundle.name === 'EmojiPickerComponent.19f337e2.js') { + // console.log(asset, {isEntry}); + // } + + // if (this.bundle.name === 'index.08e6a922.js') { + // console.log( + // asset.filePath, + // asset.meta.shouldWrap, + // // TODO shared? + // this.isAsyncBundle && isEntry, + // this.bundle.env.sourceType === 'script', + // this.bundleGraph.isAssetReferenced(this.bundle, asset), + // this.bundleGraph + // .getIncomingDependencies(asset) + // .some(dep => dep.meta.shouldWrap && dep.specifierType !== 'url'), + // // has to be dedupliated at runtime + // // TODO invalidation + // this.bundleGraph.getBundlesWithAsset(asset).some( + // // TODO object equality checks fail for bundle and target objects + // // TODO how to handle targets? + // b => + // b.id !== this.bundle.id && + // b.target.name === this.bundle.target.name, + // ), + // ); + // } + if ( asset.meta.shouldWrap || - this.isAsyncBundle || + // TODO shared? + (this.isAsyncBundle && isEntry) || this.bundle.env.sourceType === 'script' || this.bundleGraph.isAssetReferenced(this.bundle, asset) || this.bundleGraph .getIncomingDependencies(asset) - .some(dep => dep.meta.shouldWrap && dep.specifierType !== 'url') + .some(dep => dep.meta.shouldWrap && dep.specifierType !== 'url') || + // has to be dedupliated at runtime + this.duplicatedAssets.has(asset.id) ) { - this.wrappedAssets.add(asset.id); - wrapped.push(asset); + wrapped.add(asset); + this.unwrappedAssets.delete(asset); + } else { + this.unwrappedAssets.add(asset); } + + return false; }); - for (let wrappedAssetRoot of [...wrapped]) { - this.bundle.traverseAssets((asset, _, actions) => { - if (asset === wrappedAssetRoot) { - return; - } + // if (this.bundle.name === 'EmojiPickerComponent.19f337e2.js') { + // console.log({wrapped}, this.bundle.getEntryAssets()); + // } - if (this.wrappedAssets.has(asset.id)) { + let entries = this.bundle.getEntryAssets(); + for (let entry of entries) { + this.bundle.traverseAssets((asset, _, actions) => { + if (wrapped.has(asset)) { actions.skipChildren(); return; } + this.usedByUnwrappedEntries.add(asset); + }, entry); + } + + // this.bundle.traverseAssets((asset, _, actions) => { + // if (wrapped.has(asset) || this.bundle.getEntryAssets().includes(asset)) { + // console.log('skip', asset); + // actions.skipChildren(); + // return; + // } + // // todo not sure if this is really working to be unwrapped only (e.g. async/shared) + // console.log('add', asset); + // this.usedByUnwrappedEntries.add(asset); + // }); + + // console.log({wrapped, getEntryAssets: this.bundle.getEntryAssets()}); + + let usedByMultiple = new Set(); + + while (wrapped.size > 0) { + // Walk through the current frontier and create islands as deep as possible. + // console.log({wrapped}); + for (let root of [...wrapped]) { + // console.log('root', root); + let island = this.islands.get(root) ?? new Set([root]); + this.islands.set(root, island); + this.unwrappedAssets.delete(root); + wrapped.delete(root); + + this.bundle.traverseAssets((a, ctx, actions) => { + let aRoot = this.islandRoot.get(a); + // console.log('aRoot', root, a, aRoot, usedByMultiple.has(a)); + if (aRoot != null) { + if (aRoot !== root) { + // Was incorrectly added into an island by another preceding iteration. + // Rollback to make it its own separate island in a subsequent iteration. + wrapped.add(a); + // make sure that it has everything it needs + // TODO quadratic/unbounded complexity? + wrapped.add(nullthrows(aRoot)); + + // console.log('has', a, aRoot, root); + + this.bundle.traverseAssets((b, _, actions) => { + let r = this.islandRoot.get(b); + if (r == null || r != aRoot) { + actions.skipChildren(); + return; + } + // console.log('delete', { + // root, + // outer: a, + // outerRoot: aRoot, + // innerRoot: r, + // b, + // }); + nullthrows(this.islands.get(aRoot)).delete(b); + this.unwrappedAssets.add(b); + this.islandRoot.delete(b); + usedByMultiple.add(b); + }, a); + actions.skipChildren(); + return; + } else { + // already part of this island + return; + } + } + + // Don't add root to its own island + if (a === root) { + return; + } + if (this.islands.has(a)) { + // Another island starts here. + actions.skipChildren(); + return; + } + if (this.usedByUnwrappedEntries.has(a) || usedByMultiple.has(a)) { + // Has to be wrapped so that unwrapped entries can access it. + wrapped.add(a); + actions.skipChildren(); + return; + } - this.wrappedAssets.add(asset.id); - wrapped.push(asset); - }, wrappedAssetRoot); + usedByMultiple.delete(a); + this.unwrappedAssets.delete(a); + // console.log('add', root, a); + island.add(a); + this.islandRoot.set(a, root); + }, root); + usedByMultiple.delete(root); + } } + // if (this.bundle.name === 'EmojiPickerComponent.19f337e2.js') { + // console.log(this.bundle.name, this.isAsyncBundle); + // console.log({ + // islands: this.islands, + // islandRoot: this.islandRoot, + // unwrappedAssets: this.unwrappedAssets, + // usedByUnwrappedEntries: this.usedByUnwrappedEntries, + // }); + // } + // let end = process.hrtime(start); + // console.log(this.bundle.name, end[0] * 1000 + end[1] / 1000000); + + // this.bundle.traverseAssets(asset => { + // invariant( + // this.unwrappedAssets.has(asset) || + // this.islands.has(asset) != this.islandRoot.has(asset), + // asset.filePath, + // ); + // }); this.assetOutputs = new Map(await queue.run()); - return wrapped; } buildExportedSymbols() { @@ -301,7 +456,7 @@ export class ScopeHoistingPackager { // TODO: handle ESM exports of wrapped entry assets... let entry = this.bundle.getMainEntry(); - if (entry && !this.wrappedAssets.has(entry.id)) { + if (entry && !this.islands.has(entry)) { for (let { asset, exportAs, @@ -372,20 +527,32 @@ export class ScopeHoistingPackager { return `${obj}[${JSON.stringify(property)}]`; } - visitAsset(asset: Asset): [string, ?SourceMap, number] { + visitAsset( + asset: Asset, + parentDepContent: ?Array<[string, ?SourceMap, number]>, + parentDepContentAddSelf: boolean, + ): [string, ?SourceMap, number] { invariant(!this.seenAssets.has(asset.id), 'Already visited asset'); this.seenAssets.add(asset.id); let {code, map} = nullthrows(this.assetOutputs.get(asset.id)); - return this.buildAsset(asset, code, map); + return this.buildAsset( + asset, + code, + map, + parentDepContent, + parentDepContentAddSelf, + ); } buildAsset( asset: Asset, code: string, map: ?Buffer, + parentDepContent: ?Array<[string, ?SourceMap, number]>, + parentDepContentAddSelf: boolean, ): [string, ?SourceMap, number] { - let shouldWrap = this.wrappedAssets.has(asset.id); + let isIslandRoot = this.islands.has(asset); let deps = this.bundleGraph.getDependencies(asset); let sourceMap = @@ -394,7 +561,7 @@ export class ScopeHoistingPackager { : null; // If this asset is skipped, just add dependencies and not the asset's content. - if (this.shouldSkipAsset(asset)) { + if (this.shouldSkipAsset(asset) && !this.islands.has(asset)) { let depCode = ''; let lineCount = 0; for (let dep of deps) { @@ -416,7 +583,11 @@ export class ScopeHoistingPackager { this.bundle.hasAsset(resolved) && !this.seenAssets.has(resolved.id) ) { - let [code, map, lines] = this.visitAsset(resolved); + let [code, map, lines] = this.visitAsset( + resolved, + parentDepContent, + false, + ); depCode += code + '\n'; if (sourceMap && map) { sourceMap.addSourceMap(map, lineCount); @@ -425,6 +596,10 @@ export class ScopeHoistingPackager { } } + if (parentDepContentAddSelf) { + nullthrows(parentDepContent).push([depCode, sourceMap, lineCount]); + } + return [depCode, sourceMap, lineCount]; } @@ -494,14 +669,18 @@ export class ScopeHoistingPackager { this.bundle.hasAsset(resolved) && !this.seenAssets.has(resolved.id) ) { - // If this asset is wrapped, we need to hoist the code for the dependency + // If the referenced asset is wrapped, we need to hoist the code for the dependency // outside our parcelRequire.register wrapper. This is safe because all // assets referenced by this asset will also be wrapped. Otherwise, inline the // asset content where the import statement was. - if (shouldWrap) { - depContent.push(this.visitAsset(resolved)); + if (this.islands.has(resolved)) { + this.visitAsset(resolved, depContent, true); } else { - let [depCode, depMap, depLines] = this.visitAsset(resolved); + let [depCode, depMap, depLines] = this.visitAsset( + resolved, + depContent, + false, + ); res = depCode + '\n' + res; lines += 1 + depLines; map = depMap; @@ -546,9 +725,7 @@ export class ScopeHoistingPackager { }); } - // If the asset is wrapped, we need to insert the dependency code outside the parcelRequire.register - // wrapper. Dependencies must be inserted AFTER the asset is registered so that circular dependencies work. - if (shouldWrap) { + if (isIslandRoot) { // Offset by one line for the parcelRequire.register wrapper. sourceMap?.offsetLines(1, 1); lineCount++; @@ -561,19 +738,39 @@ ${code} `; lineCount += 2; + this.needsPrelude = true; + } - for (let [depCode, map, lines] of depContent) { - if (!depCode) continue; - code += depCode + '\n'; - if (sourceMap && map) { - sourceMap.addSourceMap(map, lineCount); + // If the asset is wrapped, we need to insert the dependency code outside the parcelRequire.register + // wrapper. Dependencies must be inserted AFTER the asset is registered so that circular dependencies work. + if (depContent.length > 0) { + // Either this is an island root with no parent, or append the deps to the parent's array + if (parentDepContent == null) { + invariant(isIslandRoot); + // is a toplevel asset + this.needsPrelude = true; + for (let [depCode, map, lines] of depContent) { + if (!depCode) continue; + code += depCode + '\n'; + if (sourceMap && map) { + sourceMap.addSourceMap(map, lineCount); + } + lineCount += lines + 1; + } + } else { + if (parentDepContentAddSelf) { + parentDepContent.push([code, sourceMap, lineCount]); } - lineCount += lines + 1; - } - this.needsPrelude = true; + // make the root of the island append it after the island + parentDepContent.push(...depContent); + } + } else if (parentDepContentAddSelf) { + invariant(isIslandRoot); + nullthrows(parentDepContent).push([code, sourceMap, lineCount]); } + // console.log('buildAsset 3.2', asset, code); return [code, sourceMap, lineCount]; } @@ -654,7 +851,7 @@ ${code} // If this asset is wrapped, we need to replace the exports namespace with `module.exports`, // which will be provided to us by the wrapper. if ( - this.wrappedAssets.has(asset.id) || + this.islands.has(asset) || (this.bundle.env.outputFormat === 'commonjs' && asset === this.bundle.getMainEntry()) ) { @@ -758,19 +955,41 @@ ${code} imported: string, dep?: Dependency, ): string { + let island = new Set([ + ...(this.islands.get(this.islandRoot.get(parentAsset) ?? parentAsset) ?? + this.unwrappedAssets), + parentAsset, + ]); let { asset: resolvedAsset, exportSymbol, symbol, - } = this.bundleGraph.getSymbolResolution(resolved, imported, this.bundle); + } = this.bundleGraph.getSymbolResolution( + resolved, + imported, + this.bundle, + island, + ); if (resolvedAsset.type !== 'js') { // Graceful fallback for non-js imports return '{}'; } let isWrapped = !this.bundle.hasAsset(resolvedAsset) || - (this.wrappedAssets.has(resolvedAsset.id) && - resolvedAsset !== parentAsset); + (this.islands.has(resolvedAsset) && resolvedAsset !== parentAsset); + + // if ( + // parentAsset.filePath.endsWith( + // 'node_modules/@atlaskit/theme/dist/esm/components.js', + // ) + // ) { + // console.log( + // {resolved, imported}, + // {resolvedAsset, exportSymbol, symbol}, + // isWrapped, + // ); + // } + let staticExports = resolvedAsset.meta.staticExports !== false; let publicId = this.bundleGraph.getAssetPublicId(resolvedAsset); @@ -784,6 +1003,7 @@ ${code} // Only do this if the asset is part of a different bundle (so it was definitely // parcelRequire.register'ed there), or if it is indeed registered in this bundle. (!this.bundle.hasAsset(resolvedAsset) || + this.islands.has(resolvedAsset) || !this.shouldSkipAsset(resolvedAsset)) ) { let hoisted = this.hoistedRequires.get(dep.id); @@ -829,10 +1049,7 @@ ${code} if (imported === '*' || exportSymbol === '*' || isDefaultInterop) { // Resolve to the namespace object if requested or this is a CJS default interop reqiure. - if ( - parentAsset === resolvedAsset && - this.wrappedAssets.has(resolvedAsset.id) - ) { + if (parentAsset === resolvedAsset && this.islands.has(resolvedAsset)) { // Directly use module.exports for wrapped assets importing themselves. return 'module.exports'; } else { @@ -879,7 +1096,7 @@ ${code} let lineCount = 0; let isWrapped = !this.bundle.hasAsset(resolved) || - (this.wrappedAssets.has(resolved.id) && resolved !== parentAsset); + (this.islands.has(resolved) && resolved !== parentAsset); // If the resolved asset is wrapped and is imported in the top-level by this asset, // we need to run side effects when this asset runs. If the resolved asset is not @@ -915,7 +1132,7 @@ ${code} let prependLineCount = 0; let append = ''; - let shouldWrap = this.wrappedAssets.has(asset.id); + let isIslandRoot = this.islands.has(asset); let usedSymbols = nullthrows(this.bundleGraph.getUsedSymbols(asset)); let assetId = asset.meta.id; invariant(typeof assetId === 'string'); @@ -957,7 +1174,7 @@ ${code} // this asset. if ( asset.meta.staticExports === false || - shouldWrap || + isIslandRoot || usedNamespace || defaultInterop ) { @@ -966,7 +1183,7 @@ ${code} // by the wrapper instead. This is also true of CommonJS entry assets, which will use // the `module.exports` object provided by CJS. if ( - !shouldWrap && + !isIslandRoot && (this.bundle.env.outputFormat !== 'commonjs' || asset !== this.bundle.getMainEntry()) ) { diff --git a/packages/packagers/js/src/index.js b/packages/packagers/js/src/index.js index 73f1ebc0aa6..63f3e097cdc 100644 --- a/packages/packagers/js/src/index.js +++ b/packages/packagers/js/src/index.js @@ -22,12 +22,37 @@ export default (new Packager({ parcelRequireName: 'parcelRequire' + hashString(name).slice(-4), }; }, + + loadGlobalInfo({bundle, bundleGraph}) { + let duplicatedAssets = []; + if (bundle.env.shouldScopeHoist) { + bundle.traverseAssets(asset => { + if ( + bundleGraph.getBundlesWithAsset(asset).some( + // TODO object equality checks fail for bundle and target objects + // TODO how to handle targets? + b => b.id !== bundle.id && b.target.name === bundle.target.name, + ) + ) { + duplicatedAssets.push(asset.id); + } + }); + } + + // config.setCacheKey(hashString(JSON.stringify(duplicatedAssets))); + + return { + duplicatedAssets, + }; + }, + async package({ bundle, bundleGraph, getInlineBundleContents, getSourceMapReference, config, + globalInfo, options, }) { // If this is a non-module script, and there is only one asset with no dependencies, @@ -50,7 +75,8 @@ export default (new Packager({ options, bundleGraph, bundle, - nullthrows(config).parcelRequireName, + config.parcelRequireName, + globalInfo.duplicatedAssets, ) : new DevPackager( options,