@atlaspack/bundler-default 2.14.5-canary.35 → 2.14.5-canary.351

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +589 -0
  2. package/dist/DefaultBundler.js +84 -0
  3. package/dist/MonolithicBundler.js +68 -0
  4. package/dist/bundleMerge.js +137 -0
  5. package/dist/bundlerConfig.js +223 -0
  6. package/dist/decorateLegacyGraph.js +189 -0
  7. package/dist/idealGraph.js +1471 -0
  8. package/dist/memoize.js +31 -0
  9. package/dist/stats.js +69 -0
  10. package/lib/DefaultBundler.js +6 -1
  11. package/lib/MonolithicBundler.js +11 -3
  12. package/lib/bundleMerge.js +106 -37
  13. package/lib/bundlerConfig.js +52 -6
  14. package/lib/decorateLegacyGraph.js +24 -3
  15. package/lib/idealGraph.js +410 -55
  16. package/lib/memoize.js +39 -0
  17. package/lib/stats.js +85 -0
  18. package/lib/types/DefaultBundler.d.ts +18 -0
  19. package/lib/types/MonolithicBundler.d.ts +2 -0
  20. package/lib/types/bundleMerge.d.ts +9 -0
  21. package/lib/types/bundlerConfig.d.ts +36 -0
  22. package/lib/types/decorateLegacyGraph.d.ts +3 -0
  23. package/lib/types/idealGraph.d.ts +40 -0
  24. package/lib/types/memoize.d.ts +2 -0
  25. package/lib/types/stats.d.ts +16 -0
  26. package/package.json +20 -12
  27. package/src/{DefaultBundler.js → DefaultBundler.ts} +21 -6
  28. package/src/{MonolithicBundler.js → MonolithicBundler.ts} +17 -5
  29. package/src/bundleMerge.ts +250 -0
  30. package/src/{bundlerConfig.js → bundlerConfig.ts} +106 -45
  31. package/src/{decorateLegacyGraph.js → decorateLegacyGraph.ts} +26 -7
  32. package/src/{idealGraph.js → idealGraph.ts} +729 -137
  33. package/src/memoize.ts +32 -0
  34. package/src/stats.ts +97 -0
  35. package/tsconfig.json +30 -0
  36. package/tsconfig.tsbuildinfo +1 -0
  37. package/src/bundleMerge.js +0 -103
@@ -1,6 +1,5 @@
1
- // @flow strict-local
2
-
3
1
  import path from 'path';
2
+ import sortedArray from 'sorted-array-functions';
4
3
 
5
4
  import {getFeatureFlag} from '@atlaspack/feature-flags';
