UNPKG

18.4 kBJavaScriptView Raw
1// @flow strict-local
2
3import type {
4 BundleGroup,
5 GraphVisitor,
6 Symbol,
7 TraversalActions,
8} from '@parcel/types';
9
10import type {
11 Asset,
12 AssetNode,
13 Bundle,
14 BundleGraphNode,
15 Dependency,
16 DependencyNode,
17} from './types';
18import type AssetGraph from './AssetGraph';
19
20import invariant from 'assert';
21import crypto from 'crypto';
22import nullthrows from 'nullthrows';
23import {flatMap, objectSortedEntriesDeep} from '@parcel/utils';
24
25import {getBundleGroupId} from './utils';
26import Graph, {mapVisitor} from './Graph';
27
28type BundleGraphEdgeTypes =
29 // A lack of an edge type indicates to follow the edge while traversing
30 // the bundle's contents, e.g. `bundle.traverse()` during packaging.
31 | null
32 // Used for constant-time checks of presence of a dependency or asset in a bundle,
33 // avoiding bundle traversal in cases like `isAssetInAncestors`
34 | 'contains'
35 // Connections between bundles and bundle groups, for quick traversal of the
36 // bundle hierarchy.
37 | 'bundle'
38 // Indicates that the asset a dependency references is contained in another bundle.
39 // Using this type prevents referenced assets from being traversed normally.
40 | 'references';
41
42export default class BundleGraph {
43 // TODO: These hashes are being invalidated in mutative methods, but this._graph is not a private
44 // property so it is possible to reach in and mutate the graph without invalidating these hashes.
45 // It needs to be exposed in BundlerRunner for now based on how applying runtimes works and the
46 // BundlerRunner takes care of invalidating hashes when runtimes are applied, but this is not ideal.
47 _bundleContentHashes: Map<string, string>;
48 _graph: Graph<BundleGraphNode, BundleGraphEdgeTypes>;
49
50 constructor({
51 graph,
52 bundleContentHashes,
53 }: {|
54 graph: Graph<BundleGraphNode, BundleGraphEdgeTypes>,
55 bundleContentHashes?: Map<string, string>,
56 |}) {
57 this._graph = graph;
58 this._bundleContentHashes = bundleContentHashes || new Map();
59 }
60
61 static deserialize(opts: {|
62 _graph: Graph<BundleGraphNode, BundleGraphEdgeTypes>,
63 _bundleContentHashes: Map<string, string>,
64 |}): BundleGraph {
65 return new BundleGraph({
66 graph: opts._graph,
67 bundleContentHashes: opts._bundleContentHashes,
68 });
69 }
70
71 addAssetGraphToBundle(asset: Asset, bundle: Bundle) {
72 // The root asset should be reached directly from the bundle in traversal.
73 // Its children will be traversed from there.
74 this._graph.addEdge(bundle.id, asset.id);
75 this._graph.traverse((node, _, actions) => {
76 if (node.type === 'bundle_group') {
77 actions.skipChildren();
78 return;
79 }
80
81 if (node.type === 'asset' && !this.bundleHasAsset(bundle, node.value)) {
82 bundle.stats.size += node.value.stats.size;
83 }
84
85 if (node.type === 'asset' || node.type === 'dependency') {
86 this._graph.addEdge(bundle.id, node.id, 'contains');
87 }
88
89 if (node.type === 'dependency') {
90 for (let bundleGroupNode of this._graph
91 .getNodesConnectedFrom(node)
92 .filter(node => node.type === 'bundle_group')) {
93 this._graph.addEdge(bundle.id, bundleGroupNode.id, 'bundle');
94 }
95 }
96 }, nullthrows(this._graph.getNode(asset.id)));
97 this._bundleContentHashes.delete(bundle.id);
98 }
99
100 removeAssetGraphFromBundle(asset: Asset, bundle: Bundle) {
101 // Remove all contains edges from the bundle to the nodes in the asset's
102 // subgraph.
103 this._graph.traverse((node, context, actions) => {
104 if (node.type === 'bundle_group') {
105 actions.skipChildren();
106 return;
107 }
108
109 if (node.type === 'asset' || node.type === 'dependency') {
110 if (this._graph.hasEdge(bundle.id, node.id, 'contains')) {
111 this._graph.removeEdge(
112 bundle.id,
113 node.id,
114 'contains',
115 // Removing this contains edge should not orphan the connected node. This
116 // is disabled for performance reasons as these edges are removed as part
117 // of a traversal, and checking for orphans becomes quite expensive in
118 // aggregate.
119 false /* removeOrphans */,
120 );
121 if (node.type === 'asset') {
122 bundle.stats.size -= asset.stats.size;
123 }
124 } else {
125 actions.skipChildren();
126 }
127 }
128
129 if (node.type === 'dependency') {
130 for (let bundleGroupNode of this._graph
131 .getNodesConnectedFrom(node)
132 .filter(node => node.type === 'bundle_group')) {
133 let inboundDependencies = this._graph
134 .getNodesConnectedTo(bundleGroupNode)
135 .filter(node => node.type === 'dependency');
136
137 // If every inbound dependency to this bundle group does not belong to this bundle,
138 // then the connection between this bundle and the group is safe to remove.
139 if (
140 inboundDependencies.every(
141 depNode =>
142 !this._graph.hasEdge(bundle.id, depNode.id, 'contains'),
143 )
144 ) {
145 this._graph.removeEdge(bundle.id, bundleGroupNode.id, 'bundle');
146 }
147 }
148 }
149 }, nullthrows(this._graph.getNode(asset.id)));
150
151 // Remove the untyped edge from the bundle to the entry.
152 if (this._graph.hasEdge(bundle.id, asset.id)) {
153 this._graph.removeEdge(bundle.id, asset.id);
154 }
155
156 this._bundleContentHashes.delete(bundle.id);
157 }
158
159 createAssetReference(dependency: Dependency, asset: Asset): void {
160 this._graph.addEdge(dependency.id, asset.id, 'references');
161 if (this._graph.hasEdge(dependency.id, asset.id)) {
162 this._graph.removeEdge(dependency.id, asset.id);
163 }
164 }
165
166 findBundlesWithAsset(asset: Asset): Array<Bundle> {
167 return this._graph
168 .getNodesConnectedTo(
169 nullthrows(this._graph.getNode(asset.id)),
170 'contains',
171 )
172 .filter(node => node.type === 'bundle')
173 .map(node => {
174 invariant(node.type === 'bundle');
175 return node.value;
176 });
177 }
178
179 getDependencyAssets(dependency: Dependency): Array<Asset> {
180 let dependencyNode = nullthrows(this._graph.getNode(dependency.id));
181 return this._graph
182 .getNodesConnectedFrom(dependencyNode)
183 .filter(node => node.type === 'asset')
184 .map(node => {
185 invariant(node.type === 'asset');
186 return node.value;
187 });
188 }
189
190 getDependencyResolution(dep: Dependency): ?Asset {
191 let depNode = this._graph.getNode(dep.id);
192 if (!depNode) {
193 return null;
194 }
195
196 let res = null;
197 function findFirstAsset(node, _, traversal) {
198 if (node.type === 'asset') {
199 res = node.value;
200 traversal.stop();
201 } else if (node.id !== dep.id) {
202 traversal.skipChildren();
203 }
204 }
205
206 // TODO: Combine with multiple edge type traversal?
207 this._graph.traverse(findFirstAsset, depNode);
208 if (!res) {
209 // Prefer real assets when resolving dependencies, but use the first
210 // asset reference in absence of a real one.
211 this._graph.traverse(findFirstAsset, depNode, 'references');
212 }
213
214 return res;
215 }
216
217 getDependencies(asset: Asset): Array<Dependency> {
218 let node = this._graph.getNode(asset.id);
219 if (!node) {
220 throw new Error('Asset not found');
221 }
222
223 return this._graph.getNodesConnectedFrom(node).map(node => {
224 invariant(node.type === 'dependency');
225 return node.value;
226 });
227 }
228
229 traverseAssets<TContext>(
230 bundle: Bundle,
231 visit: GraphVisitor<Asset, TContext>,
232 ): ?TContext {
233 return this.traverseBundle(
234 bundle,
235 mapVisitor(node => (node.type === 'asset' ? node.value : null), visit),
236 );
237 }
238
239 isAssetReferenced(asset: Asset): boolean {
240 return (
241 this._graph.getNodesConnectedTo(
242 nullthrows(this._graph.getNode(asset.id)),
243 'references',
244 ).length > 0
245 );
246 }
247
248 isAssetReferencedByAssetType(asset: Asset, type: string): boolean {
249 let referringBundles = new Set(
250 this._graph.getNodesConnectedTo(
251 nullthrows(this._graph.getNode(asset.id)),
252 'contains',
253 ),
254 );
255
256 // is `asset` referenced by a dependency from an asset of `type`
257 return flatMap(
258 this._graph
259 .getNodesConnectedTo(nullthrows(this._graph.getNode(asset.id)))
260 .filter(node => {
261 // Does this dependency belong to a bundle that does not include the
262 // asset it resolves to? If so, this asset is needed by a bundle but
263 // does not belong to it.
264 return this._graph
265 .getNodesConnectedTo(node, 'contains')
266 .filter(node => node.type === 'bundle')
267 .some(b => !referringBundles.has(b));
268 }),
269 node => {
270 invariant(node.type === 'dependency');
271 return this._graph.getNodesConnectedTo(node, null);
272 },
273 )
274 .filter(node => node.type === 'asset')
275 .some(node => {
276 invariant(node.type === 'asset');
277 return node.value.type === type;
278 });
279 }
280
281 hasParentBundleOfType(bundle: Bundle, type: string): boolean {
282 return flatMap(
283 this._graph.getNodesConnectedTo(
284 nullthrows(this._graph.getNode(bundle.id)),
285 'bundle',
286 ),
287 node => this._graph.getNodesConnectedTo(node, 'bundle'),
288 ).every(node => node.type === 'bundle' && node.value.type === type);
289 }
290
291 isAssetInAncestorBundles(bundle: Bundle, asset: Asset): boolean {
292 let parentBundleNodes = flatMap(
293 this._graph.getNodesConnectedTo(
294 nullthrows(this._graph.getNode(bundle.id)),
295 'bundle',
296 ),
297 bundleGroupNode => {
298 invariant(bundleGroupNode.type === 'bundle_group');
299 return this._graph.getNodesConnectedTo(bundleGroupNode, 'bundle');
300 },
301 );
302
303 return parentBundleNodes.every(parentNode => {
304 let inBundle;
305
306 this._graph.traverseAncestors(
307 parentNode,
308 (node, ctx, actions) => {
309 if (node.type !== 'bundle' || node.id === bundle.id) {
310 return;
311 }
312
313 // Don't deduplicate when context changes
314 if (node.value.env.context !== bundle.env.context) {
315 actions.skipChildren();
316 return;
317 }
318
319 if (this._graph.hasEdge(node.value.id, asset.id, 'contains')) {
320 inBundle = true;
321 actions.stop();
322 }
323 },
324 'bundle',
325 );
326
327 return inBundle;
328 });
329 }
330
331 traverseBundle<TContext>(
332 bundle: Bundle,
333 visit: GraphVisitor<AssetNode | DependencyNode, TContext>,
334 ): ?TContext {
335 return this._graph.filteredTraverse(
336 (node, actions) => {
337 if (node.id === bundle.id) {
338 return;
339 }
340
341 if (node.type === 'dependency' || node.type === 'asset') {
342 if (this._graph.hasEdge(bundle.id, node.id, 'contains')) {
343 return node;
344 }
345 }
346
347 actions.skipChildren();
348 },
349 visit,
350 nullthrows(this._graph.getNode(bundle.id)),
351 );
352 }
353
354 traverseContents<TContext>(
355 visit: GraphVisitor<AssetNode | DependencyNode, TContext>,
356 ): ?TContext {
357 return this._graph.filteredTraverse(
358 node =>
359 node.type === 'asset' || node.type === 'dependency' ? node : null,
360 visit,
361 );
362 }
363
364 getChildBundles(bundle: Bundle): Array<Bundle> {
365 let bundles = [];
366 this.traverseBundles((b, _, actions) => {
367 if (bundle.id === b.id) {
368 return;
369 }
370
371 bundles.push(b);
372 actions.skipChildren();
373 }, bundle);
374 return bundles;
375 }
376
377 traverseBundles<TContext>(
378 visit: GraphVisitor<Bundle, TContext>,
379 startBundle?: Bundle,
380 ): ?TContext {
381 return this._graph.filteredTraverse(
382 node => (node.type === 'bundle' ? node.value : null),
383 visit,
384 startBundle ? nullthrows(this._graph.getNode(startBundle.id)) : null,
385 'bundle',
386 );
387 }
388
389 getBundles(): Array<Bundle> {
390 let bundles = [];
391 this.traverseBundles(bundle => {
392 bundles.push(bundle);
393 });
394
395 return bundles;
396 }
397
398 getTotalSize(asset: Asset): number {
399 let size = 0;
400 this._graph.traverse((node, _, actions) => {
401 if (node.type === 'bundle_group') {
402 actions.skipChildren();
403 return;
404 }
405
406 if (node.type === 'asset') {
407 size += node.value.stats.size;
408 }
409 }, nullthrows(this._graph.getNode(asset.id)));
410 return size;
411 }
412
413 getBundleGroupsContainingBundle(bundle: Bundle): Array<BundleGroup> {
414 return this._graph
415 .getNodesConnectedTo(nullthrows(this._graph.getNode(bundle.id)), 'bundle')
416 .filter(node => node.type === 'bundle_group')
417 .map(node => {
418 invariant(node.type === 'bundle_group');
419 return node.value;
420 });
421 }
422
423 getBundlesInBundleGroup(bundleGroup: BundleGroup): Array<Bundle> {
424 return this._graph
425 .getNodesConnectedFrom(
426 nullthrows(this._graph.getNode(getBundleGroupId(bundleGroup))),
427 'bundle',
428 )
429 .filter(node => node.type === 'bundle')
430 .map(node => {
431 invariant(node.type === 'bundle');
432 return node.value;
433 });
434 }
435
436 getSiblingBundles(bundle: Bundle): Array<Bundle> {
437 let siblings = [];
438
439 let bundleGroups = this.getBundleGroupsContainingBundle(bundle);
440 for (let bundleGroup of bundleGroups) {
441 let bundles = this.getBundlesInBundleGroup(bundleGroup);
442 for (let b of bundles) {
443 if (b.id !== bundle.id) {
444 siblings.push(b);
445 }
446 }
447 }
448
449 return siblings;
450 }
451
452 getIncomingDependencies(asset: Asset): Array<Dependency> {
453 let node = this._graph.getNode(asset.id);
454 if (!node) {
455 return [];
456 }
457
458 return this._graph
459 .findAncestors(node, node => node.type === 'dependency')
460 .map(node => {
461 invariant(node.type === 'dependency');
462 return node.value;
463 });
464 }
465
466 bundleHasAsset(bundle: Bundle, asset: Asset): boolean {
467 return this._graph.hasEdge(bundle.id, asset.id, 'contains');
468 }
469
470 filteredTraverse<TValue, TContext>(
471 bundle: Bundle,
472 filter: (BundleGraphNode, TraversalActions) => ?TValue,
473 visit: GraphVisitor<TValue, TContext>,
474 ): ?TContext {
475 return this._graph.filteredTraverse(
476 filter,
477 visit,
478 nullthrows(this._graph.getNode(bundle.id)),
479 );
480 }
481
482 resolveSymbol(asset: Asset, symbol: Symbol) {
483 let identifier = asset.symbols.get(symbol);
484 if (symbol === '*') {
485 return {asset, exportSymbol: '*', symbol: identifier};
486 }
487
488 let deps = this.getDependencies(asset).reverse();
489 for (let dep of deps) {
490 // If this is a re-export, find the original module.
491 let symbolLookup = new Map(
492 [...dep.symbols].map(([key, val]) => [val, key]),
493 );
494 let depSymbol = symbolLookup.get(identifier);
495 if (depSymbol != null) {
496 let resolved = this.getDependencyResolution(dep);
497 if (!resolved) {
498 // External module.
499 break;
500 }
501
502 let {
503 asset: resolvedAsset,
504 symbol: resolvedSymbol,
505 exportSymbol,
506 } = this.resolveSymbol(resolved, depSymbol);
507
508 // If it didn't resolve to anything (likely CommonJS), pass through where we got to
509 if (resolvedSymbol == null) {
510 return {asset: resolvedAsset, symbol: resolvedSymbol, exportSymbol};
511 }
512
513 // Otherwise, keep the original symbol name along with the resolved symbol
514 return {
515 asset: resolvedAsset,
516 symbol: resolvedSymbol,
517 exportSymbol: symbol,
518 };
519 }
520
521 // If this module exports wildcards, resolve the original module.
522 // Default exports are excluded from wildcard exports.
523 if (dep.symbols.get('*') === '*' && symbol !== 'default') {
524 let resolved = nullthrows(this.getDependencyResolution(dep));
525 let result = this.resolveSymbol(resolved, symbol);
526 if (result.symbol != null) {
527 return {
528 asset: result.asset,
529 symbol: result.symbol,
530 exportSymbol: symbol,
531 };
532 }
533 }
534 }
535
536 return {asset, exportSymbol: symbol, symbol: identifier};
537 }
538
539 getExportedSymbols(asset: Asset) {
540 let symbols = [];
541
542 for (let symbol of asset.symbols.keys()) {
543 symbols.push(this.resolveSymbol(asset, symbol));
544 }
545
546 let deps = this.getDependencies(asset);
547 for (let dep of deps) {
548 if (dep.symbols.get('*') === '*') {
549 let resolved = nullthrows(this.getDependencyResolution(dep));
550 let exported = this.getExportedSymbols(resolved).filter(
551 s => s.exportSymbol !== 'default',
552 );
553 symbols.push(...exported);
554 }
555 }
556
557 return symbols;
558 }
559
560 getContentHash(bundle: Bundle): string {
561 let existingHash = this._bundleContentHashes.get(bundle.id);
562 if (existingHash != null) {
563 return existingHash;
564 }
565
566 let hash = crypto.createHash('md5');
567 // TODO: sort??
568 this.traverseAssets(bundle, asset => {
569 hash.update([asset.outputHash, asset.filePath].join(':'));
570 });
571
572 let hashHex = hash.digest('hex');
573 this._bundleContentHashes.set(bundle.id, hashHex);
574 return hashHex;
575 }
576
577 getHash(bundle: Bundle): string {
578 let hash = crypto.createHash('md5');
579 this.traverseBundles(childBundle => {
580 hash.update(this.getContentHash(childBundle));
581 }, bundle);
582
583 hash.update(JSON.stringify(objectSortedEntriesDeep(bundle.env)));
584 return hash.digest('hex');
585 }
586}
587
588export function removeAssetGroups(
589 assetGraph: AssetGraph,
590): Graph<BundleGraphNode> {
591 let graph = new Graph<BundleGraphNode>();
592
593 let rootNode = assetGraph.getRootNode();
594 invariant(rootNode != null && rootNode.type === 'root');
595 graph.setRootNode(rootNode);
596
597 let assetGroupIds = new Set();
598 for (let [, node] of assetGraph.nodes) {
599 if (node.type === 'asset_group') {
600 assetGroupIds.add(node.id);
601 } else {
602 graph.addNode(node);
603 }
604 }
605
606 for (let edge of assetGraph.getAllEdges()) {
607 let fromIds;
608 if (assetGroupIds.has(edge.from)) {
609 fromIds = [...assetGraph.inboundEdges.get(edge.from).get(null)];
610 } else {
611 fromIds = [edge.from];
612 }
613
614 for (let from of fromIds) {
615 if (assetGroupIds.has(edge.to)) {
616 for (let to of assetGraph.outboundEdges.get(edge.to).get(null)) {
617 graph.addEdge(from, to);
618 }
619 } else {
620 graph.addEdge(from, edge.to);
621 }
622 }
623 }
624
625 return graph;
626}