1 |
|
2 |
|
3 | import type {
|
4 | BundleGroup,
|
5 | GraphVisitor,
|
6 | Symbol,
|
7 | TraversalActions,
|
8 | } from '@parcel/types';
|
9 |
|
10 | import type {
|
11 | Asset,
|
12 | AssetNode,
|
13 | Bundle,
|
14 | BundleGraphNode,
|
15 | Dependency,
|
16 | DependencyNode,
|
17 | } from './types';
|
18 | import type AssetGraph from './AssetGraph';
|
19 |
|
20 | import invariant from 'assert';
|
21 | import crypto from 'crypto';
|
22 | import nullthrows from 'nullthrows';
|
23 | import {flatMap, objectSortedEntriesDeep} from '@parcel/utils';
|
24 |
|
25 | import {getBundleGroupId} from './utils';
|
26 | import Graph, {mapVisitor} from './Graph';
|
27 |
|
28 | type BundleGraphEdgeTypes =
|
29 |
|
30 |
|
31 | | null
|
32 |
|
33 |
|
34 | | 'contains'
|
35 |
|
36 |
|
37 | | 'bundle'
|
38 |
|
39 |
|
40 | | 'references';
|
41 |
|
42 | export default class BundleGraph {
|
43 |
|
44 |
|
45 |
|
46 |
|
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 |
|
73 |
|
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 |
|
102 |
|
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 |
|
116 |
|
117 |
|
118 |
|
119 | false ,
|
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 |
|
138 |
|
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 |
|
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 |
|
207 | this._graph.traverse(findFirstAsset, depNode);
|
208 | if (!res) {
|
209 |
|
210 |
|
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 |
|
257 | return flatMap(
|
258 | this._graph
|
259 | .getNodesConnectedTo(nullthrows(this._graph.getNode(asset.id)))
|
260 | .filter(node => {
|
261 |
|
262 |
|
263 |
|
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 |
|
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 |
|
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 |
|
499 | break;
|
500 | }
|
501 |
|
502 | let {
|
503 | asset: resolvedAsset,
|
504 | symbol: resolvedSymbol,
|
505 | exportSymbol,
|
506 | } = this.resolveSymbol(resolved, depSymbol);
|
507 |
|
508 |
|
509 | if (resolvedSymbol == null) {
|
510 | return {asset: resolvedAsset, symbol: resolvedSymbol, exportSymbol};
|
511 | }
|
512 |
|
513 |
|
514 | return {
|
515 | asset: resolvedAsset,
|
516 | symbol: resolvedSymbol,
|
517 | exportSymbol: symbol,
|
518 | };
|
519 | }
|
520 |
|
521 |
|
522 |
|
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 |
|
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 |
|
588 | export 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 | }
|