@parcel/core
Version:
1,517 lines (1,370 loc) • 68.1 kB
JavaScript
// @flow strict-local
import type {
GraphVisitor,
FilePath,
Symbol,
TraversalActions,
BundleBehavior as IBundleBehavior,
} from '@parcel/types';
import type {
ContentKey,
ContentGraphOpts,
NodeId,
SerializedContentGraph,
} from '@parcel/graph';
import type {
Asset,
AssetNode,
Bundle,
BundleGraphNode,
BundleGroup,
BundleNode,
Dependency,
DependencyNode,
Environment,
InternalSourceLocation,
Target,
} from './types';
import type AssetGraph from './AssetGraph';
import type {ProjectPath} from './projectPath';
import assert from 'assert';
import invariant from 'assert';
import nullthrows from 'nullthrows';
import {ContentGraph, ALL_EDGE_TYPES, mapVisitor} from '@parcel/graph';
import {Hash, hashString} from '@parcel/hash';
import {DefaultMap, objectSortedEntriesDeep, getRootDir} from '@parcel/utils';
import {Priority, BundleBehavior, SpecifierType} from './types';
import {getBundleGroupId, getPublicId} from './utils';
import {ISOLATED_ENVS} from './public/Environment';
import {fromProjectPath, fromProjectPathRelative} from './projectPath';
import {HASH_REF_PREFIX} from './constants';
export const bundleGraphEdgeTypes = {
// A lack of an edge type indicates to follow the edge while traversing
// the bundle's contents, e.g. `bundle.traverse()` during packaging.
null: 1,
// Used for constant-time checks of presence of a dependency or asset in a bundle,
// avoiding bundle traversal in cases like `isAssetInAncestors`
contains: 2,
// Connections between bundles and bundle groups, for quick traversal of the
// bundle hierarchy.
bundle: 3,
// When dependency -> asset: Indicates that the asset a dependency references
// is contained in another bundle.
// When dependency -> bundle: Indicates the bundle is necessary for any bundles
// with the dependency.
// When bundle -> bundle: Indicates the target bundle is necessary for the
// source bundle.
// This type prevents referenced assets from being traversed from dependencies
// along the untyped edge, and enables traversal to referenced bundles that are
// not directly connected to bundle group nodes.
references: 4,
// Signals that the dependency is internally resolvable via the bundle's ancestry,
// and that the bundle connected to the dependency is not necessary for the source bundle.
internal_async: 5,
};
export type BundleGraphEdgeType = $Values<typeof bundleGraphEdgeTypes>;
type InternalSymbolResolution = {|
asset: Asset,
exportSymbol: string,
symbol: ?Symbol | false,
loc: ?InternalSourceLocation,
|};
type InternalExportSymbolResolution = {|
...InternalSymbolResolution,
+exportAs: Symbol | string,
|};
type BundleGraphOpts = {|
graph: ContentGraphOpts<BundleGraphNode, BundleGraphEdgeType>,
bundleContentHashes: Map<string, string>,
assetPublicIds: Set<string>,
publicIdByAssetId: Map<string, string>,
|};
type SerializedBundleGraph = {|
$$raw: true,
graph: SerializedContentGraph<BundleGraphNode, BundleGraphEdgeType>,
bundleContentHashes: Map<string, string>,
assetPublicIds: Set<string>,
publicIdByAssetId: Map<string, string>,
|};
function makeReadOnlySet<T>(set: Set<T>): $ReadOnlySet<T> {
return new Proxy(set, {
get(target, property) {
if (property === 'delete' || property === 'add' || property === 'clear') {
return undefined;
} else {
// $FlowFixMe[incompatible-type]
let value = target[property];
return typeof value === 'function' ? value.bind(target) : value;
}
},
});
}
/**
* Stores assets, dependencies, bundle groups, bundles, and the relationships between them.
* The BundleGraph is passed to the bundler plugin wrapped in a MutableBundleGraph,
* and is passed to packagers and optimizers wrapped in the public BundleGraph object, both
* of which implement public api for this structure. This is the internal structure.
*/
export default class BundleGraph {
/** A set of all existing concise asset ids present in the BundleGraph */
_assetPublicIds: Set<string>;
/** Maps full asset ids (currently 32-character strings) to concise ids (minimum of 5 character strings) */
_publicIdByAssetId: Map<string, string>;
/**
* A cache of bundle hashes by bundle id.
*
* TODO: These hashes are being invalidated in mutative methods, but this._graph is not a private
* property so it is possible to reach in and mutate the graph without invalidating these hashes.
* It needs to be exposed in BundlerRunner for now based on how applying runtimes works and the
* BundlerRunner takes care of invalidating hashes when runtimes are applied, but this is not ideal.
*/
_bundleContentHashes: Map<string, string>;
_targetEntryRoots: Map<ProjectPath, FilePath> = new Map();
/** The internal core Graph structure */
_graph: ContentGraph<BundleGraphNode, BundleGraphEdgeType>;
_bundlePublicIds /*: Set<string> */ = new Set<string>();
constructor({
graph,
publicIdByAssetId,
assetPublicIds,
bundleContentHashes,
}: {|
graph: ContentGraph<BundleGraphNode, BundleGraphEdgeType>,
publicIdByAssetId: Map<string, string>,
assetPublicIds: Set<string>,
bundleContentHashes: Map<string, string>,
|}) {
this._graph = graph;
this._assetPublicIds = assetPublicIds;
this._publicIdByAssetId = publicIdByAssetId;
this._bundleContentHashes = bundleContentHashes;
}
/**
* Produce a BundleGraph from an AssetGraph by removing asset groups and retargeting dependencies
* based on the symbol data (resolving side-effect free reexports).
*/
static fromAssetGraph(
assetGraph: AssetGraph,
isProduction: boolean,
publicIdByAssetId: Map<string, string> = new Map(),
assetPublicIds: Set<string> = new Set(),
): BundleGraph {
let graph = new ContentGraph<BundleGraphNode, BundleGraphEdgeType>();
let assetGroupIds = new Map();
let dependencies = new Map();
let assetGraphNodeIdToBundleGraphNodeId = new Map<NodeId, NodeId>();
let assetGraphRootNode =
assetGraph.rootNodeId != null
? assetGraph.getNode(assetGraph.rootNodeId)
: null;
invariant(assetGraphRootNode != null && assetGraphRootNode.type === 'root');
for (let [nodeId, node] of assetGraph.nodes) {
if (node.type === 'asset') {
let {id: assetId} = node.value;
// Generate a new, short public id for this asset to use.
// If one already exists, use it.
let publicId = publicIdByAssetId.get(assetId);
if (publicId == null) {
publicId = getPublicId(assetId, existing =>
assetPublicIds.has(existing),
);
publicIdByAssetId.set(assetId, publicId);
assetPublicIds.add(publicId);
}
} else if (node.type === 'asset_group') {
assetGroupIds.set(nodeId, assetGraph.getNodeIdsConnectedFrom(nodeId));
}
}
let walkVisited = new Set();
function walk(nodeId) {
if (walkVisited.has(nodeId)) return;
walkVisited.add(nodeId);
let node = nullthrows(assetGraph.getNode(nodeId));
if (
node.type === 'dependency' &&
node.value.symbols != null &&
node.value.env.shouldScopeHoist &&
// Disable in dev mode because this feature is at odds with safeToIncrementallyBundle
isProduction
) {
// asset -> symbols that should be imported directly from that asset
let targets = new DefaultMap<ContentKey, Map<Symbol, Symbol>>(
() => new Map(),
);
let externalSymbols = new Set();
let hasAmbiguousSymbols = false;
for (let [symbol, resolvedSymbol] of node.usedSymbolsUp) {
if (resolvedSymbol) {
targets
.get(resolvedSymbol.asset)
.set(symbol, resolvedSymbol.symbol ?? symbol);
} else if (resolvedSymbol === null) {
externalSymbols.add(symbol);
} else if (resolvedSymbol === undefined) {
hasAmbiguousSymbols = true;
break;
}
}
if (
// Only perform retargeting when there is an imported symbol
// - If the target is side-effect-free, the symbols point to the actual target and removing
// the original dependency resolution is fine
// - Otherwise, keep this dependency unchanged for its potential side effects
node.usedSymbolsUp.size > 0 &&
// Only perform retargeting if the dependency only points to a single asset (e.g. CSS modules)
!hasAmbiguousSymbols &&
// It doesn't make sense to retarget dependencies where `*` is used, because the
// retargeting won't enable any benefits in that case (apart from potentially even more
// code being generated).
!node.usedSymbolsUp.has('*') &&
// TODO We currently can't rename imports in async imports, e.g. from
// (parcelRequire("...")).then(({ a }) => a);
// to
// (parcelRequire("...")).then(({ a: b }) => a);
// or
// (parcelRequire("...")).then((a)=>a);
// if the reexporting asset did `export {a as b}` or `export * as a`
node.value.priority === Priority.sync &&
// For every asset, no symbol is imported multiple times (with a different local name).
// Don't retarget because this cannot be resolved without also changing the asset symbols
// (and the asset content itself).
[...targets].every(
([, t]) => new Set([...t.values()]).size === t.size,
)
) {
// TODO adjust sourceAssetIdNode.value.dependencies ?
let deps = [
// Keep the original dependency
{
asset: null,
dep: graph.addNodeByContentKey(node.id, {
...node,
value: {
...node.value,
symbols: node.value.symbols
? new Map(
[...node.value.symbols].filter(([k]) =>
externalSymbols.has(k),
),
)
: undefined,
},
usedSymbolsUp: new Map(
[...node.usedSymbolsUp].filter(([k]) =>
externalSymbols.has(k),
),
),
usedSymbolsDown: new Set(),
excluded: externalSymbols.size === 0,
}),
},
...[...targets].map(([asset, target]) => {
let newNodeId = hashString(
node.id + [...target.keys()].join(','),
);
return {
asset,
dep: graph.addNodeByContentKey(newNodeId, {
...node,
id: newNodeId,
value: {
...node.value,
id: newNodeId,
symbols: node.value.symbols
? new Map(
[...node.value.symbols]
.filter(([k]) => target.has(k) || k === '*')
.map(([k, v]) => [target.get(k) ?? k, v]),
)
: undefined,
},
usedSymbolsUp: new Map(
[...node.usedSymbolsUp]
.filter(([k]) => target.has(k) || k === '*')
.map(([k, v]) => [target.get(k) ?? k, v]),
),
usedSymbolsDown: new Set(),
}),
};
}),
];
dependencies.set(nodeId, deps);
// Jump to the dependencies that are used in this dependency
for (let id of targets.keys()) {
walk(assetGraph.getNodeIdByContentKey(id));
}
return;
} else {
// No special handling
let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node);
assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId);
}
}
// Don't copy over asset groups into the bundle graph.
else if (node.type !== 'asset_group') {
let bundleGraphNodeId = graph.addNodeByContentKey(node.id, node);
if (node.id === assetGraphRootNode?.id) {
graph.setRootNodeId(bundleGraphNodeId);
}
assetGraphNodeIdToBundleGraphNodeId.set(nodeId, bundleGraphNodeId);
}
for (let id of assetGraph.getNodeIdsConnectedFrom(nodeId)) {
walk(id);
}
}
walk(nullthrows(assetGraph.rootNodeId));
for (let edge of assetGraph.getAllEdges()) {
if (assetGroupIds.has(edge.from)) {
continue;
}
if (dependencies.has(edge.from)) {
// Discard previous edge, insert outgoing edges for all split dependencies
for (let {asset, dep} of nullthrows(dependencies.get(edge.from))) {
if (asset != null) {
graph.addEdge(
dep,
nullthrows(
assetGraphNodeIdToBundleGraphNodeId.get(
assetGraph.getNodeIdByContentKey(asset),
),
),
);
}
}
continue;
}
if (!assetGraphNodeIdToBundleGraphNodeId.has(edge.from)) {
continue;
}
let to: Array<NodeId> = dependencies.get(edge.to)?.map(v => v.dep) ??
assetGroupIds
.get(edge.to)
?.map(id =>
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(id)),
) ?? [nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.to))];
for (let t of to) {
graph.addEdge(
nullthrows(assetGraphNodeIdToBundleGraphNodeId.get(edge.from)),
t,
);
}
}
return new BundleGraph({
graph,
assetPublicIds,
bundleContentHashes: new Map(),
publicIdByAssetId,
});
}
serialize(): SerializedBundleGraph {
return {
$$raw: true,
graph: this._graph.serialize(),
assetPublicIds: this._assetPublicIds,
bundleContentHashes: this._bundleContentHashes,
publicIdByAssetId: this._publicIdByAssetId,
};
}
static deserialize(serialized: BundleGraphOpts): BundleGraph {
return new BundleGraph({
graph: ContentGraph.deserialize(serialized.graph),
assetPublicIds: serialized.assetPublicIds,
bundleContentHashes: serialized.bundleContentHashes,
publicIdByAssetId: serialized.publicIdByAssetId,
});
}
createBundle(
opts:
| {|
+entryAsset: Asset,
+target: Target,
+needsStableName?: ?boolean,
+bundleBehavior?: ?IBundleBehavior,
+shouldContentHash: boolean,
+env: Environment,
|}
| {|
+type: string,
+env: Environment,
+uniqueKey: string,
+target: Target,
+needsStableName?: ?boolean,
+bundleBehavior?: ?IBundleBehavior,
+isSplittable?: ?boolean,
+pipeline?: ?string,
+shouldContentHash: boolean,
|},
): Bundle {
let {entryAsset, target} = opts;
let bundleId = hashString(
'bundle:' +
(opts.entryAsset ? opts.entryAsset.id : opts.uniqueKey) +
fromProjectPathRelative(target.distDir) +
(opts.bundleBehavior ?? ''),
);
let existing = this._graph.getNodeByContentKey(bundleId);
if (existing != null) {
invariant(existing.type === 'bundle');
return existing.value;
}
let publicId = getPublicId(bundleId, existing =>
this._bundlePublicIds.has(existing),
);
this._bundlePublicIds.add(publicId);
let isPlaceholder = false;
if (entryAsset) {
let entryAssetNode = this._graph.getNodeByContentKey(entryAsset.id);
invariant(entryAssetNode?.type === 'asset', 'Entry asset does not exist');
isPlaceholder = entryAssetNode.requested === false;
}
let bundleNode: BundleNode = {
type: 'bundle',
id: bundleId,
value: {
id: bundleId,
hashReference: opts.shouldContentHash
? HASH_REF_PREFIX + bundleId
: bundleId.slice(-8),
type: opts.entryAsset ? opts.entryAsset.type : opts.type,
env: opts.env,
entryAssetIds: entryAsset ? [entryAsset.id] : [],
mainEntryId: entryAsset?.id,
pipeline: opts.entryAsset ? opts.entryAsset.pipeline : opts.pipeline,
needsStableName: opts.needsStableName,
bundleBehavior:
opts.bundleBehavior != null
? BundleBehavior[opts.bundleBehavior]
: null,
isSplittable: opts.entryAsset
? opts.entryAsset.isBundleSplittable
: opts.isSplittable,
isPlaceholder,
target,
name: null,
displayName: null,
publicId,
},
};
let bundleNodeId = this._graph.addNodeByContentKey(bundleId, bundleNode);
if (opts.entryAsset) {
this._graph.addEdge(
bundleNodeId,
this._graph.getNodeIdByContentKey(opts.entryAsset.id),
);
}
invariant;
return bundleNode.value;
}
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,
shouldSkipDependency: Dependency => boolean = d =>
this.isDependencySkipped(d),
) {
let assetNodeId = this._graph.getNodeIdByContentKey(asset.id);
let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id);
// The root asset should be reached directly from the bundle in traversal.
// Its children will be traversed from there.
this._graph.addEdge(bundleNodeId, assetNodeId);
this._graph.traverse((nodeId, _, actions) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'bundle_group') {
actions.skipChildren();
return;
}
if (node.type === 'dependency' && shouldSkipDependency(node.value)) {
actions.skipChildren();
return;
}
if (node.type === 'asset' || node.type === 'dependency') {
this._graph.addEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.contains,
);
}
if (node.type === 'dependency') {
for (let [bundleGroupNodeId, bundleGroupNode] of this._graph
.getNodeIdsConnectedFrom(nodeId)
.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.
if (
this._graph
.getNodeIdsConnectedFrom(nodeId, bundleGraphEdgeTypes.references)
.map(id => nullthrows(this._graph.getNode(id)))
.some(node => node.type === 'bundle')
) {
this._graph.addEdge(
bundleNodeId,
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,
shouldSkipDependency?: Dependency => boolean,
) {
this.addAssetGraphToBundle(asset, bundle, shouldSkipDependency);
if (!bundle.entryAssetIds.includes(asset.id)) {
bundle.entryAssetIds.push(asset.id);
}
}
internalizeAsyncDependency(bundle: Bundle, dependency: Dependency) {
if (dependency.priority === Priority.sync) {
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),
bundleGraphEdgeTypes.internal_async,
);
this._removeExternalDependency(bundle, dependency);
}
isDependencySkipped(dependency: Dependency): boolean {
let node = this._graph.getNodeByContentKey(dependency.id);
invariant(node && node.type === 'dependency');
return !!node.hasDeferred || node.excluded;
}
getParentBundlesOfBundleGroup(bundleGroup: BundleGroup): Array<Bundle> {
return this._graph
.getNodeIdsConnectedTo(
this._graph.getNodeIdByContentKey(getBundleGroupId(bundleGroup)),
bundleGraphEdgeTypes.bundle,
)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle')
.map(node => {
invariant(node.type === 'bundle');
return node.value;
});
}
resolveAsyncDependency(
dependency: Dependency,
bundle: ?Bundle,
): ?(
| {|type: 'bundle_group', value: BundleGroup|}
| {|type: 'asset', value: Asset|}
) {
let depNodeId = this._graph.getNodeIdByContentKey(dependency.id);
let bundleNodeId =
bundle != null ? this._graph.getNodeIdByContentKey(bundle.id) : null;
if (
bundleNodeId != null &&
this._graph.hasEdge(
bundleNodeId,
depNodeId,
bundleGraphEdgeTypes.internal_async,
)
) {
let referencedAssetNodeIds = this._graph.getNodeIdsConnectedFrom(
depNodeId,
bundleGraphEdgeTypes.references,
);
let resolved;
if (referencedAssetNodeIds.length === 0) {
resolved = this.getResolvedAsset(dependency, bundle);
} else if (referencedAssetNodeIds.length === 1) {
let referencedAssetNode = this._graph.getNode(
referencedAssetNodeIds[0],
);
// If a referenced asset already exists, resolve this dependency to it.
invariant(referencedAssetNode?.type === 'asset');
resolved = referencedAssetNode.value;
} else {
throw new Error('Dependencies can only reference one asset');
}
if (resolved == null) {
return;
} else {
return {
type: 'asset',
value: resolved,
};
}
}
let node = this._graph
.getNodeIdsConnectedFrom(this._graph.getNodeIdByContentKey(dependency.id))
.map(id => nullthrows(this._graph.getNode(id)))
.find(node => node.type === 'bundle_group');
if (node == null) {
return;
}
invariant(node.type === 'bundle_group');
return {
type: 'bundle_group',
value: node.value,
};
}
// eslint-disable-next-line no-unused-vars
getReferencedBundle(dependency: Dependency, fromBundle: Bundle): ?Bundle {
let dependencyNodeId = this._graph.getNodeIdByContentKey(dependency.id);
// If this dependency is async, there will be a bundle group attached to it.
let node = this._graph
.getNodeIdsConnectedFrom(dependencyNodeId)
.map(id => nullthrows(this._graph.getNode(id)))
.find(node => node.type === 'bundle_group');
if (node != null) {
invariant(node.type === 'bundle_group');
return this.getBundlesInBundleGroup(node.value, {
includeInline: true,
}).find(b => {
let mainEntryId = b.entryAssetIds[b.entryAssetIds.length - 1];
return mainEntryId != null && node.value.entryAssetId === mainEntryId;
});
}
// Otherwise, find an attached bundle via a reference edge (e.g. from createAssetReference).
let bundleNode = this._graph
.getNodeIdsConnectedFrom(
dependencyNodeId,
bundleGraphEdgeTypes.references,
)
.map(id => nullthrows(this._graph.getNode(id)))
.find(node => node.type === 'bundle');
if (bundleNode) {
invariant(bundleNode.type === 'bundle');
return bundleNode.value;
}
}
removeAssetGraphFromBundle(asset: Asset, bundle: Bundle) {
let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id);
let assetNodeId = this._graph.getNodeIdByContentKey(asset.id);
// Remove all contains edges from the bundle to the nodes in the asset's
// subgraph.
this._graph.traverse((nodeId, context, actions) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'bundle_group') {
actions.skipChildren();
return;
}
if (node.type !== 'dependency' && node.type !== 'asset') {
return;
}
if (
this._graph.hasEdge(bundleNodeId, nodeId, bundleGraphEdgeTypes.contains)
) {
this._graph.removeEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.contains,
// Removing this contains edge should not orphan the connected node. This
// is disabled for performance reasons as these edges are removed as part
// of a traversal, and checking for orphans becomes quite expensive in
// aggregate.
false /* removeOrphans */,
);
} else {
actions.skipChildren();
}
if (node.type === 'asset' && this._graph.hasEdge(bundleNodeId, nodeId)) {
// Remove the untyped edge from the bundle to the node (it's an entry)
this._graph.removeEdge(bundleNodeId, nodeId);
let entryIndex = bundle.entryAssetIds.indexOf(node.value.id);
if (entryIndex >= 0) {
// Shared bundles have untyped edges to their asset graphs but don't
// have entry assets. For those that have entry asset ids, remove them.
bundle.entryAssetIds.splice(entryIndex, 1);
}
}
if (node.type === 'dependency') {
this._removeExternalDependency(bundle, node.value);
if (
this._graph.hasEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.references,
)
) {
this._graph.addEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.references,
);
this.markDependencyReferenceable(node.value);
}
if (
this._graph.hasEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.internal_async,
)
) {
this._graph.removeEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.internal_async,
);
}
}
}, assetNodeId);
// Remove bundle node if it no longer has any entry assets
if (this._graph.getNodeIdsConnectedFrom(bundleNodeId).length === 0) {
this.removeBundle(bundle);
}
this._bundleContentHashes.delete(bundle.id);
}
/**
* Remove a bundle from the bundle graph. Remove its bundle group if it is
* the only bundle in the group.
*/
removeBundle(bundle: Bundle): Set<BundleGroup> {
// Remove bundle node if it no longer has any entry assets
let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id);
let bundleGroupNodeIds = this._graph.getNodeIdsConnectedTo(
bundleNodeId,
bundleGraphEdgeTypes.bundle,
);
this._graph.removeNode(bundleNodeId);
let removedBundleGroups: Set<BundleGroup> = new Set();
// Remove bundle group node if it no longer has any bundles
for (let bundleGroupNodeId of bundleGroupNodeIds) {
let bundleGroupNode = nullthrows(this._graph.getNode(bundleGroupNodeId));
invariant(bundleGroupNode.type === 'bundle_group');
let bundleGroup = bundleGroupNode.value;
if (
// If the bundle group's entry asset belongs to this bundle, the group
// was created because of this bundle. Remove the group.
bundle.entryAssetIds.includes(bundleGroup.entryAssetId) ||
// If the bundle group is now empty, remove it.
this.getBundlesInBundleGroup(bundleGroup, {includeInline: true})
.length === 0
) {
removedBundleGroups.add(bundleGroup);
this.removeBundleGroup(bundleGroup);
}
}
this._bundleContentHashes.delete(bundle.id);
return removedBundleGroups;
}
removeBundleGroup(bundleGroup: BundleGroup) {
let bundleGroupNode = nullthrows(
this._graph.getNodeByContentKey(getBundleGroupId(bundleGroup)),
);
invariant(bundleGroupNode.type === 'bundle_group');
let bundlesInGroup = this.getBundlesInBundleGroup(bundleGroupNode.value, {
includeInline: true,
});
for (let bundle of bundlesInGroup) {
if (this.getBundleGroupsContainingBundle(bundle).length === 1) {
let removedBundleGroups = this.removeBundle(bundle);
if (removedBundleGroups.has(bundleGroup)) {
// This function can be reentered through removeBundle above. In the case this
// bundle group has already been removed, stop.
return;
}
}
}
// This function can be reentered through removeBundle above. In this case,
// the node may already been removed.
if (this._graph.hasContentKey(bundleGroupNode.id)) {
this._graph.removeNode(
this._graph.getNodeIdByContentKey(bundleGroupNode.id),
);
}
assert(
bundlesInGroup.every(
bundle => this.getBundleGroupsContainingBundle(bundle).length > 0,
),
);
}
_removeExternalDependency(bundle: Bundle, dependency: Dependency) {
let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id);
for (let bundleGroupNode of this._graph
.getNodeIdsConnectedFrom(this._graph.getNodeIdByContentKey(dependency.id))
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle_group')) {
let bundleGroupNodeId = this._graph.getNodeIdByContentKey(
bundleGroupNode.id,
);
if (
!this._graph.hasEdge(
bundleNodeId,
bundleGroupNodeId,
bundleGraphEdgeTypes.bundle,
)
) {
continue;
}
let inboundDependencies = this._graph
.getNodeIdsConnectedTo(bundleGroupNodeId)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'dependency')
.map(node => {
invariant(node.type === 'dependency');
return node.value;
});
// If every inbound dependency to this bundle group does not belong to this bundle,
// or the dependency is internal to the bundle, then the connection between
// this bundle and the group is safe to remove.
if (
inboundDependencies.every(
dependency =>
dependency.specifierType !== SpecifierType.url &&
(!this.bundleHasDependency(bundle, dependency) ||
this._graph.hasEdge(
bundleNodeId,
this._graph.getNodeIdByContentKey(dependency.id),
bundleGraphEdgeTypes.internal_async,
)),
)
) {
this._graph.removeEdge(
bundleNodeId,
bundleGroupNodeId,
bundleGraphEdgeTypes.bundle,
);
}
}
}
createAssetReference(
dependency: Dependency,
asset: Asset,
bundle: Bundle,
): void {
let dependencyId = this._graph.getNodeIdByContentKey(dependency.id);
let assetId = this._graph.getNodeIdByContentKey(asset.id);
let bundleId = this._graph.getNodeIdByContentKey(bundle.id);
this._graph.addEdge(dependencyId, assetId, bundleGraphEdgeTypes.references);
this._graph.addEdge(
dependencyId,
bundleId,
bundleGraphEdgeTypes.references,
);
this.markDependencyReferenceable(dependency);
if (this._graph.hasEdge(dependencyId, assetId)) {
this._graph.removeEdge(dependencyId, assetId);
}
}
createBundleReference(from: Bundle, to: Bundle): void {
this._graph.addEdge(
this._graph.getNodeIdByContentKey(from.id),
this._graph.getNodeIdByContentKey(to.id),
bundleGraphEdgeTypes.references,
);
}
getBundlesWithAsset(asset: Asset): Array<Bundle> {
return this._graph
.getNodeIdsConnectedTo(
this._graph.getNodeIdByContentKey(asset.id),
bundleGraphEdgeTypes.contains,
)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle')
.map(node => {
invariant(node.type === 'bundle');
return node.value;
});
}
getBundlesWithDependency(dependency: Dependency): Array<Bundle> {
return this._graph
.getNodeIdsConnectedTo(
nullthrows(this._graph.getNodeIdByContentKey(dependency.id)),
bundleGraphEdgeTypes.contains,
)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle')
.map(node => {
invariant(node.type === 'bundle');
return node.value;
});
}
getDependencyAssets(dependency: Dependency): Array<Asset> {
return this._graph
.getNodeIdsConnectedFrom(this._graph.getNodeIdByContentKey(dependency.id))
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'asset')
.map(node => {
invariant(node.type === 'asset');
return node.value;
});
}
getResolvedAsset(dep: Dependency, bundle: ?Bundle): ?Asset {
let assets = this.getDependencyAssets(dep);
let firstAsset = assets[0];
let resolved =
// If no bundle is specified, use the first concrete asset.
bundle == null
? firstAsset
: // Otherwise, find the first asset that belongs to this bundle.
assets.find(asset => this.bundleHasAsset(bundle, asset)) ||
firstAsset;
// If a resolution still hasn't been found, return the first referenced asset.
if (resolved == null) {
this._graph.traverse(
(nodeId, _, traversal) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'asset') {
resolved = node.value;
traversal.stop();
} else if (node.id !== dep.id) {
traversal.skipChildren();
}
},
this._graph.getNodeIdByContentKey(dep.id),
bundleGraphEdgeTypes.references,
);
}
return resolved;
}
getDependencies(asset: Asset): Array<Dependency> {
let nodeId = this._graph.getNodeIdByContentKey(asset.id);
return this._graph.getNodeIdsConnectedFrom(nodeId).map(id => {
let node = nullthrows(this._graph.getNode(id));
invariant(node.type === 'dependency');
return node.value;
});
}
traverseAssets<TContext>(
bundle: Bundle,
visit: GraphVisitor<Asset, TContext>,
startAsset?: Asset,
): ?TContext {
return this.traverseBundle(
bundle,
mapVisitor(node => (node.type === 'asset' ? node.value : null), visit),
startAsset,
);
}
isAssetReferenced(bundle: Bundle, asset: Asset): boolean {
// If the asset is available in multiple bundles, it's referenced.
if (this.getBundlesWithAsset(asset).length > 1) {
return true;
}
let assetNodeId = nullthrows(this._graph.getNodeIdByContentKey(asset.id));
if (
this._graph
.getNodeIdsConnectedTo(assetNodeId, bundleGraphEdgeTypes.references)
.map(id => this._graph.getNode(id))
.some(
node =>
node?.type === 'dependency' &&
node.value.priority === Priority.lazy &&
node.value.specifierType !== SpecifierType.url,
)
) {
// If this asset is referenced by any async dependency, it's referenced.
return true;
}
let dependencies = this._graph
.getNodeIdsConnectedTo(assetNodeId)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'dependency')
.map(node => {
invariant(node.type === 'dependency');
return node.value;
});
const bundleHasReference = (bundle: Bundle) => {
return (
!this.bundleHasAsset(bundle, asset) &&
dependencies.some(dependency =>
this.bundleHasDependency(bundle, dependency),
)
);
};
let visitedBundles: Set<Bundle> = new Set();
let siblingBundles = new Set(
this.getBundleGroupsContainingBundle(bundle).flatMap(bundleGroup =>
this.getBundlesInBundleGroup(bundleGroup, {includeInline: true}),
),
);
// Check if any of this bundle's descendants, referencers, bundles referenced
// by referencers, or descendants of its referencers use the asset without
// an explicit reference edge. This can happen if e.g. the asset has been
// deduplicated.
return [...siblingBundles].some(referencer => {
let isReferenced = false;
this.traverseBundles((descendant, _, actions) => {
if (descendant.id === bundle.id) {
return;
}
if (visitedBundles.has(descendant)) {
actions.skipChildren();
return;
}
visitedBundles.add(descendant);
if (
descendant.type !== bundle.type ||
descendant.env.context !== bundle.env.context
) {
actions.skipChildren();
return;
}
if (bundleHasReference(descendant)) {
isReferenced = true;
actions.stop();
return;
}
}, referencer);
return isReferenced;
});
}
hasParentBundleOfType(bundle: Bundle, type: string): boolean {
let parents = this.getParentBundles(bundle);
return parents.length > 0 && parents.every(parent => parent.type === type);
}
getParentBundles(bundle: Bundle): Array<Bundle> {
let parentBundles: Set<Bundle> = new Set();
for (let bundleGroup of this.getBundleGroupsContainingBundle(bundle)) {
for (let parentBundle of this.getParentBundlesOfBundleGroup(
bundleGroup,
)) {
parentBundles.add(parentBundle);
}
}
return [...parentBundles];
}
isAssetReachableFromBundle(asset: Asset, bundle: Bundle): boolean {
// If a bundle's environment is isolated, it can't access assets present
// in any ancestor bundles. Don't consider any assets reachable.
if (
ISOLATED_ENVS.has(bundle.env.context) ||
!bundle.isSplittable ||
bundle.bundleBehavior === BundleBehavior.isolated ||
bundle.bundleBehavior === BundleBehavior.inline
) {
return false;
}
// For an asset to be reachable from a bundle, it must either exist in a sibling bundle,
// or in an ancestor bundle group reachable from all parent bundles.
let bundleGroups = this.getBundleGroupsContainingBundle(bundle);
return bundleGroups.every(bundleGroup => {
// If the asset is in any sibling bundles of the original bundle, it is reachable.
let bundles = this.getBundlesInBundleGroup(bundleGroup);
if (
bundles.some(
b =>
b.id !== bundle.id &&
b.bundleBehavior !== BundleBehavior.isolated &&
b.bundleBehavior !== BundleBehavior.inline &&
this.bundleHasAsset(b, asset),
)
) {
return true;
}
// Get a list of parent bundle nodes pointing to the bundle group
let parentBundleNodes = this._graph.getNodeIdsConnectedTo(
this._graph.getNodeIdByContentKey(getBundleGroupId(bundleGroup)),
bundleGraphEdgeTypes.bundle,
);
// Check that every parent bundle has a bundle group in its ancestry that contains the asset.
return parentBundleNodes.every(bundleNodeId => {
let bundleNode = nullthrows(this._graph.getNode(bundleNodeId));
if (
bundleNode.type !== 'bundle' ||
bundleNode.value.bundleBehavior === BundleBehavior.isolated ||
bundleNode.value.bundleBehavior === BundleBehavior.inline
) {
return false;
}
let isReachable = true;
this._graph.traverseAncestors(
bundleNodeId,
(nodeId, ctx, actions) => {
let node = nullthrows(this._graph.getNode(nodeId));
// If we've reached the root or a context change without
// finding this asset in the ancestry, it is not reachable.
if (
node.type === 'root' ||
(node.type === 'bundle' &&
(node.value.id === bundle.id ||
node.value.env.context !== bundle.env.context))
) {
isReachable = false;
actions.stop();
return;
}
if (node.type === 'bundle_group') {
let childBundles = this.getBundlesInBundleGroup(node.value);
if (
childBundles.some(
b =>
b.id !== bundle.id &&
b.bundleBehavior !== BundleBehavior.isolated &&
b.bundleBehavior !== BundleBehavior.inline &&
this.bundleHasAsset(b, asset),
)
) {
actions.skipChildren();
return;
}
}
},
[bundleGraphEdgeTypes.references, bundleGraphEdgeTypes.bundle],
);
return isReachable;
});
});
}
traverseBundle<TContext>(
bundle: Bundle,
visit: GraphVisitor<AssetNode | DependencyNode, TContext>,
startAsset?: Asset,
): ?TContext {
let entries = !startAsset;
let bundleNodeId = this._graph.getNodeIdByContentKey(bundle.id);
// A modified DFS traversal which traverses entry assets in the same order
// as their ids appear in `bundle.entryAssetIds`.
return this._graph.dfs({
visit: mapVisitor((nodeId, actions) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (nodeId === bundleNodeId) {
return;
}
if (node.type === 'dependency' || node.type === 'asset') {
if (
this._graph.hasEdge(
bundleNodeId,
nodeId,
bundleGraphEdgeTypes.contains,
)
) {
return node;
}
}
actions.skipChildren();
}, visit),
startNodeId: startAsset
? this._graph.getNodeIdByContentKey(startAsset.id)
: bundleNodeId,
getChildren: nodeId => {
let children = this._graph
.getNodeIdsConnectedFrom(nodeId)
.map(id => [id, nullthrows(this._graph.getNode(id))]);
let sorted =
entries && bundle.entryAssetIds.length > 0
? children.sort(([, a], [, b]) => {
let aIndex = bundle.entryAssetIds.indexOf(a.id);
let bIndex = bundle.entryAssetIds.indexOf(b.id);
if (aIndex === bIndex) {
// If both don't exist in the entry asset list, or
// otherwise have the same index.
return 0;
} else if (aIndex === -1) {
return 1;
} else if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
})
: children;
entries = false;
return sorted.map(([id]) => id);
},
});
}
traverse<TContext>(
visit: GraphVisitor<AssetNode | DependencyNode, TContext>,
start?: Asset,
): ?TContext {
return this._graph.filteredTraverse(
nodeId => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'asset' || node.type === 'dependency') {
return node;
}
},
visit,
start ? this._graph.getNodeIdByContentKey(start.id) : undefined, // start with root
ALL_EDGE_TYPES,
);
}
getChildBundles(bundle: Bundle): Array<Bundle> {
let siblings = new Set(this.getReferencedBundles(bundle));
let bundles = [];
this.traverseBundles((b, _, actions) => {
if (bundle.id === b.id) {
return;
}
if (!siblings.has(b)) {
bundles.push(b);
}
actions.skipChildren();
}, bundle);
return bundles;
}
traverseBundles<TContext>(
visit: GraphVisitor<Bundle, TContext>,
startBundle: ?Bundle,
): ?TContext {
return this._graph.filteredTraverse(
nodeId => {
let node = nullthrows(this._graph.getNode(nodeId));
return node.type === 'bundle' ? node.value : null;
},
visit,
startBundle ? this._graph.getNodeIdByContentKey(startBundle.id) : null,
[bundleGraphEdgeTypes.bundle, bundleGraphEdgeTypes.references],
);
}
getBundles(opts?: {|includeInline: boolean|}): Array<Bundle> {
let bundles = [];
this.traverseBundles(bundle => {
if (
opts?.includeInline ||
bundle.bundleBehavior !== BundleBehavior.inline
) {
bundles.push(bundle);
}
});
return bundles;
}
getTotalSize(asset: Asset): number {
let size = 0;
this._graph.traverse((nodeId, _, actions) => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'bundle_group') {
actions.skipChildren();
return;
}
if (node.type === 'asset') {
size += node.value.stats.size;
}
}, this._graph.getNodeIdByContentKey(asset.id));
return size;
}
getReferencingBundles(bundle: Bundle): Array<Bundle> {
let referencingBundles: Set<Bundle> = new Set();
this._graph.traverseAncestors(
this._graph.getNodeIdByContentKey(bundle.id),
nodeId => {
let node = nullthrows(this._graph.getNode(nodeId));
if (node.type === 'bundle' && node.value.id !== bundle.id) {
referencingBundles.add(node.value);
}
},
bundleGraphEdgeTypes.references,
);
return [...referencingBundles];
}
getBundleGroupsContainingBundle(bundle: Bundle): Array<BundleGroup> {
let bundleGroups: Set<BundleGroup> = new Set();
for (let currentBundle of [bundle, ...this.getReferencingBundles(bundle)]) {
for (let bundleGroup of this.getDirectParentBundleGroups(currentBundle)) {
bundleGroups.add(bundleGroup);
}
}
return [...bundleGroups];
}
getDirectParentBundleGroups(bundle: Bundle): Array<BundleGroup> {
return this._graph
.getNodeIdsConnectedTo(
nullthrows(this._graph.getNodeIdByContentKey(bundle.id)),
bundleGraphEdgeTypes.bundle,
)
.map(id => nullthrows(this._graph.getNode(id)))
.filter(node => node.type === 'bundle_group')
.map(node => {
invariant(node.type === 'bundle_group');
return node.value;
});
}
getBundlesInBundleGroup(
bundleGroup: BundleGroup,
opts?: {|includeInline: boolean|},
): Array<Bundle> {
let bundles: Set<Bundle> = new Set();
for (let bundleNodeId of this._graph.getNodeIdsConnectedFrom(
this._graph.getNodeIdByContentKey(getBundleGroupId(bundleGroup)),
bundleGraphEdgeTypes.bundle,
)) {
let bundleNode = nullthrows(this._graph.getNode(bundleNodeId));
invariant(bundleNode.type === 'bundle');
let bundle = bundleNode.value;
if (
opts?.includeInline |