@atlaspack/bundler-default 2.14.5-canary.20 → 2.14.5-canary.200

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,
@@ -23,37 +22,43 @@ import {DefaultMap, globToRegex} from '@atlaspack/utils';
23
22
  import invariant from 'assert';
24
23
  import nullthrows from 'nullthrows';
25
24
 
26
- import {findMergeCandidates} from './bundleMerge';
27
- import type {ResolvedBundlerConfig} from './bundlerConfig';
25
+ import {findMergeCandidates, MergeGroup} from './bundleMerge';
26
+ import type {
27
+ ResolvedBundlerConfig,
28
+ SharedBundleMergeCandidates,
29
+ AsyncBundleMerge,
30
+ } from './bundlerConfig';
31
+ import {Stats} from './stats';
28
32
 
29
33
  /* BundleRoot - An asset that is the main entry of a Bundle. */
30
34
  type BundleRoot = Asset;
31
35
 
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
- |};
36
+ export type Bundle = {
37
+ uniqueKey: string | null | undefined;
38
+ assets: Set<Asset>;
39
+ internalizedAssets?: BitSet;
40
+ bundleBehavior?: BundleBehavior | null | undefined;
41
+ needsStableName: boolean;
42
+ mainEntryAsset: Asset | null | undefined;
43
+ bundleRoots: Set<Asset>;
44
+ size: number;
45
+ sourceBundles: Set<NodeId>;
46
+ target: Target;
47
+ env: Environment;
48
+ type: string;
49
+ manualSharedBundle: string | null | undefined; // for naming purposes;
50
+ };
46
51
 
47
52
  export type DependencyBundleGraph = ContentGraph<
48
- | {|
49
- value: Bundle,
50
- type: 'bundle',
51
- |}
52
- | {|
53
- value: Dependency,
54
- type: 'dependency',
55
- |},
56
- number,
53
+ | {
54
+ value: Bundle;
55
+ type: 'bundle';
56
+ }
57
+ | {
58
+ value: Dependency;
59
+ type: 'dependency';
60
+ },
61
+ number
57
62
  >;
58
63
 
59
64
  const dependencyPriorityEdges = {
@@ -61,7 +66,7 @@ const dependencyPriorityEdges = {
61
66
  parallel: 2,
62
67
  lazy: 3,
63
68
  conditional: 4,
64
- };
69
+ } as const;
65
70
 
66
71
  export const idealBundleGraphEdges = Object.freeze({
67
72
  default: 1,
@@ -70,20 +75,30 @@ export const idealBundleGraphEdges = Object.freeze({
70
75
 
71
76
  export type IdealBundleGraph = Graph<
72
77
  Bundle | 'root',
73
- $Values<typeof idealBundleGraphEdges>,
78
+ (typeof idealBundleGraphEdges)[keyof typeof idealBundleGraphEdges]
74
79
  >;
75
80
 
76
81
  // IdealGraph is the structure we will pass to decorate,
77
82
  // which mutates the assetGraph into the bundleGraph we would
78
83
  // 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
- |};
84
+ export type IdealGraph = {
85
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>;
86
+ assets: Array<Asset>;
87
+ bundleGraph: IdealBundleGraph;
88
+ bundleGroupBundleIds: Set<NodeId>;
89
+ dependencyBundleGraph: DependencyBundleGraph;
90
+ manualAssetToBundle: Map<Asset, NodeId>;
91
+ };
92
+
93
+ function isNonRootBundle(
94
+ bundle?: Bundle | 'root' | null,
95
+ message?: string,
96
+ ): Bundle {
97
+ let existingBundle = nullthrows(bundle, message);
98
+ invariant(existingBundle !== 'root', "Bundle cannot be 'root'");
99
+
100
+ return existingBundle;
101
+ }
87
102
 
88
103
  export function createIdealGraph(
89
104
  assetGraph: MutableBundleGraph,
@@ -97,8 +112,9 @@ export function createIdealGraph(
97
112
  let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph();
98
113
  let assetReference: DefaultMap<
99
114
  Asset,
100
- Array<[Dependency, Bundle]>,
115
+ Array<[Dependency, Bundle]>
101
116
  > = new DefaultMap(() => []);
117
+ let stats = new Stats(config.projectRoot);
102
118
 
103
119
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
104
120
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -111,8 +127,9 @@ export function createIdealGraph(
111
127
  };
112
128
  // Graph that models bundleRoots, with parallel & async deps only to inform reachability
113
129
  let bundleRootGraph: Graph<
114
- number, // asset index
115
- $Values<typeof bundleRootEdgeTypes>,
130
+ // asset index
131
+ number,
132
+ (typeof bundleRootEdgeTypes)[keyof typeof bundleRootEdgeTypes]
116
133
  > = new Graph();
117
134
  let assetToBundleRootNodeId = new Map<BundleRoot, number>();
118
135
 
@@ -146,7 +163,7 @@ export function createIdealGraph(
146
163
  bundleGroupBundleIds.add(nodeId);
147
164
  }
148
165
 
149
- let assets = [];
166
+ let assets: Array<Asset> = [];
150
167
  let assetToIndex = new Map<Asset, number>();
151
168
 
152
169
  function makeManualAssetToConfigLookup() {
@@ -161,6 +178,7 @@ export function createIdealGraph(
161
178
 
162
179
  for (let c of config.manualSharedBundles) {
163
180
  if (c.root != null) {
181
+ // @ts-expect-error TS2345
164
182
  parentsToConfig.get(path.join(config.projectRoot, c.root)).push(c);
165
183
  }
166
184
  }
@@ -204,17 +222,17 @@ export function createIdealGraph(
204
222
  config.projectRoot,
205
223
  node.value.filePath,
206
224
  );
207
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
208
- return;
209
- }
210
225
 
211
226
  // We track all matching MSB's for constant modules as they are never duplicated
212
227
  // and need to be assigned to all matching bundles
213
228
  if (node.value.meta.isConstantModule === true) {
229
+ // @ts-expect-error TS2345
214
230
  constantModuleToMSB.get(node.value).push(c);
215
231
  }
216
- manualAssetToConfig.set(node.value, c);
217
- return;
232
+
233
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
234
+ manualAssetToConfig.set(node.value, c);
235
+ }
218
236
  }
219
237
 
220
238
  if (
@@ -242,9 +260,19 @@ export function createIdealGraph(
242
260
  makeManualAssetToConfigLookup();
243
261
  let manualBundleToInternalizedAsset: DefaultMap<
244
262
  NodeId,
245
- Array<Asset>,
263
+ Array<Asset>
246
264
  > = new DefaultMap(() => []);
247
265
 
266
+ let mergeSourceBundleLookup = new Map<string, NodeId>();
267
+ let mergeSourceBundleAssets = new Set(
268
+ config.sharedBundleMerge?.flatMap(
269
+ (c) =>
270
+ c.sourceBundles?.map((assetMatch: string) =>
271
+ path.join(config.projectRoot, assetMatch),
272
+ ) ?? [],
273
+ ),
274
+ );
275
+
248
276
  /**
249
277
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
250
278
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -252,7 +280,27 @@ export function createIdealGraph(
252
280
  */
253
281
  assetGraph.traverse(
254
282
  {
255
- enter(node, context, actions) {
283
+ enter(
284
+ node: // @ts-expect-error TS2304
285
+ | BundleGraphTraversable
286
+ | {
287
+ readonly type: 'dependency';
288
+ value: Dependency;
289
+ },
290
+ context:
291
+ | {
292
+ readonly type: 'asset';
293
+ value: Asset;
294
+ }
295
+ | null
296
+ | undefined
297
+ | {
298
+ readonly type: 'dependency';
299
+ value: Dependency;
300
+ },
301
+ // @ts-expect-error TS2304
302
+ actions: TraversalActions,
303
+ ) {
256
304
  if (node.type === 'asset') {
257
305
  if (
258
306
  context?.type === 'dependency' &&
@@ -312,7 +360,8 @@ export function createIdealGraph(
312
360
  dependency.priority === 'lazy' ||
313
361
  (getFeatureFlag('conditionalBundlingApi') &&
314
362
  node.value.priority === 'conditional') ||
315
- childAsset.bundleBehavior === 'isolated' // An isolated Dependency, or Bundle must contain all assets it needs to load.
363
+ childAsset.bundleBehavior === 'isolated' || // An isolated Dependency, or Bundle must contain all assets it needs to load.
364
+ childAsset.bundleBehavior === 'inlineIsolated'
316
365
  ) {
317
366
  if (bundleId == null) {
318
367
  let firstBundleGroup = nullthrows(
@@ -325,7 +374,9 @@ export function createIdealGraph(
325
374
  dependency.bundleBehavior ?? childAsset.bundleBehavior,
326
375
  needsStableName:
327
376
  dependency.bundleBehavior === 'inline' ||
328
- childAsset.bundleBehavior === 'inline'
377
+ childAsset.bundleBehavior === 'inline' ||
378
+ dependency.bundleBehavior === 'inlineIsolated' ||
379
+ childAsset.bundleBehavior === 'inlineIsolated'
329
380
  ? false
330
381
  : dependency.isEntry || dependency.needsStableName,
331
382
  target: firstBundleGroup.target,
@@ -335,6 +386,14 @@ export function createIdealGraph(
335
386
  bundleRoots.set(childAsset, [bundleId, bundleId]);
336
387
  bundleGroupBundleIds.add(bundleId);
337
388
  bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
389
+ // If this asset is relevant for merging then track it's source
390
+ // bundle id for later
391
+ if (mergeSourceBundleAssets.has(childAsset.filePath)) {
392
+ mergeSourceBundleLookup.set(
393
+ path.relative(config.projectRoot, childAsset.filePath),
394
+ bundleId,
395
+ );
396
+ }
338
397
  if (manualSharedObject) {
339
398
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
340
399
  // since MSBs should not have main entry assets
@@ -349,7 +408,8 @@ export function createIdealGraph(
349
408
  if (
350
409
  // If this dependency requests isolated, but the bundle is not,
351
410
  // make the bundle isolated for all uses.
352
- dependency.bundleBehavior === 'isolated' &&
411
+ (dependency.bundleBehavior === 'isolated' ||
412
+ dependency.bundleBehavior === 'inlineIsolated') &&
353
413
  bundle.bundleBehavior == null
354
414
  ) {
355
415
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -371,6 +431,7 @@ export function createIdealGraph(
371
431
  type: 'bundle',
372
432
  },
373
433
  ),
434
+ // @ts-expect-error TS7053
374
435
  dependencyPriorityEdges[dependency.priority],
375
436
  );
376
437
 
@@ -432,6 +493,7 @@ export function createIdealGraph(
432
493
  needsStableName:
433
494
  childAsset.bundleBehavior === 'inline' ||
434
495
  dependency.bundleBehavior === 'inline' ||
496
+ dependency.bundleBehavior === 'inlineIsolated' ||
435
497
  (dependency.priority === 'parallel' &&
436
498
  !dependency.needsStableName)
437
499
  ? false
@@ -445,7 +507,8 @@ export function createIdealGraph(
445
507
  if (
446
508
  // If this dependency requests isolated, but the bundle is not,
447
509
  // make the bundle isolated for all uses.
448
- dependency.bundleBehavior === 'isolated' &&
510
+ (dependency.bundleBehavior === 'isolated' ||
511
+ dependency.bundleBehavior === 'inlineIsolated') &&
449
512
  bundle.bundleBehavior == null
450
513
  ) {
451
514
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -482,6 +545,7 @@ export function createIdealGraph(
482
545
 
483
546
  assetReference.get(childAsset).push([dependency, bundle]);
484
547
  } else {
548
+ // @ts-expect-error TS2322
485
549
  bundleId = null;
486
550
  }
487
551
  if (manualSharedObject && bundleId != null) {
@@ -489,6 +553,7 @@ export function createIdealGraph(
489
553
  // add the asset if it doesn't already have it and set key
490
554
 
491
555
  invariant(
556
+ // @ts-expect-error TS2367
492
557
  bundle !== 'root' && bundle != null && bundleId != null,
493
558
  );
494
559
 
@@ -515,7 +580,8 @@ export function createIdealGraph(
515
580
  }
516
581
  return node;
517
582
  },
518
- exit(node) {
583
+ // @ts-expect-error TS2322
584
+ exit(node: BundleGraphTraversable) {
519
585
  if (stack[stack.length - 1]?.[0] === node.value) {
520
586
  stack.pop();
521
587
  }
@@ -564,20 +630,20 @@ export function createIdealGraph(
564
630
 
565
631
  // reachableRoots is an array of bit sets for each asset. Each bit set
566
632
  // indicates which bundle roots are reachable from that asset synchronously.
567
- let reachableRoots = [];
633
+ let reachableRoots: Array<BitSet> = [];
568
634
  for (let i = 0; i < assets.length; i++) {
569
635
  reachableRoots.push(new BitSet(bundleRootGraph.nodes.length));
570
636
  }
571
637
 
572
638
  // reachableAssets is the inverse mapping of reachableRoots. For each bundle root,
573
639
  // it contains a bit set that indicates which assets are reachable from it.
574
- let reachableAssets = [];
640
+ let reachableAssets: Array<BitSet> = [];
575
641
 
576
642
  // ancestorAssets maps bundle roots to the set of all assets available to it at runtime,
577
643
  // including in earlier parallel bundles. These are intersected through all paths to
578
644
  // the bundle to ensure that the available assets are always present no matter in which
579
645
  // order the bundles are loaded.
580
- let ancestorAssets = [];
646
+ let ancestorAssets: Array<null | BitSet> = [];
581
647
 
582
648
  let inlineConstantDeps = new DefaultMap(() => new Set());
583
649
 
@@ -688,7 +754,10 @@ export function createIdealGraph(
688
754
  // not true that a bundle's available assets = all assets of all the bundleGroups
689
755
  // it belongs to. It's the intersection of those sets.
690
756
  let available;
691
- if (bundleRoot.bundleBehavior === 'isolated') {
757
+ if (
758
+ bundleRoot.bundleBehavior === 'isolated' ||
759
+ bundleRoot.bundleBehavior === 'inlineIsolated'
760
+ ) {
692
761
  available = new BitSet(assets.length);
693
762
  } else {
694
763
  available = nullthrows(ancestorAssets[nodeId]).clone();
@@ -772,7 +841,8 @@ export function createIdealGraph(
772
841
 
773
842
  let parentRoots = bundleRootGraph.getNodeIdsConnectedTo(id, ALL_EDGE_TYPES);
774
843
  let canDelete =
775
- getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated';
844
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated' &&
845
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'inlineIsolated';
776
846
  if (parentRoots.length === 0) continue;
777
847
  for (let parentId of parentRoots) {
778
848
  if (parentId === rootNodeId) {
@@ -806,8 +876,11 @@ export function createIdealGraph(
806
876
 
807
877
  function assignInlineConstants(parentAsset: Asset, bundle: Bundle) {
808
878
  for (let inlineConstant of inlineConstantDeps.get(parentAsset)) {
879
+ // @ts-expect-error TS2345
809
880
  if (!bundle.assets.has(inlineConstant)) {
881
+ // @ts-expect-error TS2345
810
882
  bundle.assets.add(inlineConstant);
883
+ // @ts-expect-error TS18046
811
884
  bundle.size += inlineConstant.stats.size;
812
885
  }
813
886
  }
@@ -849,7 +922,8 @@ export function createIdealGraph(
849
922
  !a.isBundleSplittable ||
850
923
  (bundleRoots.get(a) &&
851
924
  (getBundleFromBundleRoot(a).needsStableName ||
852
- getBundleFromBundleRoot(a).bundleBehavior === 'isolated'))
925
+ getBundleFromBundleRoot(a).bundleBehavior === 'isolated' ||
926
+ getBundleFromBundleRoot(a).bundleBehavior === 'inlineIsolated'))
853
927
  ) {
854
928
  // Add asset to non-splittable bundles.
855
929
  addAssetToBundleRoot(asset, a);
@@ -867,7 +941,7 @@ export function createIdealGraph(
867
941
  let bundle;
868
942
  let bundleId;
869
943
  let manualSharedBundleKey = manualSharedObject.name + ',' + asset.type;
870
- let sourceBundles = [];
944
+ let sourceBundles: Array<NodeId> = [];
871
945
  reachable.forEach((id) => {
872
946
  sourceBundles.push(nullthrows(bundleRoots.get(assets[id]))[0]);
873
947
  });
@@ -982,7 +1056,7 @@ export function createIdealGraph(
982
1056
  });
983
1057
  }
984
1058
 
985
- let reachableArray = [];
1059
+ let reachableArray: Array<Asset> = [];
986
1060
  reachable.forEach((id) => {
987
1061
  reachableArray.push(assets[id]);
988
1062
  });
@@ -1057,6 +1131,8 @@ export function createIdealGraph(
1057
1131
  }
1058
1132
 
1059
1133
  let manualSharedBundleIds = new Set([...manualSharedMap.values()]);
1134
+ let modifiedSourceBundles = new Set<Bundle>();
1135
+
1060
1136
  // Step split manual shared bundles for those that have the "split" property set
1061
1137
  let remainderMap = new DefaultMap(() => []);
1062
1138
  for (let id of manualSharedMap.values()) {
@@ -1075,9 +1151,9 @@ export function createIdealGraph(
1075
1151
  if (modNum != null) {
1076
1152
  for (let a of [...manualBundle.assets]) {
1077
1153
  let numRep = getBigIntFromContentKey(a.id);
1078
- // $FlowFixMe Flow doesn't know about BigInt
1079
1154
  let r = Number(numRep % BigInt(modNum));
1080
1155
 
1156
+ // @ts-expect-error TS2345
1081
1157
  remainderMap.get(r).push(a);
1082
1158
  }
1083
1159
 
@@ -1100,8 +1176,10 @@ export function createIdealGraph(
1100
1176
  }
1101
1177
  for (let sp of remainderMap.get(i)) {
1102
1178
  bundle.assets.add(sp);
1179
+ // @ts-expect-error TS2339
1103
1180
  bundle.size += sp.stats.size;
1104
1181
  manualBundle.assets.delete(sp);
1182
+ // @ts-expect-error TS2339
1105
1183
  manualBundle.size -= sp.stats.size;
1106
1184
  }
1107
1185
  }
@@ -1114,6 +1192,7 @@ export function createIdealGraph(
1114
1192
  // match multiple MSB's
1115
1193
  for (let [asset, msbs] of constantModuleToMSB.entries()) {
1116
1194
  for (let manualSharedObject of msbs) {
1195
+ // @ts-expect-error TS2339
1117
1196
  let bundleId = manualSharedMap.get(manualSharedObject.name + ',js');
1118
1197
  if (bundleId == null) continue;
1119
1198
  let bundle = nullthrows(bundleGraph.getNode(bundleId));
@@ -1122,17 +1201,97 @@ export function createIdealGraph(
1122
1201
  'We tried to use the root incorrectly',
1123
1202
  );
1124
1203
 
1204
+ // @ts-expect-error TS2345
1125
1205
  if (!bundle.assets.has(asset)) {
1206
+ // @ts-expect-error TS2345
1126
1207
  bundle.assets.add(asset);
1208
+ // @ts-expect-error TS18046
1127
1209
  bundle.size += asset.stats.size;
1128
1210
  }
1129
1211
  }
1130
1212
  }
1131
1213
 
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();
1214
+ if (getFeatureFlag('supportWebpackChunkName')) {
1215
+ // Merge webpack chunk name bundles
1216
+ let chunkNameBundles = new DefaultMap<string, Set<NodeId>>(() => new Set());
1217
+ for (let [nodeId, node] of dependencyBundleGraph.nodes.entries()) {
1218
+ // meta.chunkName is set by the Rust transformer, so we just need to find
1219
+ // bundles that have a chunkName set.
1220
+ if (
1221
+ !node ||
1222
+ node.type !== 'dependency' ||
1223
+ typeof node.value.meta.chunkName !== 'string'
1224
+ ) {
1225
+ continue;
1226
+ }
1227
+
1228
+ let connectedBundles = dependencyBundleGraph.getNodeIdsConnectedFrom(
1229
+ nodeId,
1230
+ dependencyPriorityEdges[node.value.priority],
1231
+ );
1232
+
1233
+ if (connectedBundles.length === 0) {
1234
+ continue;
1235
+ }
1236
+
1237
+ invariant(
1238
+ connectedBundles.length === 1,
1239
+ 'Expected webpackChunkName dependency to be connected to no more than one bundle',
1240
+ );
1241
+
1242
+ let bundleId = connectedBundles[0];
1243
+ let bundleNode = dependencyBundleGraph.getNode(bundleId);
1244
+ invariant(bundleNode != null && bundleNode.type === 'bundle');
1245
+
1246
+ // If a bundle does not have a main entry asset, it's somehow just a
1247
+ // shared bundle, and will be merged/deleted by other means.
1248
+ if (bundleNode.value.mainEntryAsset == null) {
1249
+ continue;
1250
+ }
1251
+
1252
+ let bundleNodeId = null;
1253
+ let mainEntryAssetId = bundleNode.value.mainEntryAsset?.id;
1254
+
1255
+ if (mainEntryAssetId != null) {
1256
+ bundleNodeId = bundles.get(mainEntryAssetId);
1257
+ }
1258
+
1259
+ if (bundleNodeId == null) {
1260
+ continue;
1261
+ }
1262
+
1263
+ chunkNameBundles
1264
+ .get(node.value.meta.chunkName)
1265
+ // DependencyBundleGraph uses content keys as node ids, so we can use that
1266
+ // to get the bundle id.
1267
+ .add(bundleNodeId);
1268
+ }
1269
+
1270
+ for (let [chunkName, bundleIds] of chunkNameBundles.entries()) {
1271
+ // The `[request]` placeholder is not yet supported
1272
+ if (
1273
+ bundleIds.size <= 1 ||
1274
+ (typeof chunkName === 'string' && chunkName.includes('[request]'))
1275
+ ) {
1276
+ continue; // Nothing to merge
1277
+ }
1278
+
1279
+ // Merge all bundles with the same chunk name into the first one.
1280
+ let [firstBundleId, ...rest] = Array.from(bundleIds);
1281
+ for (let bundleId of rest) {
1282
+ mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ // Step merge async bundles that meet the configured params
1288
+ if (config.asyncBundleMerge) {
1289
+ mergeAsyncBundles(config.asyncBundleMerge);
1290
+ }
1291
+
1292
+ // Step merge shared bundles that meet the configured params
1293
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
1294
+ mergeSharedBundles(config.sharedBundleMerge);
1136
1295
  }
1137
1296
 
1138
1297
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -1151,7 +1310,6 @@ export function createIdealGraph(
1151
1310
  }
1152
1311
 
1153
1312
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1154
- let modifiedSourceBundles = new Set();
1155
1313
 
1156
1314
  if (config.disableSharedBundles === false) {
1157
1315
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
@@ -1165,7 +1323,13 @@ export function createIdealGraph(
1165
1323
  let numBundlesContributingToPRL = bundleIdsInGroup.reduce((count, b) => {
1166
1324
  let bundle = nullthrows(bundleGraph.getNode(b));
1167
1325
  invariant(bundle !== 'root');
1168
- return count + (bundle.bundleBehavior !== 'inline');
1326
+ return (
1327
+ count +
1328
+ Number(
1329
+ bundle.bundleBehavior !== 'inline' &&
1330
+ bundle.bundleBehavior !== 'inlineIsolated',
1331
+ )
1332
+ );
1169
1333
  }, 0);
1170
1334
 
1171
1335
  if (numBundlesContributingToPRL > config.maxParallelRequests) {
@@ -1200,7 +1364,9 @@ export function createIdealGraph(
1200
1364
  numBundlesContributingToPRL > config.maxParallelRequests
1201
1365
  ) {
1202
1366
  let bundleTuple = sharedBundlesInGroup.pop();
1367
+ // @ts-expect-error TS18048
1203
1368
  let bundleToRemove = bundleTuple.bundle;
1369
+ // @ts-expect-error TS18048
1204
1370
  let bundleIdToRemove = bundleTuple.id;
1205
1371
  //TODO add integration test where bundles in bunlde group > max parallel request limit & only remove a couple shared bundles
1206
1372
  // but total # bundles still exceeds limit due to non shared bundles
@@ -1257,51 +1423,206 @@ export function createIdealGraph(
1257
1423
  }
1258
1424
 
1259
1425
  function mergeBundles(
1260
- bundleGraph: IdealBundleGraph,
1261
1426
  bundleToKeepId: NodeId,
1262
1427
  bundleToRemoveId: NodeId,
1263
- assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1428
+ reason: string,
1264
1429
  ) {
1265
- let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
1266
- let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
1267
- invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1430
+ stats.trackMerge(bundleToKeepId, bundleToRemoveId, reason);
1431
+ let bundleToKeep = isNonRootBundle(
1432
+ bundleGraph.getNode(bundleToKeepId),
1433
+ `Bundle ${bundleToKeepId} not found`,
1434
+ );
1435
+ let bundleToRemove = isNonRootBundle(
1436
+ bundleGraph.getNode(bundleToRemoveId),
1437
+ `Bundle ${bundleToRemoveId} not found`,
1438
+ );
1439
+ modifiedSourceBundles.add(bundleToKeep);
1268
1440
  for (let asset of bundleToRemove.assets) {
1269
1441
  bundleToKeep.assets.add(asset);
1270
1442
  bundleToKeep.size += asset.stats.size;
1271
1443
 
1272
1444
  let newAssetReference = assetReference
1273
1445
  .get(asset)
1274
- .map(([dep, bundle]) =>
1446
+ .map(([dep, bundle]: [any, any]) =>
1275
1447
  bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1276
1448
  );
1277
1449
 
1450
+ // @ts-expect-error TS2345
1278
1451
  assetReference.set(asset, newAssetReference);
1279
1452
  }
1280
1453
 
1454
+ // Merge any internalized assets
1455
+ if (getFeatureFlag('supportWebpackChunkName')) {
1456
+ if (bundleToKeep.internalizedAssets != null) {
1457
+ if (bundleToRemove.internalizedAssets != null) {
1458
+ bundleToKeep.internalizedAssets.intersect(
1459
+ bundleToRemove.internalizedAssets,
1460
+ );
1461
+ } else {
1462
+ bundleToKeep.internalizedAssets.clear();
1463
+ }
1464
+ }
1465
+ } else {
1466
+ invariant(
1467
+ bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets,
1468
+ 'All shared bundles should have internalized assets',
1469
+ );
1470
+ bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
1471
+ }
1472
+
1473
+ // Merge and clean up source bundles
1281
1474
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
1282
1475
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1283
1476
  continue;
1284
1477
  }
1285
1478
 
1286
- bundleToKeep.sourceBundles.add(sourceBundleId);
1287
- bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1479
+ if (sourceBundleId !== bundleToKeepId) {
1480
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1481
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1482
+ }
1288
1483
  }
1289
1484
 
1290
- // Merge any internalized assets
1291
- if (bundleToRemove.internalizedAssets) {
1292
- if (bundleToKeep.internalizedAssets) {
1293
- bundleToKeep.internalizedAssets.union(
1294
- bundleToRemove.internalizedAssets,
1485
+ if (getFeatureFlag('supportWebpackChunkName')) {
1486
+ bundleToKeep.sourceBundles.delete(bundleToRemoveId);
1487
+
1488
+ for (let bundle of bundleGraph.getNodeIdsConnectedFrom(
1489
+ bundleToRemoveId,
1490
+ )) {
1491
+ let bundleNode = nullthrows(bundleGraph.getNode(bundle));
1492
+ if (bundleNode === 'root') {
1493
+ continue;
1494
+ }
1495
+
1496
+ // If the bundle is a source bundle, add it to the bundle to keep
1497
+ if (bundleNode.sourceBundles.has(bundleToRemoveId)) {
1498
+ bundleNode.sourceBundles.delete(bundleToRemoveId);
1499
+ if (bundle !== bundleToKeepId) {
1500
+ bundleNode.sourceBundles.add(bundleToKeepId);
1501
+ bundleGraph.addEdge(bundleToKeepId, bundle);
1502
+ }
1503
+ }
1504
+ }
1505
+
1506
+ // Merge bundle roots
1507
+ for (let bundleRoot of bundleToRemove.bundleRoots) {
1508
+ bundleToKeep.bundleRoots.add(bundleRoot);
1509
+ }
1510
+
1511
+ if (bundleToRemove.mainEntryAsset != null) {
1512
+ invariant(bundleToKeep.mainEntryAsset != null);
1513
+
1514
+ // Merge the bundles in bundle group
1515
+ let bundlesInRemoveBundleGroup =
1516
+ getBundlesForBundleGroup(bundleToRemoveId);
1517
+
1518
+ let removedBundleSharedBundles = new Set<NodeId>();
1519
+ for (let bundleIdInGroup of bundlesInRemoveBundleGroup) {
1520
+ if (bundleIdInGroup === bundleToRemoveId) {
1521
+ continue;
1522
+ }
1523
+ bundleGraph.addEdge(bundleToKeepId, bundleIdInGroup);
1524
+ removedBundleSharedBundles.add(bundleIdInGroup);
1525
+ }
1526
+
1527
+ if (getFeatureFlag('removeRedundantSharedBundles')) {
1528
+ // Merge any shared bundles that now have the same source bundles due to
1529
+ // the current bundle merge
1530
+ let sharedBundles = new DefaultMap<string, Array<NodeId>>(() => []);
1531
+ for (let bundleId of removedBundleSharedBundles) {
1532
+ let bundleNode = nullthrows(bundleGraph.getNode(bundleId));
1533
+ invariant(bundleNode !== 'root');
1534
+ if (
1535
+ bundleNode.mainEntryAsset != null ||
1536
+ bundleNode.manualSharedBundle != null
1537
+ ) {
1538
+ continue;
1539
+ }
1540
+
1541
+ let key =
1542
+ Array.from(bundleNode.sourceBundles)
1543
+ .filter((sourceBundle) => sourceBundle !== bundleToRemoveId)
1544
+ .sort()
1545
+ .join(',') +
1546
+ '.' +
1547
+ bundleNode.type;
1548
+
1549
+ sharedBundles.get(key).push(bundleId);
1550
+ }
1551
+
1552
+ for (let sharedBundlesToMerge of sharedBundles.values()) {
1553
+ if (sharedBundlesToMerge.length > 1) {
1554
+ let [firstBundleId, ...rest] = sharedBundlesToMerge;
1555
+ for (let bundleId of rest) {
1556
+ mergeBundles(firstBundleId, bundleId, 'redundant-shared');
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ // Remove old bundle group
1563
+ bundleGroupBundleIds.delete(bundleToRemoveId);
1564
+
1565
+ // Clean up bundle roots
1566
+ let bundleRootToRemoveNodeId = nullthrows(
1567
+ assetToBundleRootNodeId.get(
1568
+ nullthrows(bundleToRemove.mainEntryAsset),
1569
+ ),
1295
1570
  );
1296
- } else {
1297
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
1571
+ let bundleRootToKeepNodeId = nullthrows(
1572
+ assetToBundleRootNodeId.get(nullthrows(bundleToKeep.mainEntryAsset)),
1573
+ );
1574
+
1575
+ for (let nodeId of bundleRootGraph.getNodeIdsConnectedTo(
1576
+ bundleRootToRemoveNodeId,
1577
+ )) {
1578
+ bundleRootGraph.addEdge(nodeId, bundleRootToKeepNodeId);
1579
+ bundleRootGraph.removeEdge(nodeId, bundleRootToRemoveNodeId);
1580
+ }
1581
+
1582
+ for (let nodeId of bundleRootGraph.getNodeIdsConnectedFrom(
1583
+ bundleRootToRemoveNodeId,
1584
+ )) {
1585
+ bundleRootGraph.addEdge(bundleRootToKeepNodeId, nodeId);
1586
+ bundleRootGraph.removeEdge(bundleRootToRemoveNodeId, nodeId);
1587
+ }
1588
+
1589
+ bundleRoots.set(nullthrows(bundleToRemove.mainEntryAsset), [
1590
+ bundleToKeepId,
1591
+ bundleToKeepId,
1592
+ ]);
1593
+
1594
+ // Merge dependency bundle graph
1595
+ for (let dependencyNodeId of dependencyBundleGraph.getNodeIdsConnectedTo(
1596
+ dependencyBundleGraph.getNodeIdByContentKey(String(bundleToRemoveId)),
1597
+ ALL_EDGE_TYPES,
1598
+ )) {
1599
+ let dependencyNode = nullthrows(
1600
+ dependencyBundleGraph.getNode(dependencyNodeId),
1601
+ );
1602
+ invariant(dependencyNode.type === 'dependency');
1603
+
1604
+ // Add dependency to the bundle to keep
1605
+ dependencyBundleGraph.addEdge(
1606
+ dependencyNodeId,
1607
+ dependencyBundleGraph.getNodeIdByContentKey(String(bundleToKeepId)),
1608
+ dependencyPriorityEdges[dependencyNode.value.priority],
1609
+ );
1610
+ // Remove dependency from the bundle to remove
1611
+ dependencyBundleGraph.removeEdge(
1612
+ dependencyNodeId,
1613
+ dependencyBundleGraph.getNodeIdByContentKey(
1614
+ String(bundleToRemoveId),
1615
+ ),
1616
+ dependencyPriorityEdges[dependencyNode.value.priority],
1617
+ );
1618
+ }
1298
1619
  }
1299
1620
  }
1300
1621
 
1301
1622
  bundleGraph.removeNode(bundleToRemoveId);
1302
1623
  }
1303
1624
 
1304
- function mergeOverlapBundles() {
1625
+ function mergeSharedBundles(mergeConfig: SharedBundleMergeCandidates) {
1305
1626
  // Find all shared bundles
1306
1627
  let sharedBundles = new Set<NodeId>();
1307
1628
  bundleGraph.traverse((nodeId) => {
@@ -1331,22 +1652,188 @@ export function createIdealGraph(
1331
1652
  let clusters = findMergeCandidates(
1332
1653
  bundleGraph,
1333
1654
  Array.from(sharedBundles),
1334
- config.sharedBundleMergeThreshold,
1655
+ mergeConfig.map(
1656
+ (config): MergeGroup => ({
1657
+ ...config,
1658
+ sourceBundles: config.sourceBundles?.map((assetMatch: string) => {
1659
+ let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
1660
+
1661
+ if (sourceBundleNodeId == null) {
1662
+ throw new Error(
1663
+ `Source bundle ${assetMatch} not found in merge source bundle lookup`,
1664
+ );
1665
+ }
1666
+
1667
+ return sourceBundleNodeId;
1668
+ }),
1669
+ }),
1670
+ ),
1335
1671
  );
1336
1672
 
1673
+ let mergedBundles = new Set();
1674
+
1337
1675
  for (let cluster of clusters) {
1338
1676
  let [mergeTarget, ...rest] = cluster;
1339
1677
 
1340
1678
  for (let bundleIdToMerge of rest) {
1341
- mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1679
+ mergeBundles(mergeTarget, bundleIdToMerge, 'shared-merge');
1680
+ }
1681
+
1682
+ mergedBundles.add(mergeTarget);
1683
+ }
1684
+
1685
+ if (getFeatureFlag('supportWebpackChunkName')) {
1686
+ return mergedBundles;
1687
+ }
1688
+ }
1689
+
1690
+ function mergeAsyncBundles({
1691
+ bundleSize,
1692
+ maxOverfetchSize,
1693
+ ignore,
1694
+ }: AsyncBundleMerge) {
1695
+ let mergeCandidates = [];
1696
+ let ignoreRegexes = ignore?.map((glob) => globToRegex(glob)) ?? [];
1697
+
1698
+ let isIgnored = (bundle: Bundle) => {
1699
+ if (!bundle.mainEntryAsset) {
1700
+ return false;
1701
+ }
1702
+
1703
+ let mainEntryFilePath = path.relative(
1704
+ config.projectRoot,
1705
+ nullthrows(bundle.mainEntryAsset).filePath,
1706
+ );
1707
+
1708
+ return ignoreRegexes.some((regex) => regex.test(mainEntryFilePath));
1709
+ };
1710
+
1711
+ for (let [_bundleRootAsset, [bundleRootBundleId]] of bundleRoots) {
1712
+ let bundleRootBundle = nullthrows(
1713
+ bundleGraph.getNode(bundleRootBundleId),
1714
+ );
1715
+ invariant(bundleRootBundle !== 'root');
1716
+
1717
+ if (
1718
+ bundleRootBundle.type === 'js' &&
1719
+ bundleRootBundle.bundleBehavior !== 'inline' &&
1720
+ bundleRootBundle.bundleBehavior !== 'inlineIsolated' &&
1721
+ bundleRootBundle.size <= bundleSize &&
1722
+ !isIgnored(bundleRootBundle)
1723
+ ) {
1724
+ mergeCandidates.push(bundleRootBundleId);
1725
+ }
1726
+ }
1727
+
1728
+ let candidates = [];
1729
+ for (let i = 0; i < mergeCandidates.length; i++) {
1730
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
1731
+ let a = mergeCandidates[i];
1732
+ let b = mergeCandidates[j];
1733
+ if (a === b) continue; // Skip self-comparison
1734
+
1735
+ candidates.push(scoreAsyncMerge(a, b, maxOverfetchSize));
1736
+ }
1737
+ }
1738
+
1739
+ let sortByScore = (
1740
+ a: AsyncBundleMergeCandidate,
1741
+ b: AsyncBundleMergeCandidate,
1742
+ ) => {
1743
+ let diff = a.score - b.score;
1744
+ if (diff > 0) {
1745
+ return 1;
1746
+ } else if (diff < 0) {
1747
+ return -1;
1748
+ }
1749
+ return 0;
1750
+ };
1751
+
1752
+ candidates = candidates
1753
+ .filter(
1754
+ ({overfetchSize, score}) =>
1755
+ overfetchSize <= maxOverfetchSize && score > 0,
1756
+ )
1757
+ .sort(sortByScore);
1758
+
1759
+ // Tracks the bundles that have been merged
1760
+ let merged = new Set<NodeId>();
1761
+ // Tracks the deleted bundles to the bundle they were merged into.
1762
+ let mergeRemap = new Map<NodeId, NodeId>();
1763
+ // Tracks the bundles that have been rescored and added back into the
1764
+ // candidates.
1765
+ let rescored = new DefaultMap<NodeId, Set<NodeId>>(() => new Set());
1766
+
1767
+ do {
1768
+ let [a, b] = nullthrows(candidates.pop()).bundleIds;
1769
+
1770
+ if (
1771
+ bundleGraph.hasNode(a) &&
1772
+ bundleGraph.hasNode(b) &&
1773
+ ((!merged.has(a) && !merged.has(b)) || rescored.get(a).has(b))
1774
+ ) {
1775
+ mergeRemap.set(b, a);
1776
+ merged.add(a);
1777
+ rescored.get(a).clear();
1778
+
1779
+ mergeBundles(a, b, 'async-merge');
1780
+ continue;
1342
1781
  }
1782
+
1783
+ // One or both of the bundles have been previously merged, so we'll
1784
+ // rescore and add the result back into the list of candidates.
1785
+ let getMergedBundleId = (bundleId: NodeId): NodeId | undefined => {
1786
+ let seen = new Set<NodeId>();
1787
+ while (!bundleGraph.hasNode(bundleId) && !seen.has(bundleId)) {
1788
+ seen.add(bundleId);
1789
+ bundleId = nullthrows(mergeRemap.get(bundleId));
1790
+ }
1791
+
1792
+ if (!bundleGraph.hasNode(bundleId)) {
1793
+ return;
1794
+ }
1795
+
1796
+ return bundleId;
1797
+ };
1798
+
1799
+ // Map a and b to their merged bundle ids if they've already been merged
1800
+ let currentA = getMergedBundleId(a);
1801
+ let currentB = getMergedBundleId(b);
1802
+
1803
+ if (
1804
+ !currentA ||
1805
+ !currentB ||
1806
+ // Bundles are already merged
1807
+ currentA === currentB
1808
+ ) {
1809
+ // This combiniation is not valid, so we skip it.
1810
+ continue;
1811
+ }
1812
+
1813
+ let candidate = scoreAsyncMerge(currentA, currentB, maxOverfetchSize);
1814
+
1815
+ if (candidate.overfetchSize <= maxOverfetchSize && candidate.score > 0) {
1816
+ sortedArray.add(candidates, candidate, sortByScore);
1817
+ rescored.get(currentA).add(currentB);
1818
+ }
1819
+ } while (candidates.length > 0);
1820
+ }
1821
+
1822
+ function getBundle(bundleId: NodeId): Bundle {
1823
+ let bundle = bundleGraph.getNode(bundleId);
1824
+ if (bundle === 'root') {
1825
+ throw new Error(`Cannot access root bundle`);
1826
+ }
1827
+ if (bundle == null) {
1828
+ throw new Error(`Bundle ${bundleId} not found in bundle graph`);
1343
1829
  }
1830
+
1831
+ return bundle;
1344
1832
  }
1345
1833
 
1346
- function getBigIntFromContentKey(contentKey) {
1834
+ function getBigIntFromContentKey(contentKey: string) {
1347
1835
  let b = Buffer.alloc(64);
1348
1836
  b.write(contentKey);
1349
- // $FlowFixMe Flow doesn't have BigInt types in this version
1350
1837
  return b.readBigInt64BE();
1351
1838
  }
1352
1839
  // Fix asset order in source bundles as they are likely now incorrect after shared bundle deletion
@@ -1373,14 +1860,86 @@ export function createIdealGraph(
1373
1860
  bundleRootGraph.removeNode(bundleRootId);
1374
1861
  }
1375
1862
  }
1376
- function getBundlesForBundleGroup(bundleGroupId) {
1377
- let bundlesInABundleGroup = [];
1863
+ function getBundlesForBundleGroup(bundleGroupId: NodeId) {
1864
+ let bundlesInABundleGroup: Array<NodeId> = [];
1378
1865
  bundleGraph.traverse((nodeId) => {
1379
1866
  bundlesInABundleGroup.push(nodeId);
1380
1867
  }, bundleGroupId);
1381
1868
  return bundlesInABundleGroup;
1382
1869
  }
1383
1870
 
1871
+ interface AsyncBundleMergeCandidate {
1872
+ overfetchSize: number;
1873
+ score: number;
1874
+ bundleIds: number[];
1875
+ }
1876
+ function scoreAsyncMerge(
1877
+ bundleAId: NodeId,
1878
+ bundleBId: NodeId,
1879
+ maxOverfetchSize: number,
1880
+ ): AsyncBundleMergeCandidate {
1881
+ let bundleGroupA = new Set(getBundlesForBundleGroup(bundleAId));
1882
+ let bundleGroupB = new Set(getBundlesForBundleGroup(bundleBId));
1883
+
1884
+ let overlapSize = 0;
1885
+ let overfetchSize = 0;
1886
+
1887
+ for (let bundleId of new Set([...bundleGroupA, ...bundleGroupB])) {
1888
+ let bundle = getBundle(bundleId);
1889
+
1890
+ if (bundleGroupA.has(bundleId) && bundleGroupB.has(bundleId)) {
1891
+ overlapSize += bundle.size;
1892
+ } else {
1893
+ overfetchSize += bundle.size;
1894
+ }
1895
+ }
1896
+
1897
+ let overlapPercent = overlapSize / (overfetchSize + overlapSize);
1898
+
1899
+ let bundleAParents = getBundleParents(bundleAId);
1900
+ let bundleBParents = getBundleParents(bundleBId);
1901
+
1902
+ let sharedParentOverlap = 0;
1903
+ let sharedParentMismatch = 0;
1904
+
1905
+ for (let bundleId of new Set([...bundleAParents, ...bundleBParents])) {
1906
+ if (bundleAParents.has(bundleId) && bundleBParents.has(bundleId)) {
1907
+ sharedParentOverlap++;
1908
+ } else {
1909
+ sharedParentMismatch++;
1910
+ }
1911
+ }
1912
+
1913
+ let overfetchScore = overfetchSize / maxOverfetchSize;
1914
+ let sharedParentPercent =
1915
+ sharedParentOverlap / (sharedParentOverlap + sharedParentMismatch);
1916
+ let score = sharedParentPercent + overlapPercent + overfetchScore;
1917
+
1918
+ return {
1919
+ overfetchSize,
1920
+ score,
1921
+ bundleIds: [bundleAId, bundleBId],
1922
+ };
1923
+ }
1924
+
1925
+ function getBundleParents(bundleId: NodeId): Set<NodeId> {
1926
+ let parents = new Set<NodeId>();
1927
+ let {bundleRoots} = getBundle(bundleId);
1928
+
1929
+ for (let bundleRoot of bundleRoots) {
1930
+ let bundleRootNodeId = nullthrows(
1931
+ assetToBundleRootNodeId.get(bundleRoot),
1932
+ );
1933
+ for (let parentId of bundleRootGraph.getNodeIdsConnectedTo(
1934
+ bundleRootNodeId,
1935
+ ALL_EDGE_TYPES,
1936
+ )) {
1937
+ parents.add(parentId);
1938
+ }
1939
+ }
1940
+ return parents;
1941
+ }
1942
+
1384
1943
  function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle {
1385
1944
  let bundle = bundleGraph.getNode(
1386
1945
  nullthrows(bundleRoots.get(bundleRoot))[0],
@@ -1445,6 +2004,12 @@ export function createIdealGraph(
1445
2004
  bundleGraph.removeNode(bundleId);
1446
2005
  }
1447
2006
 
2007
+ stats.report((bundleId) => {
2008
+ let bundle = bundleGraph.getNode(bundleId);
2009
+ invariant(bundle !== 'root');
2010
+ return bundle;
2011
+ });
2012
+
1448
2013
  return {
1449
2014
  assets,
1450
2015
  bundleGraph,
@@ -1455,23 +2020,24 @@ export function createIdealGraph(
1455
2020
  };
1456
2021
  }
1457
2022
 
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 {
2023
+ function createBundle(opts: {
2024
+ asset?: Asset;
2025
+ bundleBehavior?: BundleBehavior | null | undefined;
2026
+ env?: Environment;
2027
+ manualSharedBundle?: string | null | undefined;
2028
+ needsStableName?: boolean;
2029
+ sourceBundles?: Set<NodeId>;
2030
+ target: Target;
2031
+ type?: string;
2032
+ uniqueKey?: string;
2033
+ }): Bundle {
1469
2034
  if (opts.asset == null) {
1470
2035
  return {
1471
2036
  assets: new Set(),
1472
2037
  bundleBehavior: opts.bundleBehavior,
1473
2038
  env: nullthrows(opts.env),
1474
2039
  mainEntryAsset: null,
2040
+ bundleRoots: new Set(),
1475
2041
  manualSharedBundle: opts.manualSharedBundle,
1476
2042
  needsStableName: Boolean(opts.needsStableName),
1477
2043
  size: 0,
@@ -1488,6 +2054,7 @@ function createBundle(opts: {|
1488
2054
  bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior,
1489
2055
  env: opts.env ?? asset.env,
1490
2056
  mainEntryAsset: asset,
2057
+ bundleRoots: new Set([asset]),
1491
2058
  manualSharedBundle: opts.manualSharedBundle,
1492
2059
  needsStableName: Boolean(opts.needsStableName),
1493
2060
  size: asset.stats.size,