6
5
  import {
@@ -8,7 +7,7 @@ import {
8
7
  BitSet,
9
8
  ContentGraph,
10
9
  Graph,
11
- type NodeId,
10
+ NodeId,
12
11
  } from '@atlaspack/graph';
13
12
  import type {
14
13
  Asset,
@@ -18,42 +17,50 @@ import type {
18
17
  MutableBundleGraph,
19
18
  Target,
20
19
  PluginLogger,
20
+ BundleGraphTraversable,
21
+ TraversalActions,
21
22
  } from '@atlaspack/types';
22
23
  import {DefaultMap, globToRegex} from '@atlaspack/utils';
23
24
  import invariant from 'assert';
24
25
  import nullthrows from 'nullthrows';
25
26
 
26
- import {findMergeCandidates} from './bundleMerge';
27
- import type {ResolvedBundlerConfig} from './bundlerConfig';
27
+ import {findMergeCandidates, MergeGroup} from './bundleMerge';
28
+ import type {
29
+ ResolvedBundlerConfig,
30
+ SharedBundleMergeCandidates,
31
+ AsyncBundleMerge,
32
+ } from './bundlerConfig';
33
+ import {Stats} from './stats';
28
34
 
29
35
  /* BundleRoot - An asset that is the main entry of a Bundle. */
30
36
  type BundleRoot = Asset;
31
37
 
32
- export type Bundle = {|
33
- uniqueKey: ?string,
34
- assets: Set<Asset>,
35
- internalizedAssets?: BitSet,
36
- bundleBehavior?: ?BundleBehavior,
37
- needsStableName: boolean,
38
- mainEntryAsset: ?Asset,
39
- size: number,
40
- sourceBundles: Set<NodeId>,
41
- target: Target,
42
- env: Environment,
43
- type: string,
44
- manualSharedBundle: ?string, // for naming purposes
45
- |};
38
+ export type Bundle = {
39
+ uniqueKey: string | null | undefined;
40
+ assets: Set<Asset>;
41
+ internalizedAssets?: BitSet;
42
+ bundleBehavior?: BundleBehavior | null | undefined;
43
+ needsStableName: boolean;
44
+ mainEntryAsset: Asset | null | undefined;
45
+ bundleRoots: Set<Asset>;
46
+ size: number;
47
+ sourceBundles: Set<NodeId>;
48
+ target: Target;
49
+ env: Environment;
50
+ type: string;
51
+ manualSharedBundle: string | null | undefined; // for naming purposes;
52
+ };
46
53
 
47
54
  export type DependencyBundleGraph = ContentGraph<
48
- | {|
49
- value: Bundle,
50
- type: 'bundle',
51
- |}
52
- | {|
53
- value: Dependency,
54
- type: 'dependency',
55
- |},
56
- number,
55
+ | {
56
+ value: Bundle;
57
+ type: 'bundle';
58
+ }
59
+ | {
60
+ value: Dependency;
61
+ type: 'dependency';
62
+ },
63
+ number
57
64
  >;
58
65
 
59
66
  const dependencyPriorityEdges = {
@@ -61,7 +68,7 @@ const dependencyPriorityEdges = {
61
68
  parallel: 2,
62
69
  lazy: 3,
63
70
  conditional: 4,
64
- };
71
+ } as const;
65
72
 
66
73
  export const idealBundleGraphEdges = Object.freeze({
67
74
  default: 1,
@@ -70,20 +77,30 @@ export const idealBundleGraphEdges = Object.freeze({
70
77
 
71
78
  export type IdealBundleGraph = Graph<
72
79
  Bundle | 'root',
73
- $Values<typeof idealBundleGraphEdges>,
80
+ (typeof idealBundleGraphEdges)[keyof typeof idealBundleGraphEdges]
74
81
  >;
75
82
 
76
83
  // IdealGraph is the structure we will pass to decorate,
77
84
  // which mutates the assetGraph into the bundleGraph we would
78
85
  // expect from default bundler
79
- export type IdealGraph = {|
80
- assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
81
- assets: Array<Asset>,
82
- bundleGraph: IdealBundleGraph,
83
- bundleGroupBundleIds: Set<NodeId>,
84
- dependencyBundleGraph: DependencyBundleGraph,
85
- manualAssetToBundle: Map<Asset, NodeId>,
86
- |};
86
+ export type IdealGraph = {
87
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>;
88
+ assets: Array<Asset>;
89
+ bundleGraph: IdealBundleGraph;
90
+ bundleGroupBundleIds: Set<NodeId>;
91
+ dependencyBundleGraph: DependencyBundleGraph;
92
+ manualAssetToBundle: Map<Asset, NodeId>;
93
+ };
94
+
95
+ function isNonRootBundle(
96
+ bundle?: Bundle | 'root' | null,
97
+ message?: string,
98
+ ): Bundle {
99
+ let existingBundle = nullthrows(bundle, message);
100
+ invariant(existingBundle !== 'root', "Bundle cannot be 'root'");
101
+
102
+ return existingBundle;
103
+ }
87
104
 
88
105
  export function createIdealGraph(
89
106
  assetGraph: MutableBundleGraph,
@@ -97,8 +114,9 @@ export function createIdealGraph(
97
114
  let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph();
98
115
  let assetReference: DefaultMap<
99
116
  Asset,
100
- Array<[Dependency, Bundle]>,
117
+ Array<[Dependency, Bundle]>
101
118
  > = new DefaultMap(() => []);
119
+ let stats = new Stats(config.projectRoot);
102
120
 
103
121
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
104
122
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -111,8 +129,9 @@ export function createIdealGraph(
111
129
  };
112
130
  // Graph that models bundleRoots, with parallel & async deps only to inform reachability
113
131
  let bundleRootGraph: Graph<
114
- number, // asset index
115
- $Values<typeof bundleRootEdgeTypes>,
132
+ // asset index
133
+ number,
134
+ (typeof bundleRootEdgeTypes)[keyof typeof bundleRootEdgeTypes]
116
135
  > = new Graph();
117
136
  let assetToBundleRootNodeId = new Map<BundleRoot, number>();
118
137
 
@@ -146,26 +165,47 @@ export function createIdealGraph(
146
165
  bundleGroupBundleIds.add(nodeId);
147
166
  }
148
167
 
149
- let assets = [];
168
+ let assets: Array<Asset> = [];
150
169
  let assetToIndex = new Map<Asset, number>();
151
170
 
152
- function makeManualAssetToConfigLookup() {
153
- let manualAssetToConfig = new Map();
154
- let constantModuleToMSB = new DefaultMap(() => []);
171
+ function makeManualAssetToConfigLookup(): {
172
+ manualAssetToConfig: Map<
173
+ Asset,
174
+ ResolvedBundlerConfig['manualSharedBundles'][number]
175
+ >;
176
+ constantModuleToMSB: DefaultMap<
177
+ Asset,
178
+ Array<ResolvedBundlerConfig['manualSharedBundles'][number]>
179
+ >;
180
+ } {
181
+ let manualAssetToConfig = new Map<
182
+ Asset,
183
+ ResolvedBundlerConfig['manualSharedBundles'][number]
184
+ >();
185
+ let constantModuleToMSB = new DefaultMap<
186
+ Asset,
187
+ Array<ResolvedBundlerConfig['manualSharedBundles'][number]>
188
+ >(() => []);
155
189
 
156
190
  if (config.manualSharedBundles.length === 0) {
157
191
  return {manualAssetToConfig, constantModuleToMSB};
158
192
  }
159
193
 
160
- let parentsToConfig = new DefaultMap(() => []);
194
+ let parentsToConfig = new DefaultMap<
195
+ string,
196
+ Array<ResolvedBundlerConfig['manualSharedBundles'][number]>
197
+ >(() => []);
161
198
 
162
199
  for (let c of config.manualSharedBundles) {
163
200
  if (c.root != null) {
164
201
  parentsToConfig.get(path.join(config.projectRoot, c.root)).push(c);
165
202
  }
166
203
  }
167
- let numParentsToFind = parentsToConfig.size;
168
- let configToParentAsset = new Map();
204
+ let numParentsToFind: number = parentsToConfig.size;
205
+ let configToParentAsset = new Map<
206
+ ResolvedBundlerConfig['manualSharedBundles'][number],
207
+ Asset
208
+ >();
169
209
 
170
210
  assetGraph.traverse((node, _, actions) => {
171
211
  if (node.type === 'asset' && parentsToConfig.has(node.value.filePath)) {
@@ -192,8 +232,10 @@ export function createIdealGraph(
192
232
  continue;
193
233
  }
194
234
 
195
- let parentAsset = configToParentAsset.get(c);
196
- let assetRegexes = c.assets.map((glob) => globToRegex(glob));
235
+ let parentAsset: Asset | undefined = configToParentAsset.get(c);
236
+ let assetRegexes: Array<RegExp> = c.assets.map((glob) =>
237
+ globToRegex(glob),
238
+ );
197
239
 
198
240
  assetGraph.traverse((node, _, actions) => {
199
241
  if (
@@ -204,17 +246,16 @@ export function createIdealGraph(
204
246
  config.projectRoot,
205
247
  node.value.filePath,
206
248
  );
207
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
208
- return;
209
- }
210
249
 
211
250
  // We track all matching MSB's for constant modules as they are never duplicated
212
251
  // and need to be assigned to all matching bundles
213
252
  if (node.value.meta.isConstantModule === true) {
214
253
  constantModuleToMSB.get(node.value).push(c);
215
254
  }
216
- manualAssetToConfig.set(node.value, c);
217
- return;
255
+
256
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
257
+ manualAssetToConfig.set(node.value, c);
258
+ }
218
259
  }
219
260
 
220
261
  if (
@@ -242,9 +283,19 @@ export function createIdealGraph(
242
283
  makeManualAssetToConfigLookup();
243
284
  let manualBundleToInternalizedAsset: DefaultMap<
244
285
  NodeId,
245
- Array<Asset>,
286
+ Array<Asset>
246
287
  > = new DefaultMap(() => []);
247
288
 
289
+ let mergeSourceBundleLookup = new Map<string, NodeId>();
290
+ let mergeSourceBundleAssets: Set<string> = new Set(
291
+ config.sharedBundleMerge?.flatMap(
292
+ (c) =>
293
+ c.sourceBundles?.map((assetMatch: string) =>
294
+ path.join(config.projectRoot, assetMatch),
295
+ ) ?? [],
296
+ ),
297
+ );
298
+
248
299
  /**
249
300
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
250
301
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -252,7 +303,26 @@ export function createIdealGraph(
252
303
  */
253
304
  assetGraph.traverse(
254
305
  {
255
- enter(node, context, actions) {
306
+ enter(
307
+ node:
308
+ | BundleGraphTraversable
309
+ | {
310
+ readonly type: 'dependency';
311
+ value: Dependency;
312
+ },
313
+ context:
314
+ | {
315
+ readonly type: 'asset';
316
+ value: Asset;
317
+ }
318
+ | null
319
+ | undefined
320
+ | {
321
+ readonly type: 'dependency';
322
+ value: Dependency;
323
+ },
324
+ actions: TraversalActions,
325
+ ) {
256
326
  if (node.type === 'asset') {
257
327
  if (
258
328
  context?.type === 'dependency' &&
@@ -288,12 +358,16 @@ export function createIdealGraph(
288
358
  }
289
359
 
290
360
  for (let childAsset of assets) {
291
- let bundleId = bundles.get(childAsset.id);
292
- let bundle;
361
+ let bundleId: number | undefined | null = bundles.get(
362
+ childAsset.id,
363
+ );
364
+ let bundle: Bundle | 'root' | undefined;
293
365
 
294
366
  // MSB Step 1: Match glob on filepath and type for any asset
295
- let manualSharedBundleKey;
296
- let manualSharedObject = manualAssetToConfig.get(childAsset);
367
+ let manualSharedBundleKey: string | undefined;
368
+ let manualSharedObject:
369
+ | ResolvedBundlerConfig['manualSharedBundles'][number]
370
+ | undefined = manualAssetToConfig.get(childAsset);
297
371
 
298
372
  if (manualSharedObject) {
299
373
  // MSB Step 2: Generate a key for which to look up this manual bundle with
@@ -312,7 +386,8 @@ export function createIdealGraph(
312
386
  dependency.priority === 'lazy' ||
313
387
  (getFeatureFlag('conditionalBundlingApi') &&
314
388
  node.value.priority === 'conditional') ||
315
- childAsset.bundleBehavior === 'isolated' // An isolated Dependency, or Bundle must contain all assets it needs to load.
389
+ childAsset.bundleBehavior === 'isolated' || // An isolated Dependency, or Bundle must contain all assets it needs to load.
390
+ childAsset.bundleBehavior === 'inlineIsolated'
316
391
  ) {
317
392
  if (bundleId == null) {
318
393
  let firstBundleGroup = nullthrows(
@@ -325,7 +400,9 @@ export function createIdealGraph(
325
400
  dependency.bundleBehavior ?? childAsset.bundleBehavior,
326
401
  needsStableName:
327
402
  dependency.bundleBehavior === 'inline' ||
328
- childAsset.bundleBehavior === 'inline'
403
+ childAsset.bundleBehavior === 'inline' ||
404
+ dependency.bundleBehavior === 'inlineIsolated' ||
405
+ childAsset.bundleBehavior === 'inlineIsolated'
329
406
  ? false
330
407
  : dependency.isEntry || dependency.needsStableName,
331
408
  target: firstBundleGroup.target,
@@ -335,6 +412,14 @@ export function createIdealGraph(
335
412
  bundleRoots.set(childAsset, [bundleId, bundleId]);
336
413
  bundleGroupBundleIds.add(bundleId);
337
414
  bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
415
+ // If this asset is relevant for merging then track it's source
416
+ // bundle id for later
417
+ if (mergeSourceBundleAssets.has(childAsset.filePath)) {
418
+ mergeSourceBundleLookup.set(
419
+ path.relative(config.projectRoot, childAsset.filePath),
420
+ bundleId,
421
+ );
422
+ }
338
423
  if (manualSharedObject) {
339
424
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
340
425
  // since MSBs should not have main entry assets
@@ -349,7 +434,8 @@ export function createIdealGraph(
349
434
  if (
350
435
  // If this dependency requests isolated, but the bundle is not,
351
436
  // make the bundle isolated for all uses.
352
- dependency.bundleBehavior === 'isolated' &&
437
+ (dependency.bundleBehavior === 'isolated' ||
438
+ dependency.bundleBehavior === 'inlineIsolated') &&
353
439
  bundle.bundleBehavior == null
354
440
  ) {
355
441
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -371,7 +457,9 @@ export function createIdealGraph(
371
457
  type: 'bundle',
372
458
  },
373
459
  ),
374
- dependencyPriorityEdges[dependency.priority],
460
+ dependencyPriorityEdges[
461
+ dependency.priority as keyof typeof dependencyPriorityEdges
462
+ ],
375
463
  );
376
464
 
377
465
  if (
@@ -432,6 +520,7 @@ export function createIdealGraph(
432
520
  needsStableName:
433
521
  childAsset.bundleBehavior === 'inline' ||
434
522
  dependency.bundleBehavior === 'inline' ||
523
+ dependency.bundleBehavior === 'inlineIsolated' ||
435
524
  (dependency.priority === 'parallel' &&
436
525
  !dependency.needsStableName)
437
526
  ? false
@@ -439,13 +528,15 @@ export function createIdealGraph(
439
528
  });
440
529
  bundleId = bundleGraph.addNode(bundle);
441
530
  } else {
442
- bundle = bundleGraph.getNode(bundleId);
443
- invariant(bundle != null && bundle !== 'root');
531
+ let bundleNode = bundleGraph.getNode(bundleId);
532
+ invariant(bundleNode != null && bundleNode !== 'root');
444
533
 
534
+ bundle = bundleNode;
445
535
  if (
446
536
  // If this dependency requests isolated, but the bundle is not,
447
537
  // make the bundle isolated for all uses.
448
- dependency.bundleBehavior === 'isolated' &&
538
+ (dependency.bundleBehavior === 'isolated' ||
539
+ dependency.bundleBehavior === 'inlineIsolated') &&
449
540
  bundle.bundleBehavior == null
450
541
  ) {
451
542
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -515,7 +606,7 @@ export function createIdealGraph(
515
606
  }
516
607
  return node;
517
608
  },
518
- exit(node) {
609
+ exit(node: BundleGraphTraversable): undefined {
519
610
  if (stack[stack.length - 1]?.[0] === node.value) {
520
611
  stack.pop();
521
612
  }
@@ -564,22 +655,22 @@ export function createIdealGraph(
564
655
 
565
656
  // reachableRoots is an array of bit sets for each asset. Each bit set
566
657
  // indicates which bundle roots are reachable from that asset synchronously.
567
- let reachableRoots = [];
658
+ let reachableRoots: Array<BitSet> = [];
568
659
  for (let i = 0; i < assets.length; i++) {
569
660
  reachableRoots.push(new BitSet(bundleRootGraph.nodes.length));
570
661
  }
571
662
 
572
663
  // reachableAssets is the inverse mapping of reachableRoots. For each bundle root,
573
664
  // it contains a bit set that indicates which assets are reachable from it.
574
- let reachableAssets = [];
665
+ let reachableAssets: Array<BitSet> = [];
575
666
 
576
667
  // ancestorAssets maps bundle roots to the set of all assets available to it at runtime,
577
668
  // including in earlier parallel bundles. These are intersected through all paths to
578
669
  // the bundle to ensure that the available assets are always present no matter in which
579
670
  // order the bundles are loaded.
580
- let ancestorAssets = [];
671
+ let ancestorAssets: Array<null | BitSet> = [];
581
672
 
582
- let inlineConstantDeps = new DefaultMap(() => new Set());
673
+ let inlineConstantDeps = new DefaultMap<Asset, Set<Asset>>(() => new Set());
583
674
 
584
675
  for (let [bundleRootId, assetId] of bundleRootGraph.nodes.entries()) {
585
676
  let reachable = new BitSet(assets.length);
@@ -688,7 +779,10 @@ export function createIdealGraph(
688
779
  // not true that a bundle's available assets = all assets of all the bundleGroups
689
780
  // it belongs to. It's the intersection of those sets.
690
781
  let available;
691
- if (bundleRoot.bundleBehavior === 'isolated') {
782
+ if (
783
+ bundleRoot.bundleBehavior === 'isolated' ||
784
+ bundleRoot.bundleBehavior === 'inlineIsolated'
785
+ ) {
692
786
  available = new BitSet(assets.length);
693
787
  } else {
694
788
  available = nullthrows(ancestorAssets[nodeId]).clone();
@@ -772,7 +866,8 @@ export function createIdealGraph(
772
866
 
773
867
  let parentRoots = bundleRootGraph.getNodeIdsConnectedTo(id, ALL_EDGE_TYPES);
774
868
  let canDelete =
775
- getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated';
869
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated' &&
870
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'inlineIsolated';
776
871
  if (parentRoots.length === 0) continue;
777
872
  for (let parentId of parentRoots) {
778
873
  if (parentId === rootNodeId) {
@@ -820,8 +915,10 @@ export function createIdealGraph(
820
915
  let reachableNonEntries = new BitSet(assets.length);
821
916
  let reachableIntersection = new BitSet(assets.length);
822
917
  for (let i = 0; i < assets.length; i++) {
823
- let asset = assets[i];
824
- let manualSharedObject = manualAssetToConfig.get(asset);
918
+ let asset: Asset = assets[i];
919
+ let manualSharedObject:
920
+ | ResolvedBundlerConfig['manualSharedBundles'][number]
921
+ | undefined = manualAssetToConfig.get(asset);
825
922
 
826
923
  if (bundleRoots.has(asset) && inlineConstantDeps.get(asset).size > 0) {
827
924
  let entryBundleId = nullthrows(bundleRoots.get(asset))[0];
@@ -849,7 +946,8 @@ export function createIdealGraph(
849
946
  !a.isBundleSplittable ||
850
947
  (bundleRoots.get(a) &&
851
948
  (getBundleFromBundleRoot(a).needsStableName ||
852
- getBundleFromBundleRoot(a).bundleBehavior === 'isolated'))
949
+ getBundleFromBundleRoot(a).bundleBehavior === 'isolated' ||
950
+ getBundleFromBundleRoot(a).bundleBehavior === 'inlineIsolated'))
853
951
  ) {
854
952
  // Add asset to non-splittable bundles.
855
953
  addAssetToBundleRoot(asset, a);
@@ -864,10 +962,11 @@ export function createIdealGraph(
864
962
 
865
963
  // If we encounter a "manual" asset, draw an edge from reachable to its MSB
866
964
  if (manualSharedObject && !reachable.empty()) {
867
- let bundle;
868
- let bundleId;
869
- let manualSharedBundleKey = manualSharedObject.name + ',' + asset.type;
870
- let sourceBundles = [];
965
+ let bundle: Bundle | undefined;
966
+ let bundleId: NodeId | undefined;
967
+ let manualSharedBundleKey: string =
968
+ manualSharedObject.name + ',' + asset.type;
969
+ let sourceBundles: Array<NodeId> = [];
871
970
  reachable.forEach((id) => {
872
971
  sourceBundles.push(nullthrows(bundleRoots.get(assets[id]))[0]);
873
972
  });
@@ -891,12 +990,14 @@ export function createIdealGraph(
891
990
  manualSharedMap.set(manualSharedBundleKey, bundleId);
892
991
  } else {
893
992
  bundleId = nullthrows(manualSharedMap.get(manualSharedBundleKey));
894
- bundle = nullthrows(bundleGraph.getNode(bundleId));
993
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
895
994
  invariant(
896
- bundle != null && bundle !== 'root',
995
+ bundleNode != null && bundleNode !== 'root',
897
996
  'We tried to use the root incorrectly',
898
997
  );
899
998
 
999
+ bundle = bundleNode;
1000
+
900
1001
  if (!bundle.assets.has(asset)) {
901
1002
  bundle.assets.add(asset);
902
1003
  bundle.size += asset.stats.size;
@@ -982,7 +1083,7 @@ export function createIdealGraph(
982
1083
  });
983
1084
  }
984
1085
 
985
- let reachableArray = [];
1086
+ let reachableArray: Array<Asset> = [];
986
1087
  reachable.forEach((id) => {
987
1088
  reachableArray.push(assets[id]);
988
1089
  });
@@ -992,12 +1093,13 @@ export function createIdealGraph(
992
1093
  config.disableSharedBundles === false &&
993
1094
  reachableArray.length > config.minBundles
994
1095
  ) {
995
- let sourceBundles = reachableArray.map(
1096
+ let sourceBundles: Array<NodeId> = reachableArray.map(
996
1097
  (a) => nullthrows(bundleRoots.get(a))[0],
997
1098
  );
998
- let key = reachableArray.map((a) => a.id).join(',') + '.' + asset.type;
999
- let bundleId = bundles.get(key);
1000
- let bundle;
1099
+ let key: string =
1100
+ reachableArray.map((a) => a.id).join(',') + '.' + asset.type;
1101
+ let bundleId: NodeId | undefined = bundles.get(key);
1102
+ let bundle: Bundle | undefined;
1001
1103
  if (bundleId == null) {
1002
1104
  let firstSourceBundle = nullthrows(
1003
1105
  bundleGraph.getNode(sourceBundles[0]),
@@ -1028,8 +1130,9 @@ export function createIdealGraph(
1028
1130
  bundleId = bundleGraph.addNode(bundle);
1029
1131
  bundles.set(key, bundleId);
1030
1132
  } else {
1031
- bundle = nullthrows(bundleGraph.getNode(bundleId));
1032
- invariant(bundle !== 'root');
1133
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
1134
+ invariant(bundleNode !== 'root');
1135
+ bundle = bundleNode;
1033
1136
  }
1034
1137
  bundle.assets.add(asset);
1035
1138
  bundle.size += asset.stats.size;
@@ -1057,25 +1160,31 @@ export function createIdealGraph(
1057
1160
  }
1058
1161
 
1059
1162
  let manualSharedBundleIds = new Set([...manualSharedMap.values()]);
1163
+ let modifiedSourceBundles = new Set<Bundle>();
1164
+
1060
1165
  // Step split manual shared bundles for those that have the "split" property set
1061
- let remainderMap = new DefaultMap(() => []);
1166
+ let remainderMap = new DefaultMap<number, Array<Asset>>(() => []);
1062
1167
  for (let id of manualSharedMap.values()) {
1063
1168
  let manualBundle = bundleGraph.getNode(id);
1064
1169
  invariant(manualBundle !== 'root' && manualBundle != null);
1065
1170
 
1066
1171
  if (manualBundle.sourceBundles.size > 0) {
1067
- let firstSourceBundle = nullthrows(
1172
+ let firstSourceBundleNode: Bundle | 'root' = nullthrows(
1068
1173
  bundleGraph.getNode([...manualBundle.sourceBundles][0]),
1069
1174
  );
1070
- invariant(firstSourceBundle !== 'root');
1071
- let firstAsset = [...manualBundle.assets][0];
1072
- let manualSharedObject = manualAssetToConfig.get(firstAsset);
1175
+ invariant(firstSourceBundleNode !== 'root');
1176
+ let firstSourceBundle = firstSourceBundleNode;
1177
+
1178
+ let firstAsset: Asset = [...manualBundle.assets][0];
1179
+ let manualSharedObject:
1180
+ | ResolvedBundlerConfig['manualSharedBundles'][number]
1181
+ | undefined = manualAssetToConfig.get(firstAsset);
1073
1182
  invariant(manualSharedObject != null);
1074
- let modNum = manualAssetToConfig.get(firstAsset)?.split;
1183
+ let modNum: number | undefined =
1184
+ manualAssetToConfig.get(firstAsset)?.split;
1075
1185
  if (modNum != null) {
1076
1186
  for (let a of [...manualBundle.assets]) {
1077
1187
  let numRep = getBigIntFromContentKey(a.id);
1078
- // $FlowFixMe Flow doesn't know about BigInt
1079
1188
  let r = Number(numRep % BigInt(modNum));
1080
1189
 
1081
1190
  remainderMap.get(r).push(a);
@@ -1129,10 +1238,87 @@ export function createIdealGraph(
1129
1238
  }
1130
1239
  }
1131
1240
 
1132
- // Step merge shared bundles that meet the overlap threshold
1133
- // This step is skipped by default as the threshold defaults to 1
1134
- if (config.sharedBundleMergeThreshold < 1) {
1135
- mergeOverlapBundles();
1241
+ if (getFeatureFlag('supportWebpackChunkName')) {
1242
+ // Merge webpack chunk name bundles
1243
+ let chunkNameBundles = new DefaultMap<string, Set<NodeId>>(() => new Set());
1244
+ for (let [nodeId, node] of dependencyBundleGraph.nodes.entries()) {
1245
+ // meta.chunkName is set by the Rust transformer, so we just need to find
1246
+ // bundles that have a chunkName set.
1247
+ if (
1248
+ !node ||
1249
+ node.type !== 'dependency' ||
1250
+ typeof node.value.meta.chunkName !== 'string'
1251
+ ) {
1252
+ continue;
1253
+ }
1254
+
1255
+ let connectedBundles = dependencyBundleGraph.getNodeIdsConnectedFrom(
1256
+ nodeId,
1257
+ dependencyPriorityEdges[node.value.priority],
1258
+ );
1259
+
1260
+ if (connectedBundles.length === 0) {
1261
+ continue;
1262
+ }
1263
+
1264
+ invariant(
1265
+ connectedBundles.length === 1,
1266
+ 'Expected webpackChunkName dependency to be connected to no more than one bundle',
1267
+ );
1268
+
1269
+ let bundleId = connectedBundles[0];
1270
+ let bundleNode = dependencyBundleGraph.getNode(bundleId);
1271
+ invariant(bundleNode != null && bundleNode.type === 'bundle');
1272
+
1273
+ // If a bundle does not have a main entry asset, it's somehow just a
1274
+ // shared bundle, and will be merged/deleted by other means.
1275
+ if (bundleNode.value.mainEntryAsset == null) {
1276
+ continue;
1277
+ }
1278
+
1279
+ let bundleNodeId = null;
1280
+ let mainEntryAssetId = bundleNode.value.mainEntryAsset?.id;
1281
+
1282
+ if (mainEntryAssetId != null) {
1283
+ bundleNodeId = bundles.get(mainEntryAssetId);
1284
+ }
1285
+
1286
+ if (bundleNodeId == null) {
1287
+ continue;
1288
+ }
1289
+
1290
+ chunkNameBundles
1291
+ .get(node.value.meta.chunkName)
1292
+ // DependencyBundleGraph uses content keys as node ids, so we can use that
1293
+ // to get the bundle id.
1294
+ .add(bundleNodeId);
1295
+ }
1296
+
1297
+ for (let [chunkName, bundleIds] of chunkNameBundles.entries()) {
1298
+ // The `[request]` placeholder is not yet supported
1299
+ if (
1300
+ bundleIds.size <= 1 ||
1301
+ (typeof chunkName === 'string' && chunkName.includes('[request]'))
1302
+ ) {
1303
+ continue; // Nothing to merge
1304
+ }
1305
+
1306
+ // Merge all bundles with the same chunk name into the first one.
1307
+ let [firstBundleId, ...rest] = Array.from(bundleIds);
1308
+ for (let bundleId of rest) {
1309
+ mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
1310
+ }
1311
+ }
1312
+ }
1313
+
1314
+ // Step merge async bundles that meet the configured params
1315
+ if (config.asyncBundleMerge) {
1316
+ mergeAsyncBundles(config.asyncBundleMerge);
1317
+ }
1318
+
1319
+ // Step merge shared bundles that meet the configured params
1320
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
1321
+ mergeSharedBundles(config.sharedBundleMerge);
1136
1322
  }
1137
1323
 
1138
1324
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -1151,7 +1337,6 @@ export function createIdealGraph(
1151
1337
  }
1152
1338
 
1153
1339
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1154
- let modifiedSourceBundles = new Set();
1155
1340
 
1156
1341
  if (config.disableSharedBundles === false) {
1157
1342
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
@@ -1165,7 +1350,13 @@ export function createIdealGraph(
1165
1350
  let numBundlesContributingToPRL = bundleIdsInGroup.reduce((count, b) => {
1166
1351
  let bundle = nullthrows(bundleGraph.getNode(b));
1167
1352
  invariant(bundle !== 'root');
1168
- return count + (bundle.bundleBehavior !== 'inline');
1353
+ return (
1354
+ count +
1355
+ Number(
1356
+ bundle.bundleBehavior !== 'inline' &&
1357
+ bundle.bundleBehavior !== 'inlineIsolated',
1358
+ )
1359
+ );
1169
1360
  }, 0);
1170
1361
 
1171
1362
  if (numBundlesContributingToPRL > config.maxParallelRequests) {
@@ -1200,6 +1391,7 @@ export function createIdealGraph(
1200
1391
  numBundlesContributingToPRL > config.maxParallelRequests
1201
1392
  ) {
1202
1393
  let bundleTuple = sharedBundlesInGroup.pop();
1394
+ if (!bundleTuple) break;
1203
1395
  let bundleToRemove = bundleTuple.bundle;
1204
1396
  let bundleIdToRemove = bundleTuple.id;
1205
1397
  //TODO add integration test where bundles in bunlde group > max parallel request limit & only remove a couple shared bundles
@@ -1257,51 +1449,205 @@ export function createIdealGraph(
1257
1449
  }
1258
1450
 
1259
1451
  function mergeBundles(
1260
- bundleGraph: IdealBundleGraph,
1261
1452
  bundleToKeepId: NodeId,
1262
1453
  bundleToRemoveId: NodeId,
1263
- assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1454
+ reason: string,
1264
1455
  ) {
1265
- let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
1266
- let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
1267
- invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1456
+ stats.trackMerge(bundleToKeepId, bundleToRemoveId, reason);
1457
+ let bundleToKeep = isNonRootBundle(
1458
+ bundleGraph.getNode(bundleToKeepId),
1459
+ `Bundle ${bundleToKeepId} not found`,
1460
+ );
1461
+ let bundleToRemove = isNonRootBundle(
1462
+ bundleGraph.getNode(bundleToRemoveId),
1463
+ `Bundle ${bundleToRemoveId} not found`,
1464
+ );
1465
+ modifiedSourceBundles.add(bundleToKeep);
1268
1466
  for (let asset of bundleToRemove.assets) {
1269
1467
  bundleToKeep.assets.add(asset);
1270
1468
  bundleToKeep.size += asset.stats.size;
1271
1469
 
1272
1470
  let newAssetReference = assetReference
1273
1471
  .get(asset)
1274
- .map(([dep, bundle]) =>
1472
+ .map(([dep, bundle]): [Dependency, Bundle] =>
1275
1473
  bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1276
1474
  );
1277
1475
 
1278
1476
  assetReference.set(asset, newAssetReference);
1279
1477
  }
1280
1478
 
1479
+ // Merge any internalized assets
1480
+ if (getFeatureFlag('supportWebpackChunkName')) {
1481
+ if (bundleToKeep.internalizedAssets != null) {
1482
+ if (bundleToRemove.internalizedAssets != null) {
1483
+ bundleToKeep.internalizedAssets.intersect(
1484
+ bundleToRemove.internalizedAssets,
1485
+ );
1486
+ } else {
1487
+ bundleToKeep.internalizedAssets.clear();
1488
+ }
1489
+ }
1490
+ } else {
1491
+ invariant(
1492
+ bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets,
1493
+ 'All shared bundles should have internalized assets',
1494
+ );
1495
+ bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
1496
+ }
1497
+
1498
+ // Merge and clean up source bundles
1281
1499
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
1282
1500
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1283
1501
  continue;
1284
1502
  }
1285
1503
 
1286
- bundleToKeep.sourceBundles.add(sourceBundleId);
1287
- bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1504
+ if (sourceBundleId !== bundleToKeepId) {
1505
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1506
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1507
+ }
1288
1508
  }
1289
1509
 
1290
- // Merge any internalized assets
1291
- if (bundleToRemove.internalizedAssets) {
1292
- if (bundleToKeep.internalizedAssets) {
1293
- bundleToKeep.internalizedAssets.union(
1294
- bundleToRemove.internalizedAssets,
1510
+ if (getFeatureFlag('supportWebpackChunkName')) {
1511
+ bundleToKeep.sourceBundles.delete(bundleToRemoveId);
1512
+
1513
+ for (let bundle of bundleGraph.getNodeIdsConnectedFrom(
1514
+ bundleToRemoveId,
1515
+ )) {
1516
+ let bundleNode = nullthrows(bundleGraph.getNode(bundle));
1517
+ if (bundleNode === 'root') {
1518
+ continue;
1519
+ }
1520
+
1521
+ // If the bundle is a source bundle, add it to the bundle to keep
1522
+ if (bundleNode.sourceBundles.has(bundleToRemoveId)) {
1523
+ bundleNode.sourceBundles.delete(bundleToRemoveId);
1524
+ if (bundle !== bundleToKeepId) {
1525
+ bundleNode.sourceBundles.add(bundleToKeepId);
1526
+ bundleGraph.addEdge(bundleToKeepId, bundle);
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ // Merge bundle roots
1532
+ for (let bundleRoot of bundleToRemove.bundleRoots) {
1533
+ bundleToKeep.bundleRoots.add(bundleRoot);
1534
+ }
1535
+
1536
+ if (bundleToRemove.mainEntryAsset != null) {
1537
+ invariant(bundleToKeep.mainEntryAsset != null);
1538
+
1539
+ // Merge the bundles in bundle group
1540
+ let bundlesInRemoveBundleGroup =
1541
+ getBundlesForBundleGroup(bundleToRemoveId);
1542
+
1543
+ let removedBundleSharedBundles = new Set<NodeId>();
1544
+ for (let bundleIdInGroup of bundlesInRemoveBundleGroup) {
1545
+ if (bundleIdInGroup === bundleToRemoveId) {
1546
+ continue;
1547
+ }
1548
+ bundleGraph.addEdge(bundleToKeepId, bundleIdInGroup);
1549
+ removedBundleSharedBundles.add(bundleIdInGroup);
1550
+ }
1551
+
1552
+ if (getFeatureFlag('removeRedundantSharedBundles')) {
1553
+ // Merge any shared bundles that now have the same source bundles due to
1554
+ // the current bundle merge
1555
+ let sharedBundles = new DefaultMap<string, Array<NodeId>>(() => []);
1556
+ for (let bundleId of removedBundleSharedBundles) {
1557
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
1558
+ invariant(bundleNode !== 'root');
1559
+ if (
1560
+ bundleNode.mainEntryAsset != null ||
1561
+ bundleNode.manualSharedBundle != null
1562
+ ) {
1563
+ continue;
1564
+ }
1565
+
1566
+ let key =
1567
+ Array.from(bundleNode.sourceBundles)
1568
+ .filter((sourceBundle) => sourceBundle !== bundleToRemoveId)
1569
+ .sort()
1570
+ .join(',') +
1571
+ '.' +
1572
+ bundleNode.type;
1573
+
1574
+ sharedBundles.get(key).push(bundleId);
1575
+ }
1576
+
1577
+ for (let sharedBundlesToMerge of sharedBundles.values()) {
1578
+ if (sharedBundlesToMerge.length > 1) {
1579
+ let [firstBundleId, ...rest] = sharedBundlesToMerge;
1580
+ for (let bundleId of rest) {
1581
+ mergeBundles(firstBundleId, bundleId, 'redundant-shared');
1582
+ }
1583
+ }
1584
+ }
1585
+ }
1586
+
1587
+ // Remove old bundle group
1588
+ bundleGroupBundleIds.delete(bundleToRemoveId);
1589
+
1590
+ // Clean up bundle roots
1591
+ let bundleRootToRemoveNodeId = nullthrows(
1592
+ assetToBundleRootNodeId.get(
1593
+ nullthrows(bundleToRemove.mainEntryAsset),
1594
+ ),
1295
1595
  );
1296
- } else {
1297
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
1596
+ let bundleRootToKeepNodeId = nullthrows(
1597
+ assetToBundleRootNodeId.get(nullthrows(bundleToKeep.mainEntryAsset)),
1598
+ );
1599
+
1600
+ for (let nodeId of bundleRootGraph.getNodeIdsConnectedTo(
1601
+ bundleRootToRemoveNodeId,
1602
+ )) {
1603
+ bundleRootGraph.addEdge(nodeId, bundleRootToKeepNodeId);
1604
+ bundleRootGraph.removeEdge(nodeId, bundleRootToRemoveNodeId);
1605
+ }
1606
+
1607
+ for (let nodeId of bundleRootGraph.getNodeIdsConnectedFrom(
1608
+ bundleRootToRemoveNodeId,
1609
+ )) {
1610
+ bundleRootGraph.addEdge(bundleRootToKeepNodeId, nodeId);
1611
+ bundleRootGraph.removeEdge(bundleRootToRemoveNodeId, nodeId);
1612
+ }
1613
+
1614
+ bundleRoots.set(nullthrows(bundleToRemove.mainEntryAsset), [
1615
+ bundleToKeepId,
1616
+ bundleToKeepId,
1617
+ ]);
1618
+
1619
+ // Merge dependency bundle graph
1620
+ for (let dependencyNodeId of dependencyBundleGraph.getNodeIdsConnectedTo(
1621
+ dependencyBundleGraph.getNodeIdByContentKey(String(bundleToRemoveId)),
1622
+ ALL_EDGE_TYPES,
1623
+ )) {
1624
+ let dependencyNode = nullthrows(
1625
+ dependencyBundleGraph.getNode(dependencyNodeId),
1626
+ );
1627
+ invariant(dependencyNode.type === 'dependency');
1628
+
1629
+ // Add dependency to the bundle to keep
1630
+ dependencyBundleGraph.addEdge(
1631
+ dependencyNodeId,
1632
+ dependencyBundleGraph.getNodeIdByContentKey(String(bundleToKeepId)),
1633
+ dependencyPriorityEdges[dependencyNode.value.priority],
1634
+ );
1635
+ // Remove dependency from the bundle to remove
1636
+ dependencyBundleGraph.removeEdge(
1637
+ dependencyNodeId,
1638
+ dependencyBundleGraph.getNodeIdByContentKey(
1639
+ String(bundleToRemoveId),
1640
+ ),
1641
+ dependencyPriorityEdges[dependencyNode.value.priority],
1642
+ );
1643
+ }
1298
1644
  }
1299
1645
  }
1300
1646
 
1301
1647
  bundleGraph.removeNode(bundleToRemoveId);
1302
1648
  }
1303
1649
 
1304
- function mergeOverlapBundles() {
1650
+ function mergeSharedBundles(mergeConfig: SharedBundleMergeCandidates) {
1305
1651
  // Find all shared bundles
1306
1652
  let sharedBundles = new Set<NodeId>();
1307
1653
  bundleGraph.traverse((nodeId) => {
@@ -1331,22 +1677,188 @@ export function createIdealGraph(
1331
1677
  let clusters = findMergeCandidates(
1332
1678
  bundleGraph,
1333
1679
  Array.from(sharedBundles),
1334
- config.sharedBundleMergeThreshold,
1680
+ mergeConfig.map(
1681
+ (config): MergeGroup => ({
1682
+ ...config,
1683
+ sourceBundles: config.sourceBundles?.map((assetMatch: string) => {
1684
+ let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
1685
+
1686
+ if (sourceBundleNodeId == null) {
1687
+ throw new Error(
1688
+ `Source bundle ${assetMatch} not found in merge source bundle lookup`,
1689
+ );
1690
+ }
1691
+
1692
+ return sourceBundleNodeId;
1693
+ }),
1694
+ }),
1695
+ ),
1335
1696
  );
1336
1697
 
1698
+ let mergedBundles = new Set();
1699
+
1337
1700
  for (let cluster of clusters) {
1338
1701
  let [mergeTarget, ...rest] = cluster;
1339
1702
 
1340
1703
  for (let bundleIdToMerge of rest) {
1341
- mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1704
+ mergeBundles(mergeTarget, bundleIdToMerge, 'shared-merge');
1342
1705
  }
1706
+
1707
+ mergedBundles.add(mergeTarget);
1708
+ }
1709
+
1710
+ if (getFeatureFlag('supportWebpackChunkName')) {
1711
+ return mergedBundles;
1343
1712
  }
1344
1713
  }
1345
1714
 
1346
- function getBigIntFromContentKey(contentKey) {
1715
+ function mergeAsyncBundles({
1716
+ bundleSize,
1717
+ maxOverfetchSize,
1718
+ ignore,
1719
+ }: AsyncBundleMerge) {
1720
+ let mergeCandidates = [];
1721
+ let ignoreRegexes = ignore?.map((glob) => globToRegex(glob)) ?? [];
1722
+
1723
+ let isIgnored = (bundle: Bundle) => {
1724
+ if (!bundle.mainEntryAsset) {
1725
+ return false;
1726
+ }
1727
+
1728
+ let mainEntryFilePath = path.relative(
1729
+ config.projectRoot,
1730
+ nullthrows(bundle.mainEntryAsset).filePath,
1731
+ );
1732
+
1733
+ return ignoreRegexes.some((regex) => regex.test(mainEntryFilePath));
1734
+ };
1735
+
1736
+ for (let [_bundleRootAsset, [bundleRootBundleId]] of bundleRoots) {
1737
+ let bundleRootBundle = nullthrows(
1738
+ bundleGraph.getNode(bundleRootBundleId),
1739
+ );
1740
+ invariant(bundleRootBundle !== 'root');
1741
+
1742
+ if (
1743
+ bundleRootBundle.type === 'js' &&
1744
+ bundleRootBundle.bundleBehavior !== 'inline' &&
1745
+ bundleRootBundle.bundleBehavior !== 'inlineIsolated' &&
1746
+ bundleRootBundle.size <= bundleSize &&
1747
+ !isIgnored(bundleRootBundle)
1748
+ ) {
1749
+ mergeCandidates.push(bundleRootBundleId);
1750
+ }
1751
+ }
1752
+
1753
+ let candidates = [];
1754
+ for (let i = 0; i < mergeCandidates.length; i++) {
1755
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
1756
+ let a = mergeCandidates[i];
1757
+ let b = mergeCandidates[j];
1758
+ if (a === b) continue; // Skip self-comparison
1759
+
1760
+ candidates.push(scoreAsyncMerge(a, b, maxOverfetchSize));
1761
+ }
1762
+ }
1763
+
1764
+ let sortByScore = (
1765
+ a: AsyncBundleMergeCandidate,
1766
+ b: AsyncBundleMergeCandidate,
1767
+ ) => {
1768
+ let diff = a.score - b.score;
1769
+ if (diff > 0) {
1770
+ return 1;
1771
+ } else if (diff < 0) {
1772
+ return -1;
1773
+ }
1774
+ return 0;
1775
+ };
1776
+
1777
+ candidates = candidates
1778
+ .filter(
1779
+ ({overfetchSize, score}) =>
1780
+ overfetchSize <= maxOverfetchSize && score > 0,
1781
+ )
1782
+ .sort(sortByScore);
1783
+
1784
+ // Tracks the bundles that have been merged
1785
+ let merged = new Set<NodeId>();
1786
+ // Tracks the deleted bundles to the bundle they were merged into.
1787
+ let mergeRemap = new Map<NodeId, NodeId>();
1788
+ // Tracks the bundles that have been rescored and added back into the
1789
+ // candidates.
1790
+ let rescored = new DefaultMap<NodeId, Set<NodeId>>(() => new Set());
1791
+
1792
+ do {
1793
+ let [a, b] = nullthrows(candidates.pop()).bundleIds;
1794
+
1795
+ if (
1796
+ bundleGraph.hasNode(a) &&
1797
+ bundleGraph.hasNode(b) &&
1798
+ ((!merged.has(a) && !merged.has(b)) || rescored.get(a).has(b))
1799
+ ) {
1800
+ mergeRemap.set(b, a);
1801
+ merged.add(a);
1802
+ rescored.get(a).clear();
1803
+
1804
+ mergeBundles(a, b, 'async-merge');
1805
+ continue;
1806
+ }
1807
+
1808
+ // One or both of the bundles have been previously merged, so we'll
1809
+ // rescore and add the result back into the list of candidates.
1810
+ let getMergedBundleId = (bundleId: NodeId): NodeId | undefined => {
1811
+ let seen = new Set<NodeId>();
1812
+ while (!bundleGraph.hasNode(bundleId) && !seen.has(bundleId)) {
1813
+ seen.add(bundleId);
1814
+ bundleId = nullthrows(mergeRemap.get(bundleId));
1815
+ }
1816
+
1817
+ if (!bundleGraph.hasNode(bundleId)) {
1818
+ return;
1819
+ }
1820
+
1821
+ return bundleId;
1822
+ };
1823
+
1824
+ // Map a and b to their merged bundle ids if they've already been merged
1825
+ let currentA = getMergedBundleId(a);
1826
+ let currentB = getMergedBundleId(b);
1827
+
1828
+ if (
1829
+ !currentA ||
1830
+ !currentB ||
1831
+ // Bundles are already merged
1832
+ currentA === currentB
1833
+ ) {
1834
+ // This combiniation is not valid, so we skip it.
1835
+ continue;
1836
+ }
1837
+
1838
+ let candidate = scoreAsyncMerge(currentA, currentB, maxOverfetchSize);
1839
+
1840
+ if (candidate.overfetchSize <= maxOverfetchSize && candidate.score > 0) {
1841
+ sortedArray.add(candidates, candidate, sortByScore);
1842
+ rescored.get(currentA).add(currentB);
1843
+ }
1844
+ } while (candidates.length > 0);
1845
+ }
1846
+
1847
+ function getBundle(bundleId: NodeId): Bundle {
1848
+ let bundle = bundleGraph.getNode(bundleId);
1849
+ if (bundle === 'root') {
1850
+ throw new Error(`Cannot access root bundle`);
1851
+ }
1852
+ if (bundle == null) {
1853
+ throw new Error(`Bundle ${bundleId} not found in bundle graph`);
1854
+ }
1855
+
1856
+ return bundle;
1857
+ }
1858
+
1859
+ function getBigIntFromContentKey(contentKey: string) {
1347
1860
  let b = Buffer.alloc(64);
1348
1861
  b.write(contentKey);
1349
- // $FlowFixMe Flow doesn't have BigInt types in this version
1350
1862
  return b.readBigInt64BE();
1351
1863
  }
1352
1864
  // Fix asset order in source bundles as they are likely now incorrect after shared bundle deletion
@@ -1373,14 +1885,86 @@ export function createIdealGraph(
1373
1885
  bundleRootGraph.removeNode(bundleRootId);
1374
1886
  }
1375
1887
  }
1376
- function getBundlesForBundleGroup(bundleGroupId) {
1377
- let bundlesInABundleGroup = [];
1888
+ function getBundlesForBundleGroup(bundleGroupId: NodeId) {
1889
+ let bundlesInABundleGroup: Array<NodeId> = [];
1378
1890
  bundleGraph.traverse((nodeId) => {
1379
1891
  bundlesInABundleGroup.push(nodeId);
1380
1892
  }, bundleGroupId);
1381
1893
  return bundlesInABundleGroup;
1382
1894
  }
1383
1895
 
1896
+ interface AsyncBundleMergeCandidate {
1897
+ overfetchSize: number;
1898
+ score: number;
1899
+ bundleIds: number[];
1900
+ }
1901
+ function scoreAsyncMerge(
1902
+ bundleAId: NodeId,
1903
+ bundleBId: NodeId,
1904
+ maxOverfetchSize: number,
1905
+ ): AsyncBundleMergeCandidate {
1906
+ let bundleGroupA = new Set(getBundlesForBundleGroup(bundleAId));
1907
+ let bundleGroupB = new Set(getBundlesForBundleGroup(bundleBId));
1908
+
1909
+ let overlapSize = 0;
1910
+ let overfetchSize = 0;
1911
+
1912
+ for (let bundleId of new Set([...bundleGroupA, ...bundleGroupB])) {
1913
+ let bundle = getBundle(bundleId);
1914
+
1915
+ if (bundleGroupA.has(bundleId) && bundleGroupB.has(bundleId)) {
1916
+ overlapSize += bundle.size;
1917
+ } else {
1918
+ overfetchSize += bundle.size;
1919
+ }
1920
+ }
1921
+
1922
+ let overlapPercent = overlapSize / (overfetchSize + overlapSize);
1923
+
1924
+ let bundleAParents = getBundleParents(bundleAId);
1925
+ let bundleBParents = getBundleParents(bundleBId);
1926
+
1927
+ let sharedParentOverlap = 0;
1928
+ let sharedParentMismatch = 0;
1929
+
1930
+ for (let bundleId of new Set([...bundleAParents, ...bundleBParents])) {
1931
+ if (bundleAParents.has(bundleId) && bundleBParents.has(bundleId)) {
1932
+ sharedParentOverlap++;
1933
+ } else {
1934
+ sharedParentMismatch++;
1935
+ }
1936
+ }
1937
+
1938
+ let overfetchScore = overfetchSize / maxOverfetchSize;
1939
+ let sharedParentPercent =
1940
+ sharedParentOverlap / (sharedParentOverlap + sharedParentMismatch);
1941
+ let score = sharedParentPercent + overlapPercent + overfetchScore;
1942
+
1943
+ return {
1944
+ overfetchSize,
1945
+ score,
1946
+ bundleIds: [bundleAId, bundleBId],
1947
+ };
1948
+ }
1949
+
1950
+ function getBundleParents(bundleId: NodeId): Set<NodeId> {
1951
+ let parents = new Set<NodeId>();
1952
+ let {bundleRoots} = getBundle(bundleId);
1953
+
1954
+ for (let bundleRoot of bundleRoots) {
1955
+ let bundleRootNodeId = nullthrows(
1956
+ assetToBundleRootNodeId.get(bundleRoot),
1957
+ );
1958
+ for (let parentId of bundleRootGraph.getNodeIdsConnectedTo(
1959
+ bundleRootNodeId,
1960
+ ALL_EDGE_TYPES,
1961
+ )) {
1962
+ parents.add(parentId);
1963
+ }
1964
+ }
1965
+ return parents;
1966
+ }
1967
+
1384
1968
  function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle {
1385
1969
  let bundle = bundleGraph.getNode(
1386
1970
  nullthrows(bundleRoots.get(bundleRoot))[0],
@@ -1445,6 +2029,12 @@ export function createIdealGraph(
1445
2029
  bundleGraph.removeNode(bundleId);
1446
2030
  }
1447
2031
 
2032
+ stats.report((bundleId) => {
2033
+ let bundle = bundleGraph.getNode(bundleId);
2034
+ invariant(bundle !== 'root');
2035
+ return bundle;
2036
+ });
2037
+
1448
2038
  return {
1449
2039
  assets,
1450
2040
  bundleGraph,
@@ -1455,23 +2045,24 @@ export function createIdealGraph(
1455
2045
  };
1456
2046
  }
1457
2047
 
1458
- function createBundle(opts: {|
1459
- asset?: Asset,
1460
- bundleBehavior?: ?BundleBehavior,
1461
- env?: Environment,
1462
- manualSharedBundle?: ?string,
1463
- needsStableName?: boolean,
1464
- sourceBundles?: Set<NodeId>,
1465
- target: Target,
1466
- type?: string,
1467
- uniqueKey?: string,
1468
- |}): Bundle {
2048
+ function createBundle(opts: {
2049
+ asset?: Asset;
2050
+ bundleBehavior?: BundleBehavior | null | undefined;
2051
+ env?: Environment;
2052
+ manualSharedBundle?: string | null | undefined;
2053
+ needsStableName?: boolean;
2054
+ sourceBundles?: Set<NodeId>;
2055
+ target: Target;
2056
+ type?: string;
2057
+ uniqueKey?: string;
2058
+ }): Bundle {
1469
2059
  if (opts.asset == null) {
1470
2060
  return {
1471
2061
  assets: new Set(),
1472
2062
  bundleBehavior: opts.bundleBehavior,
1473
2063
  env: nullthrows(opts.env),
1474
2064
  mainEntryAsset: null,
2065
+ bundleRoots: new Set(),
1475
2066
  manualSharedBundle: opts.manualSharedBundle,
1476
2067
  needsStableName: Boolean(opts.needsStableName),
1477
2068
  size: 0,
@@ -1488,6 +2079,7 @@ function createBundle(opts: {|
1488
2079
  bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior,
1489
2080
  env: opts.env ?? asset.env,
1490
2081
  mainEntryAsset: asset,
2082
+ bundleRoots: new Set([asset]),
1491
2083
  manualSharedBundle: opts.manualSharedBundle,
1492
2084
  needsStableName: Boolean(opts.needsStableName),
1493
2085
  size: asset.stats.size,