@atlaspack/bundler-default 2.14.5-canary.2 → 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,36 +22,43 @@ import {DefaultMap, globToRegex} from '@atlaspack/utils';
23
22
  import invariant from 'assert';
24
23
  import nullthrows from 'nullthrows';
25
24
 
26
- 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';
27
32
 
28
33
  /* BundleRoot - An asset that is the main entry of a Bundle. */
29
34
  type BundleRoot = Asset;
30
35
 
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
- |};
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
+ };
45
51
 
46
52
  export type DependencyBundleGraph = ContentGraph<
47
- | {|
48
- value: Bundle,
49
- type: 'bundle',
50
- |}
51
- | {|
52
- value: Dependency,
53
- type: 'dependency',
54
- |},
55
- number,
53
+ | {
54
+ value: Bundle;
55
+ type: 'bundle';
56
+ }
57
+ | {
58
+ value: Dependency;
59
+ type: 'dependency';
60
+ },
61
+ number
56
62
  >;
57
63
 
58
64
  const dependencyPriorityEdges = {
@@ -60,29 +66,39 @@ const dependencyPriorityEdges = {
60
66
  parallel: 2,
61
67
  lazy: 3,
62
68
  conditional: 4,
63
- };
69
+ } as const;
64
70
 
65
71
  export const idealBundleGraphEdges = Object.freeze({
66
72
  default: 1,
67
73
  conditional: 2,
68
74
  });
69
75
 
70
- type IdealBundleGraph = Graph<
76
+ export type IdealBundleGraph = Graph<
71
77
  Bundle | 'root',
72
- $Values<typeof idealBundleGraphEdges>,
78
+ (typeof idealBundleGraphEdges)[keyof typeof idealBundleGraphEdges]
73
79
  >;
74
80
 
75
81
  // IdealGraph is the structure we will pass to decorate,
76
82
  // which mutates the assetGraph into the bundleGraph we would
77
83
  // 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
- |};
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
+ }
86
102
 
87
103
  export function createIdealGraph(
88
104
  assetGraph: MutableBundleGraph,
@@ -96,8 +112,9 @@ export function createIdealGraph(
96
112
  let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph();
97
113
  let assetReference: DefaultMap<
98
114
  Asset,
99
- Array<[Dependency, Bundle]>,
115
+ Array<[Dependency, Bundle]>
100
116
  > = new DefaultMap(() => []);
117
+ let stats = new Stats(config.projectRoot);
101
118
 
102
119
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
103
120
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -110,8 +127,9 @@ export function createIdealGraph(
110
127
  };
111
128
  // Graph that models bundleRoots, with parallel & async deps only to inform reachability
112
129
  let bundleRootGraph: Graph<
113
- number, // asset index
114
- $Values<typeof bundleRootEdgeTypes>,
130
+ // asset index
131
+ number,
132
+ (typeof bundleRootEdgeTypes)[keyof typeof bundleRootEdgeTypes]
115
133
  > = new Graph();
116
134
  let assetToBundleRootNodeId = new Map<BundleRoot, number>();
117
135
 
@@ -145,7 +163,7 @@ export function createIdealGraph(
145
163
  bundleGroupBundleIds.add(nodeId);
146
164
  }
147
165
 
148
- let assets = [];
166
+ let assets: Array<Asset> = [];
149
167
  let assetToIndex = new Map<Asset, number>();
150
168
 
151
169
  function makeManualAssetToConfigLookup() {
@@ -160,6 +178,7 @@ export function createIdealGraph(
160
178
 
161
179
  for (let c of config.manualSharedBundles) {
162
180
  if (c.root != null) {
181
+ // @ts-expect-error TS2345
163
182
  parentsToConfig.get(path.join(config.projectRoot, c.root)).push(c);
164
183
  }
165
184
  }
@@ -203,17 +222,17 @@ export function createIdealGraph(
203
222
  config.projectRoot,
204
223
  node.value.filePath,
205
224
  );
206
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
207
- return;
208
- }
209
225
 
210
226
  // We track all matching MSB's for constant modules as they are never duplicated
211
227
  // and need to be assigned to all matching bundles
212
228
  if (node.value.meta.isConstantModule === true) {
229
+ // @ts-expect-error TS2345
213
230
  constantModuleToMSB.get(node.value).push(c);
214
231
  }
215
- manualAssetToConfig.set(node.value, c);
216
- return;
232
+
233
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
234
+ manualAssetToConfig.set(node.value, c);
235
+ }
217
236
  }
218
237
 
219
238
  if (
@@ -241,9 +260,19 @@ export function createIdealGraph(
241
260
  makeManualAssetToConfigLookup();
242
261
  let manualBundleToInternalizedAsset: DefaultMap<
243
262
  NodeId,
244
- Array<Asset>,
263
+ Array<Asset>
245
264
  > = new DefaultMap(() => []);
246
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
+
247
276
  /**
248
277
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
249
278
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -251,7 +280,27 @@ export function createIdealGraph(
251
280
  */
252
281
  assetGraph.traverse(
253
282
  {
254
- 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
+ ) {
255
304
  if (node.type === 'asset') {
256
305
  if (
257
306
  context?.type === 'dependency' &&
@@ -311,7 +360,8 @@ export function createIdealGraph(
311
360
  dependency.priority === 'lazy' ||
312
361
  (getFeatureFlag('conditionalBundlingApi') &&
313
362
  node.value.priority === 'conditional') ||
314
- 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'
315
365
  ) {
316
366
  if (bundleId == null) {
317
367
  let firstBundleGroup = nullthrows(
@@ -324,7 +374,9 @@ export function createIdealGraph(
324
374
  dependency.bundleBehavior ?? childAsset.bundleBehavior,
325
375
  needsStableName:
326
376
  dependency.bundleBehavior === 'inline' ||
327
- childAsset.bundleBehavior === 'inline'
377
+ childAsset.bundleBehavior === 'inline' ||
378
+ dependency.bundleBehavior === 'inlineIsolated' ||
379
+ childAsset.bundleBehavior === 'inlineIsolated'
328
380
  ? false
329
381
  : dependency.isEntry || dependency.needsStableName,
330
382
  target: firstBundleGroup.target,
@@ -334,6 +386,14 @@ export function createIdealGraph(
334
386
  bundleRoots.set(childAsset, [bundleId, bundleId]);
335
387
  bundleGroupBundleIds.add(bundleId);
336
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
+ }
337
397
  if (manualSharedObject) {
338
398
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
339
399
  // since MSBs should not have main entry assets
@@ -348,7 +408,8 @@ export function createIdealGraph(
348
408
  if (
349
409
  // If this dependency requests isolated, but the bundle is not,
350
410
  // make the bundle isolated for all uses.
351
- dependency.bundleBehavior === 'isolated' &&
411
+ (dependency.bundleBehavior === 'isolated' ||
412
+ dependency.bundleBehavior === 'inlineIsolated') &&
352
413
  bundle.bundleBehavior == null
353
414
  ) {
354
415
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -370,6 +431,7 @@ export function createIdealGraph(
370
431
  type: 'bundle',
371
432
  },
372
433
  ),
434
+ // @ts-expect-error TS7053
373
435
  dependencyPriorityEdges[dependency.priority],
374
436
  );
375
437
 
@@ -431,6 +493,7 @@ export function createIdealGraph(
431
493
  needsStableName:
432
494
  childAsset.bundleBehavior === 'inline' ||
433
495
  dependency.bundleBehavior === 'inline' ||
496
+ dependency.bundleBehavior === 'inlineIsolated' ||
434
497
  (dependency.priority === 'parallel' &&
435
498
  !dependency.needsStableName)
436
499
  ? false
@@ -444,7 +507,8 @@ export function createIdealGraph(
444
507
  if (
445
508
  // If this dependency requests isolated, but the bundle is not,
446
509
  // make the bundle isolated for all uses.
447
- dependency.bundleBehavior === 'isolated' &&
510
+ (dependency.bundleBehavior === 'isolated' ||
511
+ dependency.bundleBehavior === 'inlineIsolated') &&
448
512
  bundle.bundleBehavior == null
449
513
  ) {
450
514
  bundle.bundleBehavior = dependency.bundleBehavior;
@@ -481,6 +545,7 @@ export function createIdealGraph(
481
545
 
482
546
  assetReference.get(childAsset).push([dependency, bundle]);
483
547
  } else {
548
+ // @ts-expect-error TS2322
484
549
  bundleId = null;
485
550
  }
486
551
  if (manualSharedObject && bundleId != null) {
@@ -488,6 +553,7 @@ export function createIdealGraph(
488
553
  // add the asset if it doesn't already have it and set key
489
554
 
490
555
  invariant(
556
+ // @ts-expect-error TS2367
491
557
  bundle !== 'root' && bundle != null && bundleId != null,
492
558
  );
493
559
 
@@ -514,7 +580,8 @@ export function createIdealGraph(
514
580
  }
515
581
  return node;
516
582
  },
517
- exit(node) {
583
+ // @ts-expect-error TS2322
584
+ exit(node: BundleGraphTraversable) {
518
585
  if (stack[stack.length - 1]?.[0] === node.value) {
519
586
  stack.pop();
520
587
  }
@@ -563,20 +630,20 @@ export function createIdealGraph(
563
630
 
564
631
  // reachableRoots is an array of bit sets for each asset. Each bit set
565
632
  // indicates which bundle roots are reachable from that asset synchronously.
566
- let reachableRoots = [];
633
+ let reachableRoots: Array<BitSet> = [];
567
634
  for (let i = 0; i < assets.length; i++) {
568
635
  reachableRoots.push(new BitSet(bundleRootGraph.nodes.length));
569
636
  }
570
637
 
571
638
  // reachableAssets is the inverse mapping of reachableRoots. For each bundle root,
572
639
  // it contains a bit set that indicates which assets are reachable from it.
573
- let reachableAssets = [];
640
+ let reachableAssets: Array<BitSet> = [];
574
641
 
575
642
  // ancestorAssets maps bundle roots to the set of all assets available to it at runtime,
576
643
  // including in earlier parallel bundles. These are intersected through all paths to
577
644
  // the bundle to ensure that the available assets are always present no matter in which
578
645
  // order the bundles are loaded.
579
- let ancestorAssets = [];
646
+ let ancestorAssets: Array<null | BitSet> = [];
580
647
 
581
648
  let inlineConstantDeps = new DefaultMap(() => new Set());
582
649
 
@@ -687,7 +754,10 @@ export function createIdealGraph(
687
754
  // not true that a bundle's available assets = all assets of all the bundleGroups
688
755
  // it belongs to. It's the intersection of those sets.
689
756
  let available;
690
- if (bundleRoot.bundleBehavior === 'isolated') {
757
+ if (
758
+ bundleRoot.bundleBehavior === 'isolated' ||
759
+ bundleRoot.bundleBehavior === 'inlineIsolated'
760
+ ) {
691
761
  available = new BitSet(assets.length);
692
762
  } else {
693
763
  available = nullthrows(ancestorAssets[nodeId]).clone();
@@ -771,7 +841,8 @@ export function createIdealGraph(
771
841
 
772
842
  let parentRoots = bundleRootGraph.getNodeIdsConnectedTo(id, ALL_EDGE_TYPES);
773
843
  let canDelete =
774
- getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated';
844
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'isolated' &&
845
+ getBundleFromBundleRoot(bundleRoot).bundleBehavior !== 'inlineIsolated';
775
846
  if (parentRoots.length === 0) continue;
776
847
  for (let parentId of parentRoots) {
777
848
  if (parentId === rootNodeId) {
@@ -805,8 +876,11 @@ export function createIdealGraph(
805
876
 
806
877
  function assignInlineConstants(parentAsset: Asset, bundle: Bundle) {
807
878
  for (let inlineConstant of inlineConstantDeps.get(parentAsset)) {
879
+ // @ts-expect-error TS2345
808
880
  if (!bundle.assets.has(inlineConstant)) {
881
+ // @ts-expect-error TS2345
809
882
  bundle.assets.add(inlineConstant);
883
+ // @ts-expect-error TS18046
810
884
  bundle.size += inlineConstant.stats.size;
811
885
  }
812
886
  }
@@ -848,7 +922,8 @@ export function createIdealGraph(
848
922
  !a.isBundleSplittable ||
849
923
  (bundleRoots.get(a) &&
850
924
  (getBundleFromBundleRoot(a).needsStableName ||
851
- getBundleFromBundleRoot(a).bundleBehavior === 'isolated'))
925
+ getBundleFromBundleRoot(a).bundleBehavior === 'isolated' ||
926
+ getBundleFromBundleRoot(a).bundleBehavior === 'inlineIsolated'))
852
927
  ) {
853
928
  // Add asset to non-splittable bundles.
854
929
  addAssetToBundleRoot(asset, a);
@@ -866,7 +941,7 @@ export function createIdealGraph(
866
941
  let bundle;
867
942
  let bundleId;
868
943
  let manualSharedBundleKey = manualSharedObject.name + ',' + asset.type;
869
- let sourceBundles = [];
944
+ let sourceBundles: Array<NodeId> = [];
870
945
  reachable.forEach((id) => {
871
946
  sourceBundles.push(nullthrows(bundleRoots.get(assets[id]))[0]);
872
947
  });
@@ -981,7 +1056,7 @@ export function createIdealGraph(
981
1056
  });
982
1057
  }
983
1058
 
984
- let reachableArray = [];
1059
+ let reachableArray: Array<Asset> = [];
985
1060
  reachable.forEach((id) => {
986
1061
  reachableArray.push(assets[id]);
987
1062
  });
@@ -1056,6 +1131,8 @@ export function createIdealGraph(
1056
1131
  }
1057
1132
 
1058
1133
  let manualSharedBundleIds = new Set([...manualSharedMap.values()]);
1134
+ let modifiedSourceBundles = new Set<Bundle>();
1135
+
1059
1136
  // Step split manual shared bundles for those that have the "split" property set
1060
1137
  let remainderMap = new DefaultMap(() => []);
1061
1138
  for (let id of manualSharedMap.values()) {
@@ -1074,9 +1151,9 @@ export function createIdealGraph(
1074
1151
  if (modNum != null) {
1075
1152
  for (let a of [...manualBundle.assets]) {
1076
1153
  let numRep = getBigIntFromContentKey(a.id);
1077
- // $FlowFixMe Flow doesn't know about BigInt
1078
1154
  let r = Number(numRep % BigInt(modNum));
1079
1155
 
1156
+ // @ts-expect-error TS2345
1080
1157
  remainderMap.get(r).push(a);
1081
1158
  }
1082
1159
 
@@ -1099,8 +1176,10 @@ export function createIdealGraph(
1099
1176
  }
1100
1177
  for (let sp of remainderMap.get(i)) {
1101
1178
  bundle.assets.add(sp);
1179
+ // @ts-expect-error TS2339
1102
1180
  bundle.size += sp.stats.size;
1103
1181
  manualBundle.assets.delete(sp);
1182
+ // @ts-expect-error TS2339
1104
1183
  manualBundle.size -= sp.stats.size;
1105
1184
  }
1106
1185
  }
@@ -1113,6 +1192,7 @@ export function createIdealGraph(
1113
1192
  // match multiple MSB's
1114
1193
  for (let [asset, msbs] of constantModuleToMSB.entries()) {
1115
1194
  for (let manualSharedObject of msbs) {
1195
+ // @ts-expect-error TS2339
1116
1196
  let bundleId = manualSharedMap.get(manualSharedObject.name + ',js');
1117
1197
  if (bundleId == null) continue;
1118
1198
  let bundle = nullthrows(bundleGraph.getNode(bundleId));
@@ -1121,13 +1201,99 @@ export function createIdealGraph(
1121
1201
  'We tried to use the root incorrectly',
1122
1202
  );
1123
1203
 
1204
+ // @ts-expect-error TS2345
1124
1205
  if (!bundle.assets.has(asset)) {
1206
+ // @ts-expect-error TS2345
1125
1207
  bundle.assets.add(asset);
1208
+ // @ts-expect-error TS18046
1126
1209
  bundle.size += asset.stats.size;
1127
1210
  }
1128
1211
  }
1129
1212
  }
1130
1213
 
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);
1295
+ }
1296
+
1131
1297
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
1132
1298
  // their source bundles, and remove the bundle.
1133
1299
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -1143,9 +1309,8 @@ export function createIdealGraph(
1143
1309
  }
1144
1310
  }
1145
1311
 
1146
- let modifiedSourceBundles = new Set();
1147
-
1148
1312
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1313
+
1149
1314
  if (config.disableSharedBundles === false) {
1150
1315
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
1151
1316
  // Find shared bundles in this bundle group.
@@ -1158,7 +1323,13 @@ export function createIdealGraph(
1158
1323
  let numBundlesContributingToPRL = bundleIdsInGroup.reduce((count, b) => {
1159
1324
  let bundle = nullthrows(bundleGraph.getNode(b));
1160
1325
  invariant(bundle !== 'root');
1161
- return count + (bundle.bundleBehavior !== 'inline');
1326
+ return (
1327
+ count +
1328
+ Number(
1329
+ bundle.bundleBehavior !== 'inline' &&
1330
+ bundle.bundleBehavior !== 'inlineIsolated',
1331
+ )
1332
+ );
1162
1333
  }, 0);
1163
1334
 
1164
1335
  if (numBundlesContributingToPRL > config.maxParallelRequests) {
@@ -1193,7 +1364,9 @@ export function createIdealGraph(
1193
1364
  numBundlesContributingToPRL > config.maxParallelRequests
1194
1365
  ) {
1195
1366
  let bundleTuple = sharedBundlesInGroup.pop();
1367
+ // @ts-expect-error TS18048
1196
1368
  let bundleToRemove = bundleTuple.bundle;
1369
+ // @ts-expect-error TS18048
1197
1370
  let bundleIdToRemove = bundleTuple.id;
1198
1371
  //TODO add integration test where bundles in bunlde group > max parallel request limit & only remove a couple shared bundles
1199
1372
  // but total # bundles still exceeds limit due to non shared bundles
@@ -1249,10 +1422,418 @@ export function createIdealGraph(
1249
1422
  }
1250
1423
  }
1251
1424
 
1252
- function getBigIntFromContentKey(contentKey) {
1425
+ function mergeBundles(
1426
+ bundleToKeepId: NodeId,
1427
+ bundleToRemoveId: NodeId,
1428
+ reason: string,
1429
+ ) {
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);
1440
+ for (let asset of bundleToRemove.assets) {
1441
+ bundleToKeep.assets.add(asset);
1442
+ bundleToKeep.size += asset.stats.size;
1443
+
1444
+ let newAssetReference = assetReference
1445
+ .get(asset)
1446
+ .map(([dep, bundle]: [any, any]) =>
1447
+ bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1448
+ );
1449
+
1450
+ // @ts-expect-error TS2345
1451
+ assetReference.set(asset, newAssetReference);
1452
+ }
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
1474
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
1475
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1476
+ continue;
1477
+ }
1478
+
1479
+ if (sourceBundleId !== bundleToKeepId) {
1480
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1481
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1482
+ }
1483
+ }
1484
+
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
+ ),
1570
+ );
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
+ }
1619
+ }
1620
+ }
1621
+
1622
+ bundleGraph.removeNode(bundleToRemoveId);
1623
+ }
1624
+
1625
+ function mergeSharedBundles(mergeConfig: SharedBundleMergeCandidates) {
1626
+ // Find all shared bundles
1627
+ let sharedBundles = new Set<NodeId>();
1628
+ bundleGraph.traverse((nodeId) => {
1629
+ let bundle = bundleGraph.getNode(nodeId);
1630
+
1631
+ if (!bundle) {
1632
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
1633
+ }
1634
+
1635
+ if (bundle === 'root') {
1636
+ return;
1637
+ }
1638
+
1639
+ // Only consider JS shared bundles and non-reused bundles.
1640
+ // These count potentially be considered for merging in future but they're
1641
+ // more complicated to merge
1642
+ if (
1643
+ bundle.sourceBundles.size > 0 &&
1644
+ bundle.manualSharedBundle == null &&
1645
+ !bundle.mainEntryAsset &&
1646
+ bundle.type === 'js'
1647
+ ) {
1648
+ sharedBundles.add(nodeId);
1649
+ }
1650
+ });
1651
+
1652
+ let clusters = findMergeCandidates(
1653
+ bundleGraph,
1654
+ Array.from(sharedBundles),
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
+ ),
1671
+ );
1672
+
1673
+ let mergedBundles = new Set();
1674
+
1675
+ for (let cluster of clusters) {
1676
+ let [mergeTarget, ...rest] = cluster;
1677
+
1678
+ for (let bundleIdToMerge of rest) {
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;
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`);
1829
+ }
1830
+
1831
+ return bundle;
1832
+ }
1833
+
1834
+ function getBigIntFromContentKey(contentKey: string) {
1253
1835
  let b = Buffer.alloc(64);
1254
1836
  b.write(contentKey);
1255
- // $FlowFixMe Flow doesn't have BigInt types in this version
1256
1837
  return b.readBigInt64BE();
1257
1838
  }
1258
1839
  // Fix asset order in source bundles as they are likely now incorrect after shared bundle deletion
@@ -1279,14 +1860,86 @@ export function createIdealGraph(
1279
1860
  bundleRootGraph.removeNode(bundleRootId);
1280
1861
  }
1281
1862
  }
1282
- function getBundlesForBundleGroup(bundleGroupId) {
1283
- let bundlesInABundleGroup = [];
1863
+ function getBundlesForBundleGroup(bundleGroupId: NodeId) {
1864
+ let bundlesInABundleGroup: Array<NodeId> = [];
1284
1865
  bundleGraph.traverse((nodeId) => {
1285
1866
  bundlesInABundleGroup.push(nodeId);
1286
1867
  }, bundleGroupId);
1287
1868
  return bundlesInABundleGroup;
1288
1869
  }
1289
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
+
1290
1943
  function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle {
1291
1944
  let bundle = bundleGraph.getNode(
1292
1945
  nullthrows(bundleRoots.get(bundleRoot))[0],
@@ -1351,6 +2004,12 @@ export function createIdealGraph(
1351
2004
  bundleGraph.removeNode(bundleId);
1352
2005
  }
1353
2006
 
2007
+ stats.report((bundleId) => {
2008
+ let bundle = bundleGraph.getNode(bundleId);
2009
+ invariant(bundle !== 'root');
2010
+ return bundle;
2011
+ });
2012
+
1354
2013
  return {
1355
2014
  assets,
1356
2015
  bundleGraph,
@@ -1361,23 +2020,24 @@ export function createIdealGraph(
1361
2020
  };
1362
2021
  }
1363
2022
 
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 {
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 {
1375
2034
  if (opts.asset == null) {
1376
2035
  return {
1377
2036
  assets: new Set(),
1378
2037
  bundleBehavior: opts.bundleBehavior,
1379
2038
  env: nullthrows(opts.env),
1380
2039
  mainEntryAsset: null,
2040
+ bundleRoots: new Set(),
1381
2041
  manualSharedBundle: opts.manualSharedBundle,
1382
2042
  needsStableName: Boolean(opts.needsStableName),
1383
2043
  size: 0,
@@ -1394,6 +2054,7 @@ function createBundle(opts: {|
1394
2054
  bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior,
1395
2055
  env: opts.env ?? asset.env,
1396
2056
  mainEntryAsset: asset,
2057
+ bundleRoots: new Set([asset]),
1397
2058
  manualSharedBundle: opts.manualSharedBundle,
1398
2059
  needsStableName: Boolean(opts.needsStableName),
1399
2060
  size: asset.stats.size,