diff --git a/packages/bundlers/experimental/package.json b/packages/bundlers/experimental/package.json new file mode 100644 index 00000000000..c486af4d24c --- /dev/null +++ b/packages/bundlers/experimental/package.json @@ -0,0 +1,30 @@ +{ + "name": "@parcel/bundler-experimental", + "version": "2.0.0-rc.0", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "repository": { + "type": "git", + "url": "https://github.com/parcel-bundler/parcel.git" + }, + "main": "lib/ExperimentalBundler.js", + "source": "src/ExperimentalBundler.js", + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.0.1" + }, + "dependencies": { + "@parcel/diagnostic": "^2.0.1", + "@parcel/graph": "^2.0.1", + "@parcel/hash": "^2.0.1", + "@parcel/plugin": "^2.0.1", + "@parcel/utils": "^2.0.1", + "nullthrows": "^1.1.1" + } +} diff --git a/packages/bundlers/experimental/src/ExperimentalBundler.js b/packages/bundlers/experimental/src/ExperimentalBundler.js new file mode 100644 index 00000000000..99719dba6c6 --- /dev/null +++ b/packages/bundlers/experimental/src/ExperimentalBundler.js @@ -0,0 +1,1005 @@ +// @flow strict-local + +import type { + Asset, + Bundle as LegacyBundle, + BundleBehavior, + BundleGroup, + Dependency, + Environment, + Config, + MutableBundleGraph, + PluginOptions, + Target, +} from '@parcel/types'; +import type {NodeId} from '@parcel/graph'; +import type {SchemaEntity} from '@parcel/utils'; +import {ContentGraph, Graph} from '@parcel/graph'; + +import invariant from 'assert'; +import {ALL_EDGE_TYPES} from '@parcel/graph'; +import {Bundler} from '@parcel/plugin'; +import {validateSchema, DefaultMap} from '@parcel/utils'; +import nullthrows from 'nullthrows'; +import {encodeJSONKeyComponent} from '@parcel/diagnostic'; + +type BundlerConfig = {| + http?: number, + minBundles?: number, + minBundleSize?: number, + maxParallelRequests?: number, +|}; + +type ResolvedBundlerConfig = {| + minBundles: number, + minBundleSize: number, + maxParallelRequests: number, +|}; + +// Default options by http version. +const HTTP_OPTIONS = { + '1': { + minBundles: 1, + minBundleSize: 30000, + maxParallelRequests: 6, + }, + '2': { + minBundles: 1, + minBundleSize: 20000, + maxParallelRequests: 25, + }, +}; + +type AssetId = string; +type BundleRoot = Asset; +export type Bundle = {| + assets: Set, + internalizedAssetIds: Array, + bundleBehavior?: ?BundleBehavior, + needsStableName: boolean, + size: number, + sourceBundles: Array, + target: Target, + env: Environment, + type: string, +|}; + +const dependencyPriorityEdges = { + sync: 1, + parallel: 2, + lazy: 3, +}; + +type DependencyBundleGraph = ContentGraph< + | {| + value: Bundle, + type: 'bundle', + |} + | {| + value: Dependency, + type: 'dependency', + |}, + number, +>; +type IdealGraph = {| + dependencyBundleGraph: DependencyBundleGraph, + bundleGraph: Graph, + bundleGroupBundleIds: Array, + assetReference: DefaultMap>, + sharedToSourceBundleIds: Map>, +|}; + +export default (new Bundler({ + loadConfig({config, options}) { + return loadBundlerConfig(config, options); + }, + + bundle({bundleGraph, config}) { + decorateLegacyGraph(createIdealGraph(bundleGraph, config), bundleGraph); + }, + optimize() {}, +}): Bundler); + +function decorateLegacyGraph( + idealGraph: IdealGraph, + bundleGraph: MutableBundleGraph, +): void { + let idealBundleToLegacyBundle: Map = new Map(); + + let { + bundleGraph: idealBundleGraph, + dependencyBundleGraph, + bundleGroupBundleIds, + sharedToSourceBundleIds, + } = idealGraph; + let entryBundleToBundleGroup: Map = new Map(); + + // Step 1: Create bundle groups, bundles, and shared bundles and add assets to them + for (let [bundleNodeId, idealBundle] of idealBundleGraph.nodes) { + if (idealBundle === 'root') continue; + let [entryAsset] = [...idealBundle.assets]; + let bundleGroup; + let bundle; + + if (bundleGroupBundleIds.includes(bundleNodeId)) { + let dependencies = dependencyBundleGraph + .getNodeIdsConnectedTo( + dependencyBundleGraph.getNodeIdByContentKey(String(bundleNodeId)), + // $FlowFixMe[incompatible-call] + ALL_EDGE_TYPES, + ) + .map(nodeId => { + let dependency = nullthrows(dependencyBundleGraph.getNode(nodeId)); + invariant(dependency.type === 'dependency'); + return dependency.value; + }); + for (let dependency of dependencies) { + bundleGroup = bundleGraph.createBundleGroup( + dependency, + idealBundle.target, + ); + } + invariant(bundleGroup); + entryBundleToBundleGroup.set(bundleNodeId, bundleGroup); + + bundle = nullthrows( + bundleGraph.createBundle({ + entryAsset, + needsStableName: idealBundle.needsStableName, + bundleBehavior: idealBundle.bundleBehavior, + target: idealBundle.target, + }), + ); + + bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); + } else if (idealBundle.sourceBundles.length > 0) { + bundle = nullthrows( + bundleGraph.createBundle({ + uniqueKey: + [...idealBundle.assets].map(asset => asset.id).join(',') + + idealBundle.sourceBundles.join(','), + needsStableName: idealBundle.needsStableName, + bundleBehavior: idealBundle.bundleBehavior, + type: idealBundle.type, + target: idealBundle.target, + env: idealBundle.env, + }), + ); + } else { + bundle = nullthrows( + bundleGraph.createBundle({ + entryAsset, + needsStableName: idealBundle.needsStableName, + bundleBehavior: idealBundle.bundleBehavior, + target: idealBundle.target, + }), + ); + } + + idealBundleToLegacyBundle.set(idealBundle, bundle); + + for (let asset of idealBundle.assets) { + bundleGraph.addAssetToBundle(asset, bundle); + } + } + // Step 2: Internalize dependencies for bundles + for (let [, idealBundle] of idealBundleGraph.nodes) { + if (idealBundle === 'root') continue; + let bundle = nullthrows(idealBundleToLegacyBundle.get(idealBundle)); + for (let internalized of idealBundle.internalizedAssetIds) { + let incomingDeps = bundleGraph.getIncomingDependencies( + bundleGraph.getAssetById(internalized), + ); + for (let incomingDep of incomingDeps) { + if ( + incomingDep.priority === 'lazy' && + bundle.hasDependency(incomingDep) + ) { + bundleGraph.internalizeAsyncDependency(bundle, incomingDep); + } + } + } + } + // Step 3: Add bundles to their bundle groups + for (let [bundleId, bundleGroup] of entryBundleToBundleGroup) { + let outboundNodeIds = idealBundleGraph.getNodeIdsConnectedFrom(bundleId); + for (let id of outboundNodeIds) { + let siblingBundle = nullthrows(idealBundleGraph.getNode(id)); + invariant(siblingBundle !== 'root'); + let legacySiblingBundle = nullthrows( + idealBundleToLegacyBundle.get(siblingBundle), + ); + bundleGraph.addBundleToBundleGroup(legacySiblingBundle, bundleGroup); + } + } + + // Step 4: Add references to all bundles + for (let [asset, references] of idealGraph.assetReference) { + for (let [dependency, bundle] of references) { + let legacyBundle = nullthrows(idealBundleToLegacyBundle.get(bundle)); + bundleGraph.createAssetReference(dependency, asset, legacyBundle); + } + } + + for (let [sharedBundleId, sourceBundleIds] of sharedToSourceBundleIds) { + let sharedBundle = nullthrows(idealBundleGraph.getNode(sharedBundleId)); + if (sharedBundle === 'root') continue; + let legacySharedBundle = nullthrows( + idealBundleToLegacyBundle.get(sharedBundle), + ); + for (let sourceBundleId of sourceBundleIds) { + let sourceBundle = nullthrows(idealBundleGraph.getNode(sourceBundleId)); + if (sourceBundle === 'root') continue; + let legacySourceBundle = nullthrows( + idealBundleToLegacyBundle.get(sourceBundle), + ); + bundleGraph.createBundleReference(legacySourceBundle, legacySharedBundle); + } + } +} + +function createIdealGraph( + assetGraph: MutableBundleGraph, + config: ResolvedBundlerConfig, +): IdealGraph { + // Asset to the bundle and group it's an entry of + let bundleRoots: Map = new Map(); + let bundles: Map = new Map(); + let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph(); + let assetReference: DefaultMap< + Asset, + Array<[Dependency, Bundle]>, + > = new DefaultMap(() => []); + + // bundleRoot to all bundleRoot descendants + let reachableBundles: DefaultMap< + BundleRoot, + Set, + > = new DefaultMap(() => new Set()); + + let bundleGraph: Graph = new Graph(); + let stack: Array<[BundleRoot, NodeId]> = []; + + // bundleGraph that models bundleRoots and async deps only + let asyncBundleRootGraph: ContentGraph = + new ContentGraph(); + let bundleGroupBundleIds: Array = []; + + // Step 1: Find and create bundles for entries from assetGraph + let entries: Map = new Map(); + let sharedToSourceBundleIds: Map> = new Map(); + + assetGraph.traverse((node, context, actions) => { + if (node.type !== 'asset') { + return node; + } + + invariant( + context != null && context.type === 'dependency' && context.value.isEntry, + ); + entries.set(node.value, context.value); + actions.skipChildren(); + }); + + let rootNodeId = nullthrows(asyncBundleRootGraph.addNode('root')); + let bundleGraphRootNodeId = nullthrows(bundleGraph.addNode('root')); + asyncBundleRootGraph.setRootNodeId(rootNodeId); + bundleGraph.setRootNodeId(bundleGraphRootNodeId); + + for (let [asset, dependency] of entries) { + let bundle = createBundle({ + asset, + target: nullthrows(dependency.target), + needsStableName: dependency.isEntry, + }); + let nodeId = bundleGraph.addNode(bundle); + bundles.set(asset.id, nodeId); + bundleRoots.set(asset, [nodeId, nodeId]); + asyncBundleRootGraph.addEdge( + rootNodeId, + asyncBundleRootGraph.addNodeByContentKey(asset.id, asset), + ); + bundleGraph.addEdge(bundleGraphRootNodeId, nodeId); + + dependencyBundleGraph.addEdge( + dependencyBundleGraph.addNodeByContentKeyIfNeeded(dependency.id, { + value: dependency, + type: 'dependency', + }), + dependencyBundleGraph.addNodeByContentKeyIfNeeded(String(nodeId), { + value: bundle, + type: 'bundle', + }), + dependencyPriorityEdges[dependency.priority], + ); + bundleGroupBundleIds.push(nodeId); + } + + let assets = []; + + // Step 2: Traverse the asset graph and create bundles for asset type changes and async dependencies, + // only adding the entry asset of each bundle, not the subgraph. + assetGraph.traverse({ + enter(node, context) { + if (node.type === 'asset') { + assets.push(node.value); + + let bundleIdTuple = bundleRoots.get(node.value); + if (bundleIdTuple) { + // Push to the stack when a new bundle is created + stack.push([node.value, bundleIdTuple[1]]); + } + } else if (node.type === 'dependency') { + if (context == null) { + return node; + } + let dependency = node.value; + + invariant(context?.type === 'asset'); + let parentAsset = context.value; + + let assets = assetGraph.getDependencyAssets(dependency); + if (assets.length === 0) { + return node; + } + + for (let childAsset of assets) { + if ( + dependency.priority === 'lazy' || + childAsset.bundleBehavior === 'isolated' + ) { + let bundleId = bundles.get(childAsset.id); + let bundle; + if (bundleId == null) { + let firstBundleGroup = nullthrows( + bundleGraph.getNode(stack[0][1]), + ); + invariant(firstBundleGroup !== 'root'); + bundle = createBundle({ + asset: childAsset, + target: firstBundleGroup.target, + needsStableName: + dependency.bundleBehavior === 'inline' || + childAsset.bundleBehavior === 'inline' + ? false + : dependency.isEntry || dependency.needsStableName, + bundleBehavior: + dependency.bundleBehavior ?? childAsset.bundleBehavior, + }); + bundleId = bundleGraph.addNode(bundle); + bundles.set(childAsset.id, bundleId); + bundleRoots.set(childAsset, [bundleId, bundleId]); + bundleGroupBundleIds.push(bundleId); + bundleGraph.addEdge(bundleGraphRootNodeId, bundleId); + } else { + bundle = nullthrows(bundleGraph.getNode(bundleId)); + invariant(bundle !== 'root'); + } + + dependencyBundleGraph.addEdge( + dependencyBundleGraph.addNodeByContentKeyIfNeeded(dependency.id, { + value: dependency, + type: 'dependency', + }), + dependencyBundleGraph.addNodeByContentKeyIfNeeded( + String(bundleId), + { + value: bundle, + type: 'bundle', + }, + ), + dependencyPriorityEdges[dependency.priority], + ); + + // Walk up the stack until we hit a different asset type + // and mark each bundle as reachable from every parent bundle + for (let i = stack.length - 1; i >= 0; i--) { + let [stackAsset] = stack[i]; + if ( + stackAsset.type !== childAsset.type || + stackAsset.env.context !== childAsset.env.context || + stackAsset.env.isIsolated() + ) { + break; + } + reachableBundles.get(stackAsset).add(childAsset); + } + continue; + } + if ( + parentAsset.type !== childAsset.type || + dependency.priority === 'parallel' || + childAsset.bundleBehavior === 'inline' + ) { + let [parentBundleRoot, bundleGroupNodeId] = nullthrows( + stack[stack.length - 1], + ); + let bundleGroup = nullthrows( + bundleGraph.getNode(bundleGroupNodeId), + ); + invariant(bundleGroup !== 'root'); + + // Find an existing bundle of the same type within the bundle group. + let bundleId; + if ( + childAsset.bundleBehavior !== 'inline' && + dependency.priority !== 'parallel' + ) { + bundleId = + bundleGroup.type == childAsset.type + ? bundleGroupNodeId + : bundleGraph + .getNodeIdsConnectedFrom(bundleGroupNodeId) + .find(id => { + let node = bundleGraph.getNode(id); + return node !== 'root' && node?.type == childAsset.type; + }); + } + + let bundle; + if (bundleId == null) { + let parentBundleId = nullthrows(bundles.get(parentBundleRoot.id)); + let parentBundle = nullthrows( + bundleGraph.getNode(parentBundleId), + ); + invariant(parentBundle !== 'root'); + + // Create a new bundle if none of the same type exists already. + bundle = createBundle({ + asset: childAsset, + target: bundleGroup.target, + needsStableName: + childAsset.bundleBehavior === 'inline' || + dependency.bundleBehavior === 'inline' || + (dependency.priority === 'parallel' && + !dependency.needsStableName) + ? false + : parentBundle.needsStableName, + }); + bundleId = bundleGraph.addNode(bundle); + } else { + // Otherwise, merge this asset into the existing bundle. + bundle = bundleGraph.getNode(bundleId); + invariant(bundle != null && bundle !== 'root'); + bundle.assets.add(childAsset); + } + + bundles.set(childAsset.id, bundleId); + bundleRoots.set(childAsset, [bundleId, bundleGroupNodeId]); + bundleGraph.addEdge(bundleGraphRootNodeId, bundleId); + + if (bundleId != bundleGroupNodeId) { + dependencyBundleGraph.addEdge( + dependencyBundleGraph.addNodeByContentKeyIfNeeded( + dependency.id, + { + value: dependency, + type: 'dependency', + }, + ), + dependencyBundleGraph.addNodeByContentKeyIfNeeded( + String(bundleId), + { + value: bundle, + type: 'bundle', + }, + ), + dependencyPriorityEdges.parallel, + ); + + // Add an edge from the bundle group entry to the new bundle. + // This indicates that the bundle is loaded together with the entry + bundleGraph.addEdge(bundleGroupNodeId, bundleId); + } + + assetReference.get(childAsset).push([dependency, bundle]); + continue; + } + } + } + return node; + }, + exit(node) { + if (stack[stack.length - 1]?.[0] === node.value) { + stack.pop(); + } + }, + }); + + // Step 3: Determine reachability for every asset from each bundleRoot. + // This is later used to determine which bundles to place each asset in. + for (let [root] of bundleRoots) { + if (!entries.has(root)) { + asyncBundleRootGraph.addNodeByContentKey(root.id, root); + } + } + + // Models bundleRoots and the assets that require it synchronously + let reachableRoots: ContentGraph = new ContentGraph(); + for (let [root] of bundleRoots) { + let rootNodeId = reachableRoots.addNodeByContentKeyIfNeeded(root.id, root); + assetGraph.traverse((node, isAsync, actions) => { + if (node.value === root) { + return; + } + + if (node.type === 'dependency') { + let dependency = node.value; + + if (dependencyBundleGraph.hasContentKey(dependency.id)) { + if ( + dependency.priority === 'lazy' || + dependency.priority === 'parallel' + ) { + let assets = assetGraph.getDependencyAssets(dependency); + if (assets.length === 0) { + return node; + } + + invariant(assets.length === 1); + let bundleRoot = assets[0]; + let bundle = nullthrows( + bundleGraph.getNode(nullthrows(bundles.get(bundleRoot.id))), + ); + if ( + bundle !== 'root' && + bundle.bundleBehavior !== 'isolated' && + bundle.bundleBehavior !== 'inline' && + !bundle.env.isIsolated() + ) { + asyncBundleRootGraph.addEdge( + asyncBundleRootGraph.getNodeIdByContentKey(root.id), + asyncBundleRootGraph.getNodeIdByContentKey(bundleRoot.id), + ); + } + } + return; + } + return; + } + + if (bundleRoots.has(node.value)) { + actions.skipChildren(); + return; + } + + let nodeId = reachableRoots.addNodeByContentKeyIfNeeded( + node.value.id, + node.value, + ); + reachableRoots.addEdge(rootNodeId, nodeId); + }, root); + } + + // Maps a given bundleRoot to the assets reachable from it, + // and the bundleRoots reachable from each of these assets + let ancestorAssets: Map< + BundleRoot, + Map | null>, + > = new Map(); + + // Reference count of each asset available within a given bundleRoot's bundle group + let assetRefsInBundleGroup: DefaultMap< + BundleRoot, + DefaultMap, + > = new DefaultMap(() => new DefaultMap(() => 0)); + + // Step 4: Determine assets that should be duplicated by computing asset availability in each bundle group + for (let nodeId of asyncBundleRootGraph.topoSort()) { + const bundleRoot = asyncBundleRootGraph.getNode(nodeId); + if (bundleRoot === 'root') continue; + invariant(bundleRoot != null); + let ancestors = ancestorAssets.get(bundleRoot); + + // First consider bundle group asset availability, processing only + // non-isolated bundles within that bundle group + let bundleGroupId = nullthrows(bundleRoots.get(bundleRoot))[1]; + // Map of assets in the bundle group to their refcounts + let assetRefs = assetRefsInBundleGroup.get(bundleRoot); + + for (let bundleIdInGroup of [ + bundleGroupId, + ...bundleGraph.getNodeIdsConnectedFrom(bundleGroupId), + ]) { + let bundleInGroup = nullthrows(bundleGraph.getNode(bundleIdInGroup)); + invariant(bundleInGroup !== 'root'); + if ( + bundleInGroup.bundleBehavior === 'isolated' || + bundleInGroup.bundleBehavior === 'inline' + ) { + continue; + } + let [bundleRoot] = [...bundleInGroup.assets]; + // Assets directly connected to current bundleRoot + let assetsFromBundleRoot = reachableRoots + .getNodeIdsConnectedFrom( + reachableRoots.getNodeIdByContentKey(bundleRoot.id), + ) + .map(id => nullthrows(reachableRoots.getNode(id))); + + for (let asset of assetsFromBundleRoot) { + assetRefs.set(asset, assetRefs.get(asset) + 1); + } + } + + // Enumerate bundleRoots connected to the node (parent), taking the intersection + // between the assets synchronously loaded by the parent, and those loaded by the child. + let bundleGroupAssets = new Set(assetRefs.keys()); + + let combined = ancestors + ? new Map([...bundleGroupAssets, ...ancestors.keys()].map(a => [a, null])) + : new Map([...bundleGroupAssets].map(a => [a, null])); + + let children = asyncBundleRootGraph.getNodeIdsConnectedFrom(nodeId); + + for (let childId of children) { + let child = asyncBundleRootGraph.getNode(childId); + invariant(child !== 'root' && child != null); + const availableAssets = ancestorAssets.get(child); + + if (availableAssets != null) { + ancestryIntersect(availableAssets, combined); + } else { + ancestorAssets.set(child, combined); + } + } + + let siblingAncestors = ancestors + ? ancestryUnion(new Set(ancestors.keys()), assetRefs, bundleRoot) + : new Map([...bundleGroupAssets].map(a => [a, [bundleRoot]])); + + for (let bundleIdInGroup of bundleGraph.getNodeIdsConnectedFrom( + bundleGroupId, + )) { + let bundleInGroup = bundleGraph.getNode(bundleIdInGroup); + invariant( + bundleInGroup != null && + bundleInGroup !== 'root' && + bundleInGroup.assets != null, + ); + + let [bundleRoot] = [...bundleInGroup.assets]; + + const availableAssets = ancestorAssets.get(bundleRoot); + + if (availableAssets != null) { + ancestryIntersect(availableAssets, siblingAncestors); + } else { + ancestorAssets.set(bundleRoot, siblingAncestors); + } + } + } + + // Step 5: Place all assets into bundles or create shared bundles. Each asset + // is placed into a single bundle based on the bundle entries it is reachable from. + // This creates a maximally code split bundle graph with no duplication. + for (let asset of assets) { + // Unreliable bundleRoot assets which need to pulled in by shared bundles or other means + let reachable: Array = getReachableBundleRoots( + asset, + reachableRoots, + ).reverse(); + + // Filter out bundles from this asset's reachable array if + // bundle does not contain the asset in its ancestry + // or if any of the bundles in the ancestry have a refcount of <= 1 for that asset, + // meaning it may not be deduplicated. Otherwise, decrement all references in + // the ancestry and keep it + reachable = reachable.filter(b => { + let ancestry = ancestorAssets.get(b)?.get(asset); + if (ancestry === undefined) { + // No reachable bundles from this asset + return true; + } else if (ancestry === null) { + // Asset is reachable from this bundle + return false; + } else { + // If every bundle in its ancestry has more than 1 reference to the asset + if ( + ancestry.every( + bundleId => assetRefsInBundleGroup.get(bundleId).get(asset) > 1, + ) + ) { + for (let bundleRoot of ancestry) { + assetRefsInBundleGroup + .get(bundleRoot) + .set( + asset, + assetRefsInBundleGroup.get(bundleRoot).get(asset) - 1, + ); + } + return false; + } + return true; + } + }); + + let rootBundleTuple = bundleRoots.get(asset); + if (rootBundleTuple != null) { + let rootBundle = nullthrows(bundleGraph.getNode(rootBundleTuple[0])); + invariant(rootBundle !== 'root'); + + if (!rootBundle.env.isIsolated()) { + if (!bundles.has(asset.id)) { + bundles.set(asset.id, rootBundleTuple[0]); + } + + for (let reachableAsset of reachable) { + if (reachableAsset !== asset) { + bundleGraph.addEdge( + nullthrows(bundleRoots.get(reachableAsset))[1], + rootBundleTuple[0], + ); + } + } + + let willInternalizeRoots = asyncBundleRootGraph + .getNodeIdsConnectedTo( + asyncBundleRootGraph.getNodeIdByContentKey(asset.id), + ) + .map(id => nullthrows(asyncBundleRootGraph.getNode(id))) + .filter(bundleRoot => { + if (bundleRoot === 'root') { + return false; + } + + return ( + reachableRoots.hasEdge( + reachableRoots.getNodeIdByContentKey(bundleRoot.id), + reachableRoots.getNodeIdByContentKey(asset.id), + ) || ancestorAssets.get(bundleRoot)?.has(asset) + ); + }) + .map(bundleRoot => { + // For Flow + invariant(bundleRoot !== 'root'); + return bundleRoot; + }); + + for (let bundleRoot of willInternalizeRoots) { + if (bundleRoot !== asset) { + let bundle = nullthrows( + bundleGraph.getNode(nullthrows(bundles.get(bundleRoot.id))), + ); + invariant(bundle !== 'root'); + bundle.internalizedAssetIds.push(asset.id); + } + } + } + } else if (reachable.length > 0) { + let reachableEntries = reachable.filter( + a => entries.has(a) || !a.isBundleSplittable, + ); + reachable = reachable.filter( + a => !entries.has(a) && a.isBundleSplittable, + ); + + // Add assets to non-splittable bundles. + for (let entry of reachableEntries) { + let bundleId = nullthrows(bundles.get(entry.id)); + let bundle = nullthrows(bundleGraph.getNode(bundleId)); + invariant(bundle !== 'root'); + bundle.assets.add(asset); + bundle.size += asset.stats.size; + } + + // Create shared bundles for splittable bundles. + if (reachable.length > 0) { + let sourceBundles = reachable.map(a => nullthrows(bundles.get(a.id))); + let key = reachable.map(a => a.id).join(','); + let bundleId = bundles.get(key); + let bundle; + if (bundleId == null) { + let firstSourceBundle = nullthrows( + bundleGraph.getNode(sourceBundles[0]), + ); + invariant(firstSourceBundle !== 'root'); + bundle = createBundle({ + target: firstSourceBundle.target, + type: firstSourceBundle.type, + env: firstSourceBundle.env, + }); + bundle.sourceBundles = sourceBundles; + bundleId = bundleGraph.addNode(bundle); + bundles.set(key, bundleId); + } else { + bundle = nullthrows(bundleGraph.getNode(bundleId)); + invariant(bundle !== 'root'); + } + bundle.assets.add(asset); + bundle.size += asset.stats.size; + + for (let sourceBundleId of sourceBundles) { + if (bundleId !== sourceBundleId) { + bundleGraph.addEdge(sourceBundleId, bundleId); + } + } + sharedToSourceBundleIds.set(bundleId, sourceBundles); + + dependencyBundleGraph.addNodeByContentKeyIfNeeded(String(bundleId), { + value: bundle, + type: 'bundle', + }); + } + } + } + + // Step 7: Merge any sibling bundles required by entry bundles back into the entry bundle. + // Entry bundles must be predictable, so cannot have unpredictable siblings. + for (let [bundleNodeId, bundle] of bundleGraph.nodes) { + if (bundle === 'root') continue; + if (bundle.sourceBundles.length > 0 && bundle.size < config.minBundleSize) { + sharedToSourceBundleIds.delete(bundleNodeId); + removeBundle(bundleGraph, bundleNodeId); + } + } + + return { + bundleGraph, + dependencyBundleGraph, + bundleGroupBundleIds, + assetReference, + sharedToSourceBundleIds, + }; +} + +const CONFIG_SCHEMA: SchemaEntity = { + type: 'object', + properties: { + http: { + type: 'number', + enum: Object.keys(HTTP_OPTIONS).map(k => Number(k)), + }, + minBundles: { + type: 'number', + }, + minBundleSize: { + type: 'number', + }, + maxParallelRequests: { + type: 'number', + }, + }, + additionalProperties: false, +}; + +function createBundle( + opts: + | {| + target: Target, + env: Environment, + type: string, + needsStableName?: boolean, + bundleBehavior?: ?BundleBehavior, + |} + | {| + target: Target, + asset: Asset, + env?: Environment, + type?: string, + needsStableName?: boolean, + bundleBehavior?: ?BundleBehavior, + |}, +): Bundle { + if (opts.asset == null) { + return { + assets: new Set(), + internalizedAssetIds: [], + size: 0, + sourceBundles: [], + target: opts.target, + type: nullthrows(opts.type), + env: nullthrows(opts.env), + needsStableName: Boolean(opts.needsStableName), + bundleBehavior: opts.bundleBehavior, + }; + } + + let asset = nullthrows(opts.asset); + return { + assets: new Set([asset]), + internalizedAssetIds: [], + size: asset.stats.size, + sourceBundles: [], + target: opts.target, + type: opts.type ?? asset.type, + env: opts.env ?? asset.env, + needsStableName: Boolean(opts.needsStableName), + bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior, + }; +} + +function removeBundle(bundleGraph: Graph, bundleId: NodeId) { + let bundle = nullthrows(bundleGraph.getNode(bundleId)); + invariant(bundle !== 'root'); + + for (let asset of bundle.assets) { + for (let sourceBundleId of bundle.sourceBundles) { + let sourceBundle = nullthrows(bundleGraph.getNode(sourceBundleId)); + invariant(sourceBundle !== 'root'); + sourceBundle.assets.add(asset); + sourceBundle.size += asset.stats.size; + } + } + + bundleGraph.removeNode(bundleId); +} + +async function loadBundlerConfig( + config: Config, + options: PluginOptions, +): Promise { + let conf = await config.getConfig([], { + packageKey: '@parcel/bundler-default', + }); + if (!conf) { + return HTTP_OPTIONS['2']; + } + + invariant(conf?.contents != null); + + validateSchema.diagnostic( + CONFIG_SCHEMA, + { + data: conf?.contents, + source: await options.inputFS.readFile(conf.filePath, 'utf8'), + filePath: conf.filePath, + prependKey: `/${encodeJSONKeyComponent('@parcel/bundler-default')}`, + }, + '@parcel/bundler-default', + 'Invalid config for @parcel/bundler-default', + ); + + let http = conf.contents.http ?? 2; + let defaults = HTTP_OPTIONS[http]; + + return { + minBundles: conf.contents.minBundles ?? defaults.minBundles, + minBundleSize: conf.contents.minBundleSize ?? defaults.minBundleSize, + maxParallelRequests: + conf.contents.maxParallelRequests ?? defaults.maxParallelRequests, + }; +} + +function ancestryUnion( + ancestors: Set, + assetRefs: Map, + bundleRoot: BundleRoot, +): Map | null> { + let map = new Map(); + for (let a of ancestors) { + map.set(a, null); + } + for (let [asset, refCount] of assetRefs) { + if (!ancestors.has(asset) && refCount > 1) { + map.set(asset, [bundleRoot]); + } + } + return map; +} + +function ancestryIntersect( + currentMap: Map | null>, + map: Map | null>, +): void { + for (let [bundleRoot, currentAssets] of currentMap) { + if (map.has(bundleRoot)) { + let assets = map.get(bundleRoot); + if (assets) { + if (currentAssets) { + currentAssets.push(...assets); + } else { + currentMap.set(bundleRoot, [...assets]); + } + } + } else { + currentMap.delete(bundleRoot); + } + } +} + +function getReachableBundleRoots(asset, graph): Array { + return graph + .getNodeIdsConnectedTo(graph.getNodeIdByContentKey(asset.id)) + .map(nodeId => nullthrows(graph.getNode(nodeId))); +} diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index 94dddb16617..537ecc0f78a 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -245,6 +245,59 @@ export default class BundleGraph { }); } + addAssetToBundle(asset: Asset, bundle: Bundle) { + let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id); + this._graph.addEdge( + bundleNodeId, + this._graph.getNodeIdByContentKey(asset.id), + bundleGraphEdgeTypes.contains, + ); + this._graph.addEdge( + bundleNodeId, + this._graph.getNodeIdByContentKey(asset.id), + ); + + let dependencies = this.getDependencies(asset); + for (let dependency of dependencies) { + let dependencyNodeId = this._graph.getNodeIdByContentKey(dependency.id); + this._graph.addEdge( + bundleNodeId, + dependencyNodeId, + bundleGraphEdgeTypes.contains, + ); + + for (let [bundleGroupNodeId, bundleGroupNode] of this._graph + .getNodeIdsConnectedFrom(dependencyNodeId) + .map(id => [id, nullthrows(this._graph.getNode(id))]) + .filter(([, node]) => node.type === 'bundle_group')) { + invariant(bundleGroupNode.type === 'bundle_group'); + this._graph.addEdge( + bundleNodeId, + bundleGroupNodeId, + bundleGraphEdgeTypes.bundle, + ); + } + // If the dependency references a target bundle, add a reference edge from + // the source bundle to the dependency for easy traversal. + // TODO: Consider bundle being created from dependency + if ( + this._graph + .getNodeIdsConnectedFrom( + dependencyNodeId, + bundleGraphEdgeTypes.references, + ) + .map(id => nullthrows(this._graph.getNode(id))) + .some(node => node.type === 'bundle') + ) { + this._graph.addEdge( + bundleNodeId, + dependencyNodeId, + bundleGraphEdgeTypes.references, + ); + } + } + } + addAssetGraphToBundle( asset: Asset, bundle: Bundle, @@ -303,12 +356,24 @@ export default class BundleGraph { nodeId, bundleGraphEdgeTypes.references, ); + this.markDependencyReferenceable(node.value); + //all bundles that have this dependency need to have an edge from bundle to that dependency } } }, assetNodeId); this._bundleContentHashes.delete(bundle.id); } + markDependencyReferenceable(dependency: Dependency) { + for (let bundle of this.getBundlesWithDependency(dependency)) { + this._graph.addEdge( + this._graph.getNodeIdByContentKey(bundle.id), + this._graph.getNodeIdByContentKey(dependency.id), + bundleGraphEdgeTypes.references, + ); + } + } + addEntryToBundle( asset: Asset, bundle: Bundle, @@ -325,6 +390,30 @@ export default class BundleGraph { throw new Error('Expected an async dependency'); } + // It's possible for internalized async dependencies to not have + // reference edges and still have untyped edges. + // TODO: Maybe don't use internalized async edges at all? + let dependencyNodeId = this._graph.getNodeIdByContentKey(dependency.id); + let resolved = this.getResolvedAsset(dependency); + if (resolved) { + let resolvedNodeId = this._graph.getNodeIdByContentKey(resolved.id); + + if ( + !this._graph.hasEdge( + dependencyNodeId, + resolvedNodeId, + bundleGraphEdgeTypes.references, + ) + ) { + this._graph.addEdge( + dependencyNodeId, + resolvedNodeId, + bundleGraphEdgeTypes.references, + ); + this._graph.removeEdge(dependencyNodeId, resolvedNodeId); + } + } + this._graph.addEdge( this._graph.getNodeIdByContentKey(bundle.id), this._graph.getNodeIdByContentKey(dependency.id), @@ -513,6 +602,7 @@ export default class BundleGraph { nodeId, bundleGraphEdgeTypes.references, ); + this.markDependencyReferenceable(node.value); } if ( this._graph.hasEdge( @@ -674,6 +764,7 @@ export default class BundleGraph { bundleId, bundleGraphEdgeTypes.references, ); + this.markDependencyReferenceable(dependency); if (this._graph.hasEdge(dependencyId, assetId)) { this._graph.removeEdge(dependencyId, assetId); } @@ -1028,6 +1119,7 @@ export default class BundleGraph { traverse( visit: GraphVisitor, + start?: Asset, ): ?TContext { return this._graph.filteredTraverse( nodeId => { @@ -1037,7 +1129,7 @@ export default class BundleGraph { } }, visit, - undefined, // start with root + start ? this._graph.getNodeIdByContentKey(start.id) : undefined, // start with root ALL_EDGE_TYPES, ); } diff --git a/packages/core/core/src/dumpGraphToGraphViz.js b/packages/core/core/src/dumpGraphToGraphViz.js index 65ec13025cd..8eada354afb 100644 --- a/packages/core/core/src/dumpGraphToGraphViz.js +++ b/packages/core/core/src/dumpGraphToGraphViz.js @@ -1,11 +1,13 @@ // @flow +import type {Asset, BundleBehavior} from '@parcel/types'; import type {Graph} from '@parcel/graph'; import type {AssetGraphNode, BundleGraphNode, Environment} from './types'; import {bundleGraphEdgeTypes} from './BundleGraph'; import {requestGraphEdgeTypes} from './RequestTracker'; import path from 'path'; +import {fromNodeId} from '@parcel/graph'; import {fromProjectPathRelative} from './projectPath'; import {SpecifierType, Priority} from './types'; @@ -32,7 +34,14 @@ const TYPE_COLORS = { export default async function dumpGraphToGraphViz( // $FlowFixMe - graph: Graph | Graph, + graph: + | Graph + | Graph<{| + assets: Array, + sourceBundles: Array, + bundleBehavior?: ?BundleBehavior, + |}> + | Graph, name: string, edgeTypes?: typeof bundleGraphEdgeTypes | typeof requestGraphEdgeTypes, ): Promise { @@ -55,38 +64,73 @@ export default async function dumpGraphToGraphViz( n.set('color', COLORS[node.type || 'default']); n.set('shape', 'box'); n.set('style', 'filled'); - let label = `${node.type || 'No Type'}: [${node.id}]: `; - if (node.type === 'dependency') { - label += node.value.specifier; - let parts = []; - if (node.value.priority !== Priority.sync) - parts.push(node.value.priority); - if (node.value.isOptional) parts.push('optional'); - if (node.value.specifierType === SpecifierType.url) parts.push('url'); - if (node.hasDeferred) parts.push('deferred'); - if (node.excluded) parts.push('excluded'); - if (parts.length) label += ' (' + parts.join(', ') + ')'; - if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; - let depSymbols = node.value.symbols; - if (detailedSymbols) { - if (depSymbols) { - if (depSymbols.size) { + let label; + if (typeof node === 'string') { + label = node; + } else if (node.assets) { + label = `(${nodeId(id)}), (assetIds: ${[...node.assets] + .map(a => { + let arr = a.filePath.split('/'); + return arr[arr.length - 1]; + }) + .join(', ')}) (sourceBundles: ${node.sourceBundles.join(', ')}) (bb ${ + node.bundleBehavior ?? 'none' + })`; + } else if (node.type) { + label = `[${fromNodeId(id)}] ${node.type || 'No Type'}: [${node.id}]: `; + if (node.type === 'dependency') { + label += node.value.specifier; + let parts = []; + if (node.value.priority !== Priority.sync) + parts.push(node.value.priority); + if (node.value.isOptional) parts.push('optional'); + if (node.value.specifierType === SpecifierType.url) parts.push('url'); + if (node.hasDeferred) parts.push('deferred'); + if (node.excluded) parts.push('excluded'); + if (parts.length) label += ' (' + parts.join(', ') + ')'; + if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; + let depSymbols = node.value.symbols; + if (detailedSymbols) { + if (depSymbols) { + if (depSymbols.size) { + label += + '\\nsymbols: ' + + [...depSymbols].map(([e, {local}]) => [e, local]).join(';'); + } + let weakSymbols = [...depSymbols] + .filter(([, {isWeak}]) => isWeak) + .map(([s]) => s); + if (weakSymbols.length) { + label += '\\nweakSymbols: ' + weakSymbols.join(','); + } + if (node.usedSymbolsUp.size > 0) { + label += '\\nusedSymbolsUp: ' + [...node.usedSymbolsUp].join(','); + } + if (node.usedSymbolsDown.size > 0) { + label += + '\\nusedSymbolsDown: ' + [...node.usedSymbolsDown].join(','); + } + } else { + label += '\\nsymbols: cleared'; + } + } + } else if (node.type === 'asset') { + label += + path.basename(fromProjectPathRelative(node.value.filePath)) + + '#' + + node.value.type; + if (detailedSymbols) { + if (!node.value.symbols) { + label += '\\nsymbols: cleared'; + } else if (node.value.symbols.size) { label += '\\nsymbols: ' + - [...depSymbols].map(([e, {local}]) => [e, local]).join(';'); - } - let weakSymbols = [...depSymbols] - .filter(([, {isWeak}]) => isWeak) - .map(([s]) => s); - if (weakSymbols.length) { - label += '\\nweakSymbols: ' + weakSymbols.join(','); + [...node.value.symbols] + .map(([e, {local}]) => [e, local]) + .join(';'); } - if (node.usedSymbolsUp.size > 0) { - label += '\\nusedSymbolsUp: ' + [...node.usedSymbolsUp].join(','); - } - if (node.usedSymbolsDown.size > 0) { - label += - '\\nusedSymbolsDown: ' + [...node.usedSymbolsDown].join(','); + if (node.usedSymbols.size) { + label += '\\nusedSymbols: ' + [...node.usedSymbols].join(','); } } else { label += '\\nsymbols: cleared'; @@ -108,27 +152,28 @@ export default async function dumpGraphToGraphViz( if (node.usedSymbols.size) { label += '\\nusedSymbols: ' + [...node.usedSymbols].join(','); } + } else if (node.type === 'asset_group') { + if (node.deferred) label += '(deferred)'; + // $FlowFixMe + } else if (node.type === 'file') { + label += path.basename(node.value.filePath); + // $FlowFixMe + } else if (node.type === 'transformer_request') { + label += + path.basename(node.value.filePath) + + ` (${getEnvDescription(node.value.env)})`; + // $FlowFixMe + } else if (node.type === 'bundle') { + let parts = []; + if (node.value.needsStableName) parts.push('stable name'); + if (node.value.name) parts.push('[name ', node.value.name, ']'); + parts.push('bb-', node.value.bundleBehavior); + if (parts.length) label += ' (' + parts.join(', ') + ')'; + if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; + // $FlowFixMe + } else if (node.type === 'request') { + label = node.value.type + ':' + node.id; } - } else if (node.type === 'asset_group') { - if (node.deferred) label += '(deferred)'; - // $FlowFixMe - } else if (node.type === 'file') { - label += path.basename(node.value.filePath); - // $FlowFixMe - } else if (node.type === 'transformer_request') { - label += - path.basename(node.value.filePath) + - ` (${getEnvDescription(node.value.env)})`; - // $FlowFixMe - } else if (node.type === 'bundle') { - let parts = []; - if (node.value.needsStableName) parts.push('stable name'); - if (node.value.bundleBehavior) parts.push(node.value.bundleBehavior); - if (parts.length) label += ' (' + parts.join(', ') + ')'; - if (node.value.env) label += ` (${getEnvDescription(node.value.env)})`; - // $FlowFixMe - } else if (node.type === 'request') { - label = node.value.type + ':' + node.id; } n.set('label', label); } diff --git a/packages/core/core/src/public/BundleGraph.js b/packages/core/core/src/public/BundleGraph.js index 47372f53c08..b42a7b68fed 100644 --- a/packages/core/core/src/public/BundleGraph.js +++ b/packages/core/core/src/public/BundleGraph.js @@ -255,6 +255,7 @@ export default class BundleGraph traverse( visit: GraphVisitor, + start?: ?IAsset, ): ?TContext { return this.#graph.traverse( mapVisitor( @@ -267,6 +268,7 @@ export default class BundleGraph }, visit, ), + start ? assetToAssetValue(start) : undefined, ); } diff --git a/packages/core/core/src/public/Environment.js b/packages/core/core/src/public/Environment.js index 7d465079eb1..16e27070aea 100644 --- a/packages/core/core/src/public/Environment.js +++ b/packages/core/core/src/public/Environment.js @@ -17,6 +17,8 @@ import browserslist from 'browserslist'; import semver from 'semver'; import {fromInternalSourceLocation} from '../utils'; +const inspect = Symbol.for('nodejs.util.inspect.custom'); + export const BROWSER_ENVS: Set = new Set([ 'browser', 'web-worker', @@ -191,6 +193,11 @@ export default class Environment implements IEnvironment { ); } + // $FlowFixMe[unsupported-syntax] + [inspect](): string { + return `Env(${this.#environment.context})`; + } + isBrowser(): boolean { return BROWSER_ENVS.has(this.#environment.context); } diff --git a/packages/core/core/src/public/MutableBundleGraph.js b/packages/core/core/src/public/MutableBundleGraph.js index 65c174fb8c5..f732d9409ec 100644 --- a/packages/core/core/src/public/MutableBundleGraph.js +++ b/packages/core/core/src/public/MutableBundleGraph.js @@ -41,6 +41,13 @@ export default class MutableBundleGraph this.#options = options; } + addAssetToBundle(asset: IAsset, bundle: IBundle) { + this.#graph.addAssetToBundle( + assetToAssetValue(asset), + bundleToInternalBundle(bundle), + ); + } + addAssetGraphToBundle( asset: IAsset, bundle: IBundle, diff --git a/packages/core/core/src/public/Target.js b/packages/core/core/src/public/Target.js index 18ab4e7c5e7..396b054a0f6 100644 --- a/packages/core/core/src/public/Target.js +++ b/packages/core/core/src/public/Target.js @@ -12,6 +12,8 @@ import Environment from './Environment'; import {fromProjectPath} from '../projectPath'; import {fromInternalSourceLocation} from '../utils'; +const inspect = Symbol.for('nodejs.util.inspect.custom'); + const internalTargetToTarget: WeakMap = new WeakMap(); const _targetToInternalTarget: WeakMap = new WeakMap(); export function targetToInternalTarget(target: ITarget): TargetValue { @@ -61,4 +63,9 @@ export default class Target implements ITarget { this.#target.loc, ); } + + // $FlowFixMe[unsupported-syntax] + [inspect](): string { + return `Target(${this.name} - ${this.env[inspect]()})`; + } } diff --git a/packages/core/graph/src/ContentGraph.js b/packages/core/graph/src/ContentGraph.js index 3b8d386fbf7..f9b3ce33535 100644 --- a/packages/core/graph/src/ContentGraph.js +++ b/packages/core/graph/src/ContentGraph.js @@ -62,6 +62,12 @@ export default class ContentGraph extends Graph< return nodeId; } + addNodeByContentKeyIfNeeded(contentKey: ContentKey, node: TNode): NodeId { + return this.hasContentKey(contentKey) + ? this.getNodeIdByContentKey(contentKey) + : this.addNodeByContentKey(contentKey, node); + } + getNodeByContentKey(contentKey: ContentKey): ?TNode { let nodeId = this._contentKeyToNodeId.get(contentKey); if (nodeId != null) { diff --git a/packages/core/graph/src/Graph.js b/packages/core/graph/src/Graph.js index 77f15341165..58aec325b92 100644 --- a/packages/core/graph/src/Graph.js +++ b/packages/core/graph/src/Graph.js @@ -403,6 +403,16 @@ export default class Graph { return null; } + topoSort(): Array { + let sorted: Array = []; + this.traverse({ + exit: nodeId => { + sorted.push(nodeId); + }, + }); + return sorted.reverse(); + } + findAncestor(nodeId: NodeId, fn: (nodeId: NodeId) => boolean): ?NodeId { let res = null; this.traverseAncestors(nodeId, (nodeId, ctx, traversal) => { diff --git a/packages/core/graph/test/Graph.test.js b/packages/core/graph/test/Graph.test.js index fb0849966da..a25323b4ef1 100644 --- a/packages/core/graph/test/Graph.test.js +++ b/packages/core/graph/test/Graph.test.js @@ -83,11 +83,14 @@ describe('Graph', () => { it('isOrphanedNode should return true or false if the node is orphaned or not', () => { let graph = new Graph(); + let rootNode = graph.addNode('root'); + graph.setRootNodeId(rootNode); + let nodeA = graph.addNode('a'); let nodeB = graph.addNode('b'); let nodeC = graph.addNode('c'); - graph.addEdge(nodeA, nodeB); - graph.addEdge(nodeA, nodeC, 1); + graph.addEdge(rootNode, nodeB); + graph.addEdge(nodeB, nodeC, 1); assert(graph.isOrphanedNode(nodeA)); assert(!graph.isOrphanedNode(nodeB)); assert(!graph.isOrphanedNode(nodeC)); @@ -101,6 +104,7 @@ describe('Graph', () => { // c let graph = new Graph(); let nodeA = graph.addNode('a'); + graph.setRootNodeId(nodeA); let nodeB = graph.addNode('b'); let nodeC = graph.addNode('c'); let nodeD = graph.addNode('d'); @@ -140,6 +144,7 @@ describe('Graph', () => { let graph = new Graph(); let nodeA = graph.addNode('a'); + graph.setRootNodeId(nodeA); let nodeB = graph.addNode('b'); let nodeC = graph.addNode('c'); let nodeD = graph.addNode('d'); @@ -268,6 +273,7 @@ describe('Graph', () => { it("replaceNodeIdsConnectedTo should update a node's downstream nodes", () => { let graph = new Graph(); let nodeA = graph.addNode('a'); + graph.setRootNodeId(nodeA); let nodeB = graph.addNode('b'); let nodeC = graph.addNode('c'); graph.addEdge(nodeA, nodeB); diff --git a/packages/core/integration-tests/test/sourcemaps.js b/packages/core/integration-tests/test/sourcemaps.js index e35e4f7272e..61816b3bb75 100644 --- a/packages/core/integration-tests/test/sourcemaps.js +++ b/packages/core/integration-tests/test/sourcemaps.js @@ -1365,8 +1365,8 @@ describe('sourcemaps', function () { let sourceMap = new SourceMap('/'); sourceMap.addVLQMap(map); - let sourceContent = map.sourcesContent[0]; let sourcePath = 'index.js'; + let sourceContent = sourceMap.getSourceContent(sourcePath); checkSourceMapping({ map: sourceMap, @@ -1410,8 +1410,8 @@ describe('sourcemaps', function () { let sourceMap = new SourceMap('/'); sourceMap.addVLQMap(map); - let sourceContent = map.sourcesContent[0]; let sourcePath = 'index.tsx'; + let sourceContent = sourceMap.getSourceContent(sourcePath); checkSourceMapping({ map: sourceMap, diff --git a/packages/core/test-utils/src/.parcelrc-experimental-bundler b/packages/core/test-utils/src/.parcelrc-experimental-bundler new file mode 100644 index 00000000000..219efa52e96 --- /dev/null +++ b/packages/core/test-utils/src/.parcelrc-experimental-bundler @@ -0,0 +1,5 @@ +{ + "extends": "@parcel/config-default", + "bundler": "@parcel/bundler-experimental", + "reporters": [] +} diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 47e1fcca383..e0d88d14adb 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -111,7 +111,12 @@ export function getParcelOptions( entries, shouldDisableCache: true, logLevel: 'none', - defaultConfig: path.join(__dirname, '.parcelrc-no-reporters'), + defaultConfig: path.join( + __dirname, + process.env.PARCEL_TEST_EXPERIMENTAL_BUNDLER == null + ? '.parcelrc-no-reporters' + : '.parcelrc-experimental-bundler', + ), inputFS, outputFS, workerFarm, diff --git a/packages/core/types/index.js b/packages/core/types/index.js index a30e6fca871..36e6af9fd42 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -1296,6 +1296,7 @@ export interface MutableBundleGraph extends BundleGraph { Bundle, shouldSkipDependency?: (Dependency) => boolean, ): void; + addAssetToBundle(Asset, Bundle): void; addEntryToBundle( Asset, Bundle, @@ -1329,7 +1330,10 @@ export interface BundleGraph { /** Returns a list of bundles in the bundle graph. By default, inline bundles are excluded. */ getBundles(opts?: {|includeInline: boolean|}): Array; /** Traverses the assets and dependencies in the bundle graph, in depth first order. */ - traverse(GraphVisitor): ?TContext; + traverse( + visit: GraphVisitor, + startAsset: ?Asset, + ): ?TContext; /** Traverses all bundles in the bundle graph, including inline bundles, in depth first order. */ traverseBundles( visit: GraphVisitor, diff --git a/packages/core/utils/src/collection.js b/packages/core/utils/src/collection.js index 077c1034980..8c62599f869 100644 --- a/packages/core/utils/src/collection.js +++ b/packages/core/utils/src/collection.js @@ -43,3 +43,15 @@ export function setDifference(a: Set, b: Set): Set { } return difference; } + +export function setIntersect(a: Set, b: Set): void { + for (let entry of a) { + if (!b.has(entry)) { + a.delete(entry); + } + } +} + +export function setUnion(a: Iterable, b: Iterable): Set { + return new Set([...a, ...b]); +} diff --git a/packages/core/utils/src/index.js b/packages/core/utils/src/index.js index 4b6b20f24d8..c5aa3849bfc 100644 --- a/packages/core/utils/src/index.js +++ b/packages/core/utils/src/index.js @@ -35,6 +35,8 @@ export { objectSortedEntries, objectSortedEntriesDeep, setDifference, + setIntersect, + setUnion, } from './collection'; export {resolveConfig, resolveConfigSync, loadConfig} from './config'; export {DefaultMap, DefaultWeakMap} from './DefaultMap'; diff --git a/packages/core/utils/src/replaceBundleReferences.js b/packages/core/utils/src/replaceBundleReferences.js index 573391fc10c..4e34e6ca99e 100644 --- a/packages/core/utils/src/replaceBundleReferences.js +++ b/packages/core/utils/src/replaceBundleReferences.js @@ -66,7 +66,7 @@ export function replaceURLReferences({ continue; } - if (!resolved || resolved.bundleBehavior === 'inline') { + if (resolved.bundleBehavior === 'inline') { // If a bundle is inline, it should be replaced with inline contents, // not a URL. continue; diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index 120b35efc08..87ed3952bd1 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -109,7 +109,7 @@ export class ScopeHoistingPackager { } async package(): Promise<{|contents: string, map: ?SourceMap|}> { - await this.loadAssets(); + let wrappedAssets = await this.loadAssets(); this.buildExportedSymbols(); // If building a library, the target is actually another bundler rather @@ -127,17 +127,10 @@ export class ScopeHoistingPackager { } } - // Add each asset that is directly connected to the bundle. Dependencies will be handled - // by replacing `import` statements in the code. let res = ''; let lineCount = 0; let sourceMap = null; - this.bundle.traverseAssets((asset, _, actions) => { - if (this.seenAssets.has(asset.id)) { - actions.skipChildren(); - return; - } - + let processAsset = asset => { let [content, map, lines] = this.visitAsset(asset); if (sourceMap && map) { sourceMap.addSourceMap(map, lineCount); @@ -147,6 +140,25 @@ export class ScopeHoistingPackager { res += content + '\n'; lineCount += lines + 1; + }; + + // Hoist wrapped asset to the top of the bundle to ensure that they are registered + // before they are used. + for (let asset of wrappedAssets) { + if (!this.seenAssets.has(asset.id)) { + processAsset(asset); + } + } + + // Add each asset that is directly connected to the bundle. Dependencies will be handled + // by replacing `import` statements in the code. + this.bundle.traverseAssets((asset, _, actions) => { + if (this.seenAssets.has(asset.id)) { + actions.skipChildren(); + return; + } + + processAsset(asset); actions.skipChildren(); }); @@ -226,8 +238,9 @@ export class ScopeHoistingPackager { }; } - async loadAssets() { + async loadAssets(): Promise> { let queue = new PromiseQueue({maxConcurrent: 32}); + let wrapped = []; this.bundle.traverseAssets((asset, shouldWrap) => { queue.add(async () => { let [code, map] = await Promise.all([ @@ -248,11 +261,13 @@ export class ScopeHoistingPackager { .some(dep => dep.meta.shouldWrap && dep.specifierType !== 'url') ) { this.wrappedAssets.add(asset.id); + wrapped.push(asset); return true; } }); this.assetOutputs = new Map(await queue.run()); + return wrapped; } buildExportedSymbols() {