@atlaspack/bundler-default 2.14.5-canary.3 → 2.14.5-canary.300

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.
@@ -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,41 +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 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';
27
34
 
28
35
  /* BundleRoot - An asset that is the main entry of a Bundle. */
29
36
  type BundleRoot = Asset;
30
37
 
31
- export type Bundle = {|
32
- uniqueKey: ?string,
33
- assets: Set<Asset>,
34
- internalizedAssets?: BitSet,
35
- bundleBehavior?: ?BundleBehavior,
36
- needsStableName: boolean,
37
- mainEntryAsset: ?Asset,
38
- size: number,
39
- sourceBundles: Set<NodeId>,
40
- target: Target,
41
- env: Environment,
42
- type: string,
43
- manualSharedBundle: ?string, // for naming purposes
44
- |};
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
+ };
45
53
 
46
54
  export type DependencyBundleGraph = ContentGraph<
47
- | {|
48
- value: Bundle,
49
- type: 'bundle',
50
- |}
51
- | {|
52
- value: Dependency,
53
- type: 'dependency',
54
- |},
55
- number,
55
+ | {
56
+ value: Bundle;
57
+ type: 'bundle';
58
+ }
59
+ | {
60
+ value: Dependency;
61
+ type: 'dependency';
62
+ },
63
+ number
56
64
  >;
57
65
 
58
66
  const dependencyPriorityEdges = {
@@ -60,29 +68,39 @@ const dependencyPriorityEdges = {
60
68
  parallel: 2,
61
69
  lazy: 3,
62
70
  conditional: 4,
63
- };
71
+ } as const;
64
72
 
65
73
  export const idealBundleGraphEdges = Object.freeze({
66
74
  default: 1,
67
75
  conditional: 2,
68
76
  });
69
77
 
70
- type IdealBundleGraph = Graph<
78
+ export type IdealBundleGraph = Graph<
71
79
  Bundle | 'root',
72
- $Values<typeof idealBundleGraphEdges>,
80
+ (typeof idealBundleGraphEdges)[keyof typeof idealBundleGraphEdges]
73
81
  >;
74
82
 
75
83
  // IdealGraph is the structure we will pass to decorate,
76
84
  // which mutates the assetGraph into the bundleGraph we would
77
85
  // expect from default bundler
78
- export type IdealGraph = {|
79
- assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
80
- assets: Array<Asset>,
81
- bundleGraph: IdealBundleGraph,
82
- bundleGroupBundleIds: Set<NodeId>,
83
- dependencyBundleGraph: DependencyBundleGraph,
84
- manualAssetToBundle: Map<Asset, NodeId>,
85
- |};
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
+ }
86
104
 
87
105
  export function createIdealGraph(
88
106
  assetGraph: MutableBundleGraph,
@@ -96,8 +114,9 @@ export function createIdealGraph(
96
114
  let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph();
97
115
  let assetReference: DefaultMap<
98
116
  Asset,
99
- Array<[Dependency, Bundle]>,
117
+ Array<[Dependency, Bundle]>
100
118
  > = new DefaultMap(() => []);
119
+ let stats = new Stats(config.projectRoot);
101
120
 
102
121
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
103
122
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -110,8 +129,9 @@ export function createIdealGraph(
110
129
  };
111
130
  // Graph that models bundleRoots, with parallel & async deps only to inform reachability
112
131
  let bundleRootGraph: Graph<
113
- number, // asset index
114
- $Values<typeof bundleRootEdgeTypes>,
132
+ // asset index
133
+ number,
134
+ (typeof bundleRootEdgeTypes)[keyof typeof bundleRootEdgeTypes]
115
135
  > = new Graph();
116
136
  let assetToBundleRootNodeId = new Map<BundleRoot, number>();
117
137
 
@@ -145,26 +165,47 @@ export function createIdealGraph(
145
165
  bundleGroupBundleIds.add(nodeId);
146
166
  }
147
167
 
148
- let assets = [];
168
+ let assets: Array<Asset> = [];
149
169
  let assetToIndex = new Map<Asset, number>();
150
170
 
151
- function makeManualAssetToConfigLookup() {
152
- let manualAssetToConfig = new Map();
153
- 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
+ >(() => []);
154
189
 
155
190
  if (config.manualSharedBundles.length === 0) {
156
191
  return {manualAssetToConfig, constantModuleToMSB};
157
192
  }
158
193
 
159
- let parentsToConfig = new DefaultMap(() => []);
194
+ let parentsToConfig = new DefaultMap<
195
+ string,
196
+ Array<ResolvedBundlerConfig['manualSharedBundles'][number]>
197
+ >(() => []);
160
198
 
161
199
  for (let c of config.manualSharedBundles) {
162
200
  if (c.root != null) {
163
201
  parentsToConfig.get(path.join(config.projectRoot, c.root)).push(c);
164
202
  }
165
203
  }
166
- let numParentsToFind = parentsToConfig.size;
167
- let configToParentAsset = new Map();
204
+ let numParentsToFind: number = parentsToConfig.size;
205
+ let configToParentAsset = new Map<
206
+ ResolvedBundlerConfig['manualSharedBundles'][number],
207
+ Asset
208
+ >();
168
209
 
169
210
  assetGraph.traverse((node, _, actions) => {
170
211
  if (node.type === 'asset' && parentsToConfig.has(node.value.filePath)) {
@@ -191,8 +232,10 @@ export function createIdealGraph(
191
232
  continue;
192
233
  }
193
234
 
194
- let parentAsset = configToParentAsset.get(c);
195
- 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
+ );
196
239
 
197
240
  assetGraph.traverse((node, _, actions) => {
198
241
  if (
@@ -203,17 +246,16 @@ export function createIdealGraph(
203
246
  config.projectRoot,
204
247
  node.value.filePath,
205
248
  );
206
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
207
- return;
208
- }
209
249
 
210
250
  // We track all matching MSB's for constant modules as they are never duplicated
211
251
  // and need to be assigned to all matching bundles
212
252
  if (node.value.meta.isConstantModule === true) {
213
253
  constantModuleToMSB.get(node.value).push(c);
214
254
  }
215
- manualAssetToConfig.set(node.value, c);
216
- return;
255
+
256
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
257
+ manualAssetToConfig.set(node.value, c);
258
+ }
217
259
  }
218
260
 
219
261
  if (
@@ -241,9 +283,19 @@ export function createIdealGraph(
241
283
  makeManualAssetToConfigLookup();
242
284
  let manualBundleToInternalizedAsset: DefaultMap<
243
285
  NodeId,
244
- Array<Asset>,
286
+ Array<Asset>
245
287
  > = new DefaultMap(() => []);
246
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
+
247
299
  /**
248
300
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
249
301
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -251,7 +303,26 @@ export function createIdealGraph(
251
303
  */
252
304
  assetGraph.traverse(
253
305
  {
254
- 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
+ ) {
255
326
  if (node.type === 'asset') {
256
327
  if (
257
328
  context?.type === 'dependency' &&
@@ -287,12 +358,16 @@ export function createIdealGraph(
287
358
  }
288
359
 
289
360
  for (let childAsset of assets) {
290
- let bundleId = bundles.get(childAsset.id);
291
- let bundle;
361
+ let bundleId: number | undefined | null = bundles.get(
362
+ childAsset.id,
363
+ );
364
+ let bundle: Bundle | 'root' | undefined;
292
365
 
293
366
  // MSB Step 1: Match glob on filepath and type for any asset
294
- let manualSharedBundleKey;
295
- let manualSharedObject = manualAssetToConfig.get(childAsset);
367
+ let manualSharedBundleKey: string | undefined;
368
+ let manualSharedObject:
369
+ | ResolvedBundlerConfig['manualSharedBundles'][number]
370
+ | undefined = manualAssetToConfig.get(childAsset);
296
371
 
297
372
  if (manualSharedObject) {
298
373
  // MSB Step 2: Generate a key for which to look up this manual bundle with
@@ -311,7 +386,8 @@ export function createIdealGraph(
311
386
  dependency.priority === 'lazy' ||
312
387
  (getFeatureFlag('conditionalBundlingApi') &&
313
388
  node.value.priority === 'conditional') ||
314
- 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'
315
391
  ) {
316
392
  if (bundleId == null) {
317
393
  let firstBundleGroup = nullthrows(
@@ -324,7 +400,9 @@ export function createIdealGraph(
324
400
  dependency.bundleBehavior ?? childAsset.bundleBehavior,
325
401
  needsStableName:
326
402
  dependency.bundleBehavior === 'inline' ||
327
- childAsset.bundleBehavior === 'inline'
403
+ childAsset.bundleBehavior === 'inline' ||
404
+ dependency.bundleBehavior === 'inlineIsolated' ||
405
+ childAsset.bundleBehavior === 'inlineIsolated'
328
406
  ? false
329
407
  : dependency.isEntry || dependency.needsStableName,
330
408
  target: firstBundleGroup.target,
@@ -334,6 +412,14 @@ export function createIdealGraph(
334
412
  bundleRoots.set(childAsset, [bundleId, bundleId]);
335
413
  bundleGroupBundleIds.add(bundleId);
336
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
+ }
337
423
  if (manualSharedObject) {
338
424
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
339
425
  // since MSBs should not have main entry assets
@@ -348,7 +434,8 @@ export function createIdealGraph(
348
434
  if (
349
435
  // If this dependency requests isolated, but the bundle is not,
350
436
  // make the bundle isolated for all uses.
351
- dependency.bundleBehavior === 'isolated' &&
437
+ (dependency.bundleBehavior === 'isolated' ||
438
+ dependency.bundleBehavior === 'inlineIsolated') &&
352
439
  bundle.bundleBehavior == null
353
440
  ) {
354
441
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -370,7 +457,9 @@ export function createIdealGraph(
370
457
  type: 'bundle',
371
458
  },
372
459
  ),
373
- dependencyPriorityEdges[dependency.priority],
460
+ dependencyPriorityEdges[
461
+ dependency.priority as keyof typeof dependencyPriorityEdges
462
+ ],
374
463
  );
375
464
 
376
465
  if (
@@ -431,6 +520,7 @@ export function createIdealGraph(
431
520
  needsStableName:
432
521
  childAsset.bundleBehavior === 'inline' ||
433
522
  dependency.bundleBehavior === 'inline' ||
523
+ dependency.bundleBehavior === 'inlineIsolated' ||
434
524
  (dependency.priority === 'parallel' &&
435
525
  !dependency.needsStableName)
436
526
  ? false
@@ -438,13 +528,15 @@ export function createIdealGraph(
438
528
  });
439
529
  bundleId = bundleGraph.addNode(bundle);
440
530
  } else {
441
- bundle = bundleGraph.getNode(bundleId);
442
- invariant(bundle != null && bundle !== 'root');
531
+ let bundleNode = bundleGraph.getNode(bundleId);
532
+ invariant(bundleNode != null && bundleNode !== 'root');
443
533
 
534
+ bundle = bundleNode;
444
535
  if (
445
536
  // If this dependency requests isolated, but the bundle is not,
446
537
  // make the bundle isolated for all uses.
447
- dependency.bundleBehavior === 'isolated' &&
538
+ (dependency.bundleBehavior === 'isolated' ||
539
+ dependency.bundleBehavior === 'inlineIsolated') &&
448
540
  bundle.bundleBehavior == null
449
541
  ) {
450
542
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -514,7 +606,7 @@ export function createIdealGraph(
514
606
  }
515
607
  return node;
516
608
  },
517
- exit(node) {
609
+ exit(node: BundleGraphTraversable): undefined {
518
610
  if (stack[stack.length - 1]?.[0] === node.value) {
519
611
  stack.pop();
520
612
  }
@@ -563,22 +655,22 @@ export function createIdealGraph(
563
655
 
564
656
  // reachableRoots is an array of bit sets for each asset. Each bit set
565
657
  // indicates which bundle roots are reachable from that asset synchronously.
566
- let reachableRoots = [];
658
+ let reachableRoots: Array<BitSet> = [];
567
659
  for (let i = 0; i < assets.length; i++) {
568
660
  reachableRoots.push(new BitSet(bundleRootGraph.nodes.length));
569
661
  }
570
662
 
571
663
  // reachableAssets is the inverse mapping of reachableRoots. For each bundle root,
572
664
  // it contains a bit set that indicates which assets are reachable from it.
573
- let reachableAssets = [];
665
+ let reachableAssets: Array<BitSet> = [];
574
666
 
575
667
  // ancestorAssets maps bundle roots to the set of all assets available to it at runtime,
576
668
  // including in earlier parallel bundles. These are intersected through all paths to
577
669
  // the bundle to ensure that the available assets are always present no matter in which
578
670
  // order the bundles are loaded.
579
- let ancestorAssets = [];
671
+ let ancestorAssets: Array<null | BitSet> = [];
580
672
 
581
- let inlineConstantDeps = new DefaultMap(() => new Set());
673
+ let inlineConstantDeps = new DefaultMap<Asset, Set<Asset>>(() => new Set());
582
674
 
583
675
  for (let [bundleRootId, assetId] of bundleRootGraph.nodes.entries()) {
584
676
  let reachable = new BitSet(assets.length);
@@ -687,7 +779,10 @@ export function createIdealGraph(
687
779
  // not true that a bundle's available assets = all assets of all the bundleGroups
688
780
  // it belongs to. It's the intersection of those sets.
689
781
  let available;
690
- if (bundleRoot.bundleBehavior === 'isolated') {
782
+ if (
783
+ bundleRoot.bundleBehavior === 'isolated' ||
784
+ bundleRoot.bundleBehavior === 'inlineIsolated'
785
+ ) {
691
786
  available = new BitSet(assets.length);
692
787
  } else {
693
788
  available = nullthrows(ancestorAssets[nodeId]).clone();
@@ -771,7 +866,8 @@ export function createIdealGraph(
771
866
 
772
867
  let parentRoots = bundleRootGraph.getNodeIdsConnectedTo(id, ALL_EDGE_TYPES);
773
868
  let canDelete =
774
- getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated';
869
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated' &&
870
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'inlineIsolated';
775
871
  if (parentRoots.length === 0) continue;
776
872
  for (let parentId of parentRoots) {
777
873
  if (parentId === rootNodeId) {
@@ -819,8 +915,10 @@ export function createIdealGraph(
819
915
  let reachableNonEntries = new BitSet(assets.length);
820
916
  let reachableIntersection = new BitSet(assets.length);
821
917
  for (let i = 0; i < assets.length; i++) {
822
- let asset = assets[i];
823
- let manualSharedObject = manualAssetToConfig.get(asset);
918
+ let asset: Asset = assets[i];
919
+ let manualSharedObject:
920
+ | ResolvedBundlerConfig['manualSharedBundles'][number]
921
+ | undefined = manualAssetToConfig.get(asset);
824
922
 
825
923
  if (bundleRoots.has(asset) && inlineConstantDeps.get(asset).size > 0) {
826
924
  let entryBundleId = nullthrows(bundleRoots.get(asset))[0];
@@ -848,7 +946,8 @@ export function createIdealGraph(
848
946
  !a.isBundleSplittable ||
849
947
  (bundleRoots.get(a) &&
850
948
  (getBundleFromBundleRoot(a).needsStableName ||
851
- getBundleFromBundleRoot(a).bundleBehavior === 'isolated'))
949
+ getBundleFromBundleRoot(a).bundleBehavior === 'isolated' ||
950
+ getBundleFromBundleRoot(a).bundleBehavior === 'inlineIsolated'))
852
951
  ) {
853
952
  // Add asset to non-splittable bundles.
854
953
  addAssetToBundleRoot(asset, a);
@@ -863,10 +962,11 @@ export function createIdealGraph(
863
962
 
864
963
  // If we encounter a "manual" asset, draw an edge from reachable to its MSB
865
964
  if (manualSharedObject && !reachable.empty()) {
866
- let bundle;
867
- let bundleId;
868
- let manualSharedBundleKey = manualSharedObject.name + ',' + asset.type;
869
- 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> = [];
870
970
  reachable.forEach((id) => {
871
971
  sourceBundles.push(nullthrows(bundleRoots.get(assets[id]))[0]);
872
972
  });
@@ -890,12 +990,14 @@ export function createIdealGraph(
890
990
  manualSharedMap.set(manualSharedBundleKey, bundleId);
891
991
  } else {
892
992
  bundleId = nullthrows(manualSharedMap.get(manualSharedBundleKey));
893
- bundle = nullthrows(bundleGraph.getNode(bundleId));
993
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
894
994
  invariant(
895
- bundle != null && bundle !== 'root',
995
+ bundleNode != null && bundleNode !== 'root',
896
996
  'We tried to use the root incorrectly',
897
997
  );
898
998
 
999
+ bundle = bundleNode;
1000
+
899
1001
  if (!bundle.assets.has(asset)) {
900
1002
  bundle.assets.add(asset);
901
1003
  bundle.size += asset.stats.size;
@@ -981,7 +1083,7 @@ export function createIdealGraph(
981
1083
  });
982
1084
  }
983
1085
 
984
- let reachableArray = [];
1086
+ let reachableArray: Array<Asset> = [];
985
1087
  reachable.forEach((id) => {
986
1088
  reachableArray.push(assets[id]);
987
1089
  });
@@ -991,12 +1093,13 @@ export function createIdealGraph(
991
1093
  config.disableSharedBundles === false &&
992
1094
  reachableArray.length > config.minBundles
993
1095
  ) {
994
- let sourceBundles = reachableArray.map(
1096
+ let sourceBundles: Array<NodeId> = reachableArray.map(
995
1097
  (a) => nullthrows(bundleRoots.get(a))[0],
996
1098
  );
997
- let key = reachableArray.map((a) => a.id).join(',') + '.' + asset.type;
998
- let bundleId = bundles.get(key);
999
- 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;
1000
1103
  if (bundleId == null) {
1001
1104
  let firstSourceBundle = nullthrows(
1002
1105
  bundleGraph.getNode(sourceBundles[0]),
@@ -1027,8 +1130,9 @@ export function createIdealGraph(
1027
1130
  bundleId = bundleGraph.addNode(bundle);
1028
1131
  bundles.set(key, bundleId);
1029
1132
  } else {
1030
- bundle = nullthrows(bundleGraph.getNode(bundleId));
1031
- invariant(bundle !== 'root');
1133
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
1134
+ invariant(bundleNode !== 'root');
1135
+ bundle = bundleNode;
1032
1136
  }
1033
1137
  bundle.assets.add(asset);
1034
1138
  bundle.size += asset.stats.size;
@@ -1056,25 +1160,31 @@ export function createIdealGraph(
1056
1160
  }
1057
1161
 
1058
1162
  let manualSharedBundleIds = new Set([...manualSharedMap.values()]);
1163
+ let modifiedSourceBundles = new Set<Bundle>();
1164
+
1059
1165
  // Step split manual shared bundles for those that have the "split" property set
1060
- let remainderMap = new DefaultMap(() => []);
1166
+ let remainderMap = new DefaultMap<number, Array<Asset>>(() => []);
1061
1167
  for (let id of manualSharedMap.values()) {
1062
1168
  let manualBundle = bundleGraph.getNode(id);
1063
1169
  invariant(manualBundle !== 'root' && manualBundle != null);
1064
1170
 
1065
1171
  if (manualBundle.sourceBundles.size > 0) {
1066
- let firstSourceBundle = nullthrows(
1172
+ let firstSourceBundleNode: Bundle | 'root' = nullthrows(
1067
1173
  bundleGraph.getNode([...manualBundle.sourceBundles][0]),
1068
1174
  );
1069
- invariant(firstSourceBundle !== 'root');
1070
- let firstAsset = [...manualBundle.assets][0];
1071
- 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);
1072
1182
  invariant(manualSharedObject != null);
1073
- let modNum = manualAssetToConfig.get(firstAsset)?.split;
1183
+ let modNum: number | undefined =
1184
+ manualAssetToConfig.get(firstAsset)?.split;
1074
1185
  if (modNum != null) {
1075
1186
  for (let a of [...manualBundle.assets]) {
1076
1187
  let numRep = getBigIntFromContentKey(a.id);
1077
- // $FlowFixMe Flow doesn't know about BigInt
1078
1188
  let r = Number(numRep % BigInt(modNum));
1079
1189
 
1080
1190
  remainderMap.get(r).push(a);
@@ -1128,6 +1238,89 @@ export function createIdealGraph(
1128
1238
  }
1129
1239
  }
1130
1240
 
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);
1322
+ }
1323
+
1131
1324
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
1132
1325
  // their source bundles, and remove the bundle.
1133
1326
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -1143,9 +1336,8 @@ export function createIdealGraph(
1143
1336
  }
1144
1337
  }
1145
1338
 
1146
- let modifiedSourceBundles = new Set();
1147
-
1148
1339
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1340
+
1149
1341
  if (config.disableSharedBundles === false) {
1150
1342
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
1151
1343
  // Find shared bundles in this bundle group.
@@ -1158,7 +1350,13 @@ export function createIdealGraph(
1158
1350
  let numBundlesContributingToPRL = bundleIdsInGroup.reduce((count, b) => {
1159
1351
  let bundle = nullthrows(bundleGraph.getNode(b));
1160
1352
  invariant(bundle !== 'root');
1161
- return count + (bundle.bundleBehavior !== 'inline');
1353
+ return (
1354
+ count +
1355
+ Number(
1356
+ bundle.bundleBehavior !== 'inline' &&
1357
+ bundle.bundleBehavior !== 'inlineIsolated',
1358
+ )
1359
+ );
1162
1360
  }, 0);
1163
1361
 
1164
1362
  if (numBundlesContributingToPRL > config.maxParallelRequests) {
@@ -1193,6 +1391,7 @@ export function createIdealGraph(
1193
1391
  numBundlesContributingToPRL > config.maxParallelRequests
1194
1392
  ) {
1195
1393
  let bundleTuple = sharedBundlesInGroup.pop();
1394
+ if (!bundleTuple) break;
1196
1395
  let bundleToRemove = bundleTuple.bundle;
1197
1396
  let bundleIdToRemove = bundleTuple.id;
1198
1397
  //TODO add integration test where bundles in bunlde group > max parallel request limit & only remove a couple shared bundles
@@ -1249,10 +1448,417 @@ export function createIdealGraph(
1249
1448
  }
1250
1449
  }
1251
1450
 
1252
- function getBigIntFromContentKey(contentKey) {
1451
+ function mergeBundles(
1452
+ bundleToKeepId: NodeId,
1453
+ bundleToRemoveId: NodeId,
1454
+ reason: string,
1455
+ ) {
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);
1466
+ for (let asset of bundleToRemove.assets) {
1467
+ bundleToKeep.assets.add(asset);
1468
+ bundleToKeep.size += asset.stats.size;
1469
+
1470
+ let newAssetReference = assetReference
1471
+ .get(asset)
1472
+ .map(([dep, bundle]): [Dependency, Bundle] =>
1473
+ bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1474
+ );
1475
+
1476
+ assetReference.set(asset, newAssetReference);
1477
+ }
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
1499
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
1500
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1501
+ continue;
1502
+ }
1503
+
1504
+ if (sourceBundleId !== bundleToKeepId) {
1505
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1506
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1507
+ }
1508
+ }
1509
+
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
+ ),
1595
+ );
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
+ }
1644
+ }
1645
+ }
1646
+
1647
+ bundleGraph.removeNode(bundleToRemoveId);
1648
+ }
1649
+
1650
+ function mergeSharedBundles(mergeConfig: SharedBundleMergeCandidates) {
1651
+ // Find all shared bundles
1652
+ let sharedBundles = new Set<NodeId>();
1653
+ bundleGraph.traverse((nodeId) => {
1654
+ let bundle = bundleGraph.getNode(nodeId);
1655
+
1656
+ if (!bundle) {
1657
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
1658
+ }
1659
+
1660
+ if (bundle === 'root') {
1661
+ return;
1662
+ }
1663
+
1664
+ // Only consider JS shared bundles and non-reused bundles.
1665
+ // These count potentially be considered for merging in future but they're
1666
+ // more complicated to merge
1667
+ if (
1668
+ bundle.sourceBundles.size > 0 &&
1669
+ bundle.manualSharedBundle == null &&
1670
+ !bundle.mainEntryAsset &&
1671
+ bundle.type === 'js'
1672
+ ) {
1673
+ sharedBundles.add(nodeId);
1674
+ }
1675
+ });
1676
+
1677
+ let clusters = findMergeCandidates(
1678
+ bundleGraph,
1679
+ Array.from(sharedBundles),
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
+ ),
1696
+ );
1697
+
1698
+ let mergedBundles = new Set();
1699
+
1700
+ for (let cluster of clusters) {
1701
+ let [mergeTarget, ...rest] = cluster;
1702
+
1703
+ for (let bundleIdToMerge of rest) {
1704
+ mergeBundles(mergeTarget, bundleIdToMerge, 'shared-merge');
1705
+ }
1706
+
1707
+ mergedBundles.add(mergeTarget);
1708
+ }
1709
+
1710
+ if (getFeatureFlag('supportWebpackChunkName')) {
1711
+ return mergedBundles;
1712
+ }
1713
+ }
1714
+
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) {
1253
1860
  let b = Buffer.alloc(64);
1254
1861
  b.write(contentKey);
1255
- // $FlowFixMe Flow doesn't have BigInt types in this version
1256
1862
  return b.readBigInt64BE();
1257
1863
  }
1258
1864
  // Fix asset order in source bundles as they are likely now incorrect after shared bundle deletion
@@ -1279,14 +1885,86 @@ export function createIdealGraph(
1279
1885
  bundleRootGraph.removeNode(bundleRootId);
1280
1886
  }
1281
1887
  }
1282
- function getBundlesForBundleGroup(bundleGroupId) {
1283
- let bundlesInABundleGroup = [];
1888
+ function getBundlesForBundleGroup(bundleGroupId: NodeId) {
1889
+ let bundlesInABundleGroup: Array<NodeId> = [];
1284
1890
  bundleGraph.traverse((nodeId) => {
1285
1891
  bundlesInABundleGroup.push(nodeId);
1286
1892
  }, bundleGroupId);
1287
1893
  return bundlesInABundleGroup;
1288
1894
  }
1289
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
+
1290
1968
  function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle {
1291
1969
  let bundle = bundleGraph.getNode(
1292
1970
  nullthrows(bundleRoots.get(bundleRoot))[0],
@@ -1351,6 +2029,12 @@ export function createIdealGraph(
1351
2029
  bundleGraph.removeNode(bundleId);
1352
2030
  }
1353
2031
 
2032
+ stats.report((bundleId) => {
2033
+ let bundle = bundleGraph.getNode(bundleId);
2034
+ invariant(bundle !== 'root');
2035
+ return bundle;
2036
+ });
2037
+
1354
2038
  return {
1355
2039
  assets,
1356
2040
  bundleGraph,
@@ -1361,23 +2045,24 @@ export function createIdealGraph(
1361
2045
  };
1362
2046
  }
1363
2047
 
1364
- function createBundle(opts: {|
1365
- asset?: Asset,
1366
- bundleBehavior?: ?BundleBehavior,
1367
- env?: Environment,
1368
- manualSharedBundle?: ?string,
1369
- needsStableName?: boolean,
1370
- sourceBundles?: Set<NodeId>,
1371
- target: Target,
1372
- type?: string,
1373
- uniqueKey?: string,
1374
- |}): 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 {
1375
2059
  if (opts.asset == null) {
1376
2060
  return {
1377
2061
  assets: new Set(),
1378
2062
  bundleBehavior: opts.bundleBehavior,
1379
2063
  env: nullthrows(opts.env),
1380
2064
  mainEntryAsset: null,
2065
+ bundleRoots: new Set(),
1381
2066
  manualSharedBundle: opts.manualSharedBundle,
1382
2067
  needsStableName: Boolean(opts.needsStableName),
1383
2068
  size: 0,
@@ -1394,6 +2079,7 @@ function createBundle(opts: {|
1394
2079
  bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior,
1395
2080
  env: opts.env ?? asset.env,
1396
2081
  mainEntryAsset: asset,
2082
+ bundleRoots: new Set([asset]),
1397
2083
  manualSharedBundle: opts.manualSharedBundle,
1398
2084
  needsStableName: Boolean(opts.needsStableName),
1399
2085
  size: asset.stats.size,