@atlaspack/bundler-default 2.14.5-canary.170 → 2.14.5-canary.171

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.
@@ -133,6 +133,26 @@ const CONFIG_SCHEMA = {
133
133
  additionalProperties: false
134
134
  }
135
135
  },
136
+ asyncBundleMerge: {
137
+ type: 'object',
138
+ properties: {
139
+ bundleSize: {
140
+ type: 'number',
141
+ required: true
142
+ },
143
+ maxOverfetchSize: {
144
+ type: 'number',
145
+ required: true
146
+ },
147
+ ignore: {
148
+ type: 'array',
149
+ items: {
150
+ type: 'string'
151
+ }
152
+ }
153
+ },
154
+ additionalProperties: false
155
+ },
136
156
  minBundles: {
137
157
  type: 'number'
138
158
  },
@@ -218,6 +238,7 @@ async function loadBundlerConfig(config, options, logger) {
218
238
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
219
239
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
220
240
  sharedBundleMerge: modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
241
+ asyncBundleMerge: modeConfig.asyncBundleMerge,
221
242
  maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
222
243
  projectRoot: options.projectRoot,
223
244
  disableSharedBundles: modeConfig.disableSharedBundles ?? defaults.disableSharedBundles,
package/lib/idealGraph.js CHANGED
@@ -12,6 +12,13 @@ function _path() {
12
12
  };
13
13
  return data;
14
14
  }
15
+ function _sortedArrayFunctions() {
16
+ const data = _interopRequireDefault(require("sorted-array-functions"));
17
+ _sortedArrayFunctions = function () {
18
+ return data;
19
+ };
20
+ return data;
21
+ }
15
22
  function _featureFlags() {
16
23
  const data = require("@atlaspack/feature-flags");
17
24
  _featureFlags = function () {
@@ -48,6 +55,7 @@ function _nullthrows() {
48
55
  return data;
49
56
  }
50
57
  var _bundleMerge = require("./bundleMerge");
58
+ var _stats = require("./stats");
51
59
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
52
60
  /* BundleRoot - An asset that is the main entry of a Bundle. */
53
61
 
@@ -78,6 +86,7 @@ function createIdealGraph(assetGraph, config, entries, logger) {
78
86
  let bundles = new Map();
79
87
  let dependencyBundleGraph = new (_graph().ContentGraph)();
80
88
  let assetReference = new (_utils().DefaultMap)(() => []);
89
+ let stats = new _stats.Stats(config.projectRoot);
81
90
 
82
91
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
83
92
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -896,7 +905,7 @@ function createIdealGraph(assetGraph, config, entries, logger) {
896
905
  var _bundleNode$value$mai;
897
906
  // meta.chunkName is set by the Rust transformer, so we just need to find
898
907
  // bundles that have a chunkName set.
899
- if (!node || node.type !== 'dependency' || node.value.meta.chunkName == null) {
908
+ if (!node || node.type !== 'dependency' || typeof node.value.meta.chunkName !== 'string') {
900
909
  continue;
901
910
  }
902
911
  let connectedBundles = dependencyBundleGraph.getNodeIdsConnectedFrom(nodeId, dependencyPriorityEdges[node.value.priority]);
@@ -935,16 +944,19 @@ function createIdealGraph(assetGraph, config, entries, logger) {
935
944
  // Merge all bundles with the same chunk name into the first one.
936
945
  let [firstBundleId, ...rest] = Array.from(bundleIds);
937
946
  for (let bundleId of rest) {
938
- // @ts-expect-error TS2345
939
- mergeBundles(firstBundleId, bundleId);
947
+ mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
940
948
  }
941
949
  }
942
950
  }
943
951
 
944
- // Step merge shared bundles that meet the overlap threshold
945
- // This step is skipped by default as the threshold defaults to 1
952
+ // Step merge async bundles that meet the configured params
953
+ if (config.asyncBundleMerge) {
954
+ mergeAsyncBundles(config.asyncBundleMerge);
955
+ }
956
+
957
+ // Step merge shared bundles that meet the configured params
946
958
  if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
947
- mergeOverlapBundles(config.sharedBundleMerge);
959
+ mergeSharedBundles(config.sharedBundleMerge);
948
960
  }
949
961
 
950
962
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -1045,7 +1057,8 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1045
1057
  }
1046
1058
  }
1047
1059
  }
1048
- function mergeBundles(bundleToKeepId, bundleToRemoveId) {
1060
+ function mergeBundles(bundleToKeepId, bundleToRemoveId, reason) {
1061
+ stats.trackMerge(bundleToKeepId, bundleToRemoveId, reason);
1049
1062
  let bundleToKeep = isNonRootBundle(bundleGraph.getNode(bundleToKeepId), `Bundle ${bundleToKeepId} not found`);
1050
1063
  let bundleToRemove = isNonRootBundle(bundleGraph.getNode(bundleToRemoveId), `Bundle ${bundleToRemoveId} not found`);
1051
1064
  modifiedSourceBundles.add(bundleToKeep);
@@ -1077,8 +1090,10 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1077
1090
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1078
1091
  continue;
1079
1092
  }
1080
- bundleToKeep.sourceBundles.add(sourceBundleId);
1081
- bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1093
+ if (sourceBundleId !== bundleToKeepId) {
1094
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1095
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1096
+ }
1082
1097
  }
1083
1098
  if ((0, _featureFlags().getFeatureFlag)('supportWebpackChunkName')) {
1084
1099
  bundleToKeep.sourceBundles.delete(bundleToRemoveId);
@@ -1090,9 +1105,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1090
1105
 
1091
1106
  // If the bundle is a source bundle, add it to the bundle to keep
1092
1107
  if (bundleNode.sourceBundles.has(bundleToRemoveId)) {
1093
- bundleNode.sourceBundles.add(bundleToKeepId);
1094
1108
  bundleNode.sourceBundles.delete(bundleToRemoveId);
1095
- bundleGraph.addEdge(bundleToKeepId, bundle);
1109
+ if (bundle !== bundleToKeepId) {
1110
+ bundleNode.sourceBundles.add(bundleToKeepId);
1111
+ bundleGraph.addEdge(bundleToKeepId, bundle);
1112
+ }
1096
1113
  }
1097
1114
  }
1098
1115
 
@@ -1105,11 +1122,35 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1105
1122
 
1106
1123
  // Merge the bundles in bundle group
1107
1124
  let bundlesInRemoveBundleGroup = getBundlesForBundleGroup(bundleToRemoveId);
1125
+ let removedBundleSharedBundles = new Set();
1108
1126
  for (let bundleIdInGroup of bundlesInRemoveBundleGroup) {
1109
1127
  if (bundleIdInGroup === bundleToRemoveId) {
1110
1128
  continue;
1111
1129
  }
1112
1130
  bundleGraph.addEdge(bundleToKeepId, bundleIdInGroup);
1131
+ removedBundleSharedBundles.add(bundleIdInGroup);
1132
+ }
1133
+ if ((0, _featureFlags().getFeatureFlag)('removeRedundantSharedBundles')) {
1134
+ // Merge any shared bundles that now have the same source bundles due to
1135
+ // the current bundle merge
1136
+ let sharedBundles = new (_utils().DefaultMap)(() => []);
1137
+ for (let bundleId of removedBundleSharedBundles) {
1138
+ let bundleNode = (0, _nullthrows().default)(bundleGraph.getNode(bundleId));
1139
+ (0, _assert().default)(bundleNode !== 'root');
1140
+ if (bundleNode.mainEntryAsset != null || bundleNode.manualSharedBundle != null) {
1141
+ continue;
1142
+ }
1143
+ let key = Array.from(bundleNode.sourceBundles).filter(sourceBundle => sourceBundle !== bundleToRemoveId).sort().join(',') + '.' + bundleNode.type;
1144
+ sharedBundles.get(key).push(bundleId);
1145
+ }
1146
+ for (let sharedBundlesToMerge of sharedBundles.values()) {
1147
+ if (sharedBundlesToMerge.length > 1) {
1148
+ let [firstBundleId, ...rest] = sharedBundlesToMerge;
1149
+ for (let bundleId of rest) {
1150
+ mergeBundles(firstBundleId, bundleId, 'redundant-shared');
1151
+ }
1152
+ }
1153
+ }
1113
1154
  }
1114
1155
 
1115
1156
  // Remove old bundle group
@@ -1142,7 +1183,7 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1142
1183
  }
1143
1184
  bundleGraph.removeNode(bundleToRemoveId);
1144
1185
  }
1145
- function mergeOverlapBundles(mergeConfig) {
1186
+ function mergeSharedBundles(mergeConfig) {
1146
1187
  // Find all shared bundles
1147
1188
  let sharedBundles = new Set();
1148
1189
  bundleGraph.traverse(nodeId => {
@@ -1178,7 +1219,7 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1178
1219
  for (let cluster of clusters) {
1179
1220
  let [mergeTarget, ...rest] = cluster;
1180
1221
  for (let bundleIdToMerge of rest) {
1181
- mergeBundles(mergeTarget, bundleIdToMerge);
1222
+ mergeBundles(mergeTarget, bundleIdToMerge, 'shared-merge');
1182
1223
  }
1183
1224
  mergedBundles.add(mergeTarget);
1184
1225
  }
@@ -1186,6 +1227,108 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1186
1227
  return mergedBundles;
1187
1228
  }
1188
1229
  }
1230
+ function mergeAsyncBundles({
1231
+ bundleSize,
1232
+ maxOverfetchSize,
1233
+ ignore
1234
+ }) {
1235
+ let mergeCandidates = [];
1236
+ let ignoreRegexes = (ignore === null || ignore === void 0 ? void 0 : ignore.map(glob => (0, _utils().globToRegex)(glob))) ?? [];
1237
+ let isIgnored = bundle => {
1238
+ if (!bundle.mainEntryAsset) {
1239
+ return false;
1240
+ }
1241
+ let mainEntryFilePath = _path().default.relative(config.projectRoot, (0, _nullthrows().default)(bundle.mainEntryAsset).filePath);
1242
+ return ignoreRegexes.some(regex => regex.test(mainEntryFilePath));
1243
+ };
1244
+ for (let [_bundleRootAsset, [bundleRootBundleId]] of bundleRoots) {
1245
+ let bundleRootBundle = (0, _nullthrows().default)(bundleGraph.getNode(bundleRootBundleId));
1246
+ (0, _assert().default)(bundleRootBundle !== 'root');
1247
+ if (bundleRootBundle.type === 'js' && bundleRootBundle.bundleBehavior !== 'inline' && bundleRootBundle.bundleBehavior !== 'inlineIsolated' && bundleRootBundle.size <= bundleSize && !isIgnored(bundleRootBundle)) {
1248
+ mergeCandidates.push(bundleRootBundleId);
1249
+ }
1250
+ }
1251
+ let candidates = [];
1252
+ for (let i = 0; i < mergeCandidates.length; i++) {
1253
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
1254
+ let a = mergeCandidates[i];
1255
+ let b = mergeCandidates[j];
1256
+ if (a === b) continue; // Skip self-comparison
1257
+
1258
+ candidates.push(scoreAsyncMerge(a, b, maxOverfetchSize));
1259
+ }
1260
+ }
1261
+ let sortByScore = (a, b) => {
1262
+ let diff = a.score - b.score;
1263
+ if (diff > 0) {
1264
+ return 1;
1265
+ } else if (diff < 0) {
1266
+ return -1;
1267
+ }
1268
+ return 0;
1269
+ };
1270
+ candidates = candidates.filter(({
1271
+ overfetchSize,
1272
+ score
1273
+ }) => overfetchSize <= maxOverfetchSize && score > 0).sort(sortByScore);
1274
+
1275
+ // Tracks the bundles that have been merged
1276
+ let merged = new Set();
1277
+ // Tracks the deleted bundles to the bundle they were merged into.
1278
+ let mergeRemap = new Map();
1279
+ // Tracks the bundles that have been rescored and added back into the
1280
+ // candidates.
1281
+ let rescored = new (_utils().DefaultMap)(() => new Set());
1282
+ do {
1283
+ let [a, b] = (0, _nullthrows().default)(candidates.pop()).bundleIds;
1284
+ if (bundleGraph.hasNode(a) && bundleGraph.hasNode(b) && (!merged.has(a) && !merged.has(b) || rescored.get(a).has(b))) {
1285
+ mergeRemap.set(b, a);
1286
+ merged.add(a);
1287
+ rescored.get(a).clear();
1288
+ mergeBundles(a, b, 'async-merge');
1289
+ continue;
1290
+ }
1291
+
1292
+ // One or both of the bundles have been previously merged, so we'll
1293
+ // rescore and add the result back into the list of candidates.
1294
+ let getMergedBundleId = bundleId => {
1295
+ let seen = new Set();
1296
+ while (!bundleGraph.hasNode(bundleId) && !seen.has(bundleId)) {
1297
+ seen.add(bundleId);
1298
+ bundleId = (0, _nullthrows().default)(mergeRemap.get(bundleId));
1299
+ }
1300
+ if (!bundleGraph.hasNode(bundleId)) {
1301
+ return;
1302
+ }
1303
+ return bundleId;
1304
+ };
1305
+
1306
+ // Map a and b to their merged bundle ids if they've already been merged
1307
+ let currentA = getMergedBundleId(a);
1308
+ let currentB = getMergedBundleId(b);
1309
+ if (!currentA || !currentB ||
1310
+ // Bundles are already merged
1311
+ currentA === currentB) {
1312
+ // This combiniation is not valid, so we skip it.
1313
+ continue;
1314
+ }
1315
+ let candidate = scoreAsyncMerge(currentA, currentB, maxOverfetchSize);
1316
+ if (candidate.overfetchSize <= maxOverfetchSize && candidate.score > 0) {
1317
+ _sortedArrayFunctions().default.add(candidates, candidate, sortByScore);
1318
+ rescored.get(currentA).add(currentB);
1319
+ }
1320
+ } while (candidates.length > 0);
1321
+ }
1322
+ function getBundle(bundleId) {
1323
+ let bundle = bundleGraph.getNode(bundleId);
1324
+ if (bundle === 'root') {
1325
+ throw new Error(`Cannot access root bundle`);
1326
+ }
1327
+ if (bundle == null) {
1328
+ throw new Error(`Bundle ${bundleId} not found in bundle graph`);
1329
+ }
1330
+ return bundle;
1331
+ }
1189
1332
  function getBigIntFromContentKey(contentKey) {
1190
1333
  let b = Buffer.alloc(64);
1191
1334
  b.write(contentKey);
@@ -1218,6 +1361,53 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1218
1361
  }, bundleGroupId);
1219
1362
  return bundlesInABundleGroup;
1220
1363
  }
1364
+ function scoreAsyncMerge(bundleAId, bundleBId, maxOverfetchSize) {
1365
+ let bundleGroupA = new Set(getBundlesForBundleGroup(bundleAId));
1366
+ let bundleGroupB = new Set(getBundlesForBundleGroup(bundleBId));
1367
+ let overlapSize = 0;
1368
+ let overfetchSize = 0;
1369
+ for (let bundleId of new Set([...bundleGroupA, ...bundleGroupB])) {
1370
+ let bundle = getBundle(bundleId);
1371
+ if (bundleGroupA.has(bundleId) && bundleGroupB.has(bundleId)) {
1372
+ overlapSize += bundle.size;
1373
+ } else {
1374
+ overfetchSize += bundle.size;
1375
+ }
1376
+ }
1377
+ let overlapPercent = overlapSize / (overfetchSize + overlapSize);
1378
+ let bundleAParents = getBundleParents(bundleAId);
1379
+ let bundleBParents = getBundleParents(bundleBId);
1380
+ let sharedParentOverlap = 0;
1381
+ let sharedParentMismatch = 0;
1382
+ for (let bundleId of new Set([...bundleAParents, ...bundleBParents])) {
1383
+ if (bundleAParents.has(bundleId) && bundleBParents.has(bundleId)) {
1384
+ sharedParentOverlap++;
1385
+ } else {
1386
+ sharedParentMismatch++;
1387
+ }
1388
+ }
1389
+ let overfetchScore = overfetchSize / maxOverfetchSize;
1390
+ let sharedParentPercent = sharedParentOverlap / (sharedParentOverlap + sharedParentMismatch);
1391
+ let score = sharedParentPercent + overlapPercent + overfetchScore;
1392
+ return {
1393
+ overfetchSize,
1394
+ score,
1395
+ bundleIds: [bundleAId, bundleBId]
1396
+ };
1397
+ }
1398
+ function getBundleParents(bundleId) {
1399
+ let parents = new Set();
1400
+ let {
1401
+ bundleRoots
1402
+ } = getBundle(bundleId);
1403
+ for (let bundleRoot of bundleRoots) {
1404
+ let bundleRootNodeId = (0, _nullthrows().default)(assetToBundleRootNodeId.get(bundleRoot));
1405
+ for (let parentId of bundleRootGraph.getNodeIdsConnectedTo(bundleRootNodeId, _graph().ALL_EDGE_TYPES)) {
1406
+ parents.add(parentId);
1407
+ }
1408
+ }
1409
+ return parents;
1410
+ }
1221
1411
  function getBundleFromBundleRoot(bundleRoot) {
1222
1412
  let bundle = bundleGraph.getNode((0, _nullthrows().default)(bundleRoots.get(bundleRoot))[0]);
1223
1413
  (0, _assert().default)(bundle !== 'root' && bundle != null);
@@ -1267,6 +1457,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1267
1457
  }
1268
1458
  bundleGraph.removeNode(bundleId);
1269
1459
  }
1460
+ stats.report(bundleId => {
1461
+ let bundle = bundleGraph.getNode(bundleId);
1462
+ (0, _assert().default)(bundle !== 'root');
1463
+ return bundle;
1464
+ });
1270
1465
  return {
1271
1466
  assets,
1272
1467
  bundleGraph,
package/lib/stats.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.Stats = void 0;
7
+ function _path() {
8
+ const data = require("path");
9
+ _path = function () {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function _utils() {
15
+ const data = require("@atlaspack/utils");
16
+ _utils = function () {
17
+ return data;
18
+ };
19
+ return data;
20
+ }
21
+ class Stats {
22
+ merges = new (_utils().DefaultMap)(() => []);
23
+ constructor(projectRoot) {
24
+ this.projectRoot = projectRoot;
25
+ }
26
+ trackMerge(bundleToKeep, bundleToRemove, reason) {
27
+ if (!_utils().debugTools['bundle-stats']) {
28
+ return;
29
+ }
30
+ this.merges.get(bundleToKeep).push(...this.merges.get(bundleToRemove), {
31
+ id: bundleToRemove,
32
+ reason
33
+ });
34
+ this.merges.delete(bundleToRemove);
35
+ }
36
+ getBundleLabel(bundle) {
37
+ if (bundle.manualSharedBundle) {
38
+ return bundle.manualSharedBundle;
39
+ }
40
+ if (bundle.mainEntryAsset) {
41
+ let relativePath = (0, _path().relative)(this.projectRoot, bundle.mainEntryAsset.filePath);
42
+ if (relativePath.length > 100) {
43
+ relativePath = relativePath.slice(0, 50) + '...' + relativePath.slice(-50);
44
+ }
45
+ return relativePath;
46
+ }
47
+ return `shared`;
48
+ }
49
+ report(getBundle) {
50
+ if (!_utils().debugTools['bundle-stats']) {
51
+ return;
52
+ }
53
+ let mergeResults = [];
54
+ let totals = {
55
+ label: 'Totals',
56
+ merges: 0
57
+ };
58
+ for (let [bundleId, mergedBundles] of this.merges) {
59
+ let bundle = getBundle(bundleId);
60
+ if (!bundle) {
61
+ continue;
62
+ }
63
+ let result = {
64
+ label: this.getBundleLabel(bundle),
65
+ size: bundle.size,
66
+ merges: mergedBundles.length
67
+ };
68
+ for (let merged of mergedBundles) {
69
+ result[merged.reason] = (result[merged.reason] || 0) + 1;
70
+ totals[merged.reason] = (totals[merged.reason] || 0) + 1;
71
+ }
72
+ totals.merges += mergedBundles.length;
73
+ mergeResults.push(result);
74
+ }
75
+ mergeResults.sort((a, b) => {
76
+ // Sort by bundle size descending
77
+ return b.size - a.size;
78
+ });
79
+ mergeResults.push(totals);
80
+
81
+ // eslint-disable-next-line no-console
82
+ console.table(mergeResults);
83
+ }
84
+ }
85
+ exports.Stats = Stats;
@@ -7,12 +7,20 @@ type ManualSharedBundles = Array<{
7
7
  root?: string;
8
8
  split?: number;
9
9
  }>;
10
- export type MergeCandidates = Array<{
10
+ export type SharedBundleMergeCandidates = Array<{
11
11
  overlapThreshold?: number;
12
12
  maxBundleSize?: number;
13
13
  sourceBundles?: Array<string>;
14
14
  minBundlesInGroup?: number;
15
15
  }>;
16
+ export interface AsyncBundleMerge {
17
+ /** Consider all async bundles smaller than this for merging */
18
+ bundleSize: number;
19
+ /** The max bytes allowed to be potentially overfetched due to a merge */
20
+ maxOverfetchSize: number;
21
+ /** Bundles to ignore from merging */
22
+ ignore?: Array<Glob>;
23
+ }
16
24
  export type ResolvedBundlerConfig = {
17
25
  minBundles: number;
18
26
  minBundleSize: number;
@@ -21,7 +29,8 @@ export type ResolvedBundlerConfig = {
21
29
  disableSharedBundles: boolean;
22
30
  manualSharedBundles: ManualSharedBundles;
23
31
  loadConditionalBundlesInParallel?: boolean;
24
- sharedBundleMerge?: MergeCandidates;
32
+ sharedBundleMerge?: SharedBundleMergeCandidates;
33
+ asyncBundleMerge?: AsyncBundleMerge;
25
34
  };
26
35
  export declare function loadBundlerConfig(config: Config, options: PluginOptions, logger: PluginLogger): Promise<ResolvedBundlerConfig>;
27
36
  export {};
@@ -0,0 +1,16 @@
1
+ import { NodeId } from '@atlaspack/graph';
2
+ import { DefaultMap } from '@atlaspack/utils';
3
+ import { Bundle } from './idealGraph';
4
+ interface MergedBundle {
5
+ id: NodeId;
6
+ reason: string;
7
+ }
8
+ export declare class Stats {
9
+ projectRoot: string;
10
+ merges: DefaultMap<NodeId, MergedBundle[]>;
11
+ constructor(projectRoot: string);
12
+ trackMerge(bundleToKeep: NodeId, bundleToRemove: NodeId, reason: string): void;
13
+ getBundleLabel(bundle: Bundle): string;
14
+ report(getBundle: (bundleId: NodeId) => Bundle | null | undefined): void;
15
+ }
16
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/bundler-default",
3
- "version": "2.14.5-canary.170+c940954d2",
3
+ "version": "2.14.5-canary.171+f0349a6b9",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -17,19 +17,21 @@
17
17
  "node": ">= 16.0.0"
18
18
  },
19
19
  "dependencies": {
20
- "@atlaspack/diagnostic": "2.14.1-canary.238+c940954d2",
21
- "@atlaspack/feature-flags": "2.14.1-canary.238+c940954d2",
22
- "@atlaspack/graph": "3.4.1-canary.238+c940954d2",
23
- "@atlaspack/plugin": "2.14.5-canary.170+c940954d2",
24
- "@atlaspack/rust": "3.2.1-canary.170+c940954d2",
25
- "@atlaspack/types-internal": "2.14.1-canary.238+c940954d2",
26
- "@atlaspack/utils": "2.14.5-canary.170+c940954d2",
20
+ "@atlaspack/diagnostic": "2.14.1-canary.239+f0349a6b9",
21
+ "@atlaspack/feature-flags": "2.14.1-canary.239+f0349a6b9",
22
+ "@atlaspack/graph": "3.4.1-canary.239+f0349a6b9",
23
+ "@atlaspack/plugin": "2.14.5-canary.171+f0349a6b9",
24
+ "@atlaspack/rust": "3.2.1-canary.171+f0349a6b9",
25
+ "@atlaspack/types-internal": "2.14.1-canary.239+f0349a6b9",
26
+ "@atlaspack/utils": "2.14.5-canary.171+f0349a6b9",
27
+ "@types/sorted-array-functions": "^1.0.0",
27
28
  "many-keys-map": "^1.0.3",
28
- "nullthrows": "^1.1.1"
29
+ "nullthrows": "^1.1.1",
30
+ "sorted-array-functions": "^1.0.0"
29
31
  },
30
32
  "scripts": {
31
33
  "check-ts": "tsc --emitDeclarationOnly --rootDir src",
32
34
  "build:lib": "gulp build --gulpfile ../../../gulpfile.js --cwd ."
33
35
  },
34
- "gitHead": "c940954d27d657c3c93cd328dfb394778da46eab"
36
+ "gitHead": "f0349a6b9b04755088f121095ca6301a2ada3767"
35
37
  }
@@ -19,13 +19,22 @@ type ManualSharedBundles = Array<{
19
19
  split?: number;
20
20
  }>;
21
21
 
22
- export type MergeCandidates = Array<{
22
+ export type SharedBundleMergeCandidates = Array<{
23
23
  overlapThreshold?: number;
24
24
  maxBundleSize?: number;
25
25
  sourceBundles?: Array<string>;
26
26
  minBundlesInGroup?: number;
27
27
  }>;
28
28
 
29
+ export interface AsyncBundleMerge {
30
+ /** Consider all async bundles smaller than this for merging */
31
+ bundleSize: number;
32
+ /** The max bytes allowed to be potentially overfetched due to a merge */
33
+ maxOverfetchSize: number;
34
+ /** Bundles to ignore from merging */
35
+ ignore?: Array<Glob>;
36
+ }
37
+
29
38
  type BaseBundlerConfig = {
30
39
  http?: number;
31
40
  minBundles?: number;
@@ -34,7 +43,8 @@ type BaseBundlerConfig = {
34
43
  disableSharedBundles?: boolean;
35
44
  manualSharedBundles?: ManualSharedBundles;
36
45
  loadConditionalBundlesInParallel?: boolean;
37
- sharedBundleMerge?: MergeCandidates;
46
+ sharedBundleMerge?: SharedBundleMergeCandidates;
47
+ asyncBundleMerge?: AsyncBundleMerge;
38
48
  };
39
49
 
40
50
  type BundlerConfig = Partial<Record<BuildMode, BaseBundlerConfig>> &
@@ -48,7 +58,8 @@ export type ResolvedBundlerConfig = {
48
58
  disableSharedBundles: boolean;
49
59
  manualSharedBundles: ManualSharedBundles;
50
60
  loadConditionalBundlesInParallel?: boolean;
51
- sharedBundleMerge?: MergeCandidates;
61
+ sharedBundleMerge?: SharedBundleMergeCandidates;
62
+ asyncBundleMerge?: AsyncBundleMerge;
52
63
  };
53
64
 
54
65
  function resolveModeConfig(
@@ -157,6 +168,26 @@ const CONFIG_SCHEMA: SchemaEntity = {
157
168
  additionalProperties: false,
158
169
  },
159
170
  },
171
+ asyncBundleMerge: {
172
+ type: 'object',
173
+ properties: {
174
+ bundleSize: {
175
+ type: 'number',
176
+ required: true,
177
+ },
178
+ maxOverfetchSize: {
179
+ type: 'number',
180
+ required: true,
181
+ },
182
+ ignore: {
183
+ type: 'array',
184
+ items: {
185
+ type: 'string',
186
+ },
187
+ },
188
+ },
189
+ additionalProperties: false,
190
+ },
160
191
  minBundles: {
161
192
  type: 'number',
162
193
  },
@@ -272,6 +303,7 @@ export async function loadBundlerConfig(
272
303
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
273
304
  sharedBundleMerge:
274
305
  modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
306
+ asyncBundleMerge: modeConfig.asyncBundleMerge,
275
307
  maxParallelRequests:
276
308
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
277
309
  projectRoot: options.projectRoot,
package/src/idealGraph.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import sortedArray from 'sorted-array-functions';
2
3
 
3
4
  import {getFeatureFlag} from '@atlaspack/feature-flags';
4
5
  import {
@@ -22,7 +23,12 @@ import invariant from 'assert';
22
23
  import nullthrows from 'nullthrows';
23
24
 
24
25
  import {findMergeCandidates, MergeGroup} from './bundleMerge';
25
- import type {ResolvedBundlerConfig, MergeCandidates} from './bundlerConfig';
26
+ import type {
27
+ ResolvedBundlerConfig,
28
+ SharedBundleMergeCandidates,
29
+ AsyncBundleMerge,
30
+ } from './bundlerConfig';
31
+ import {Stats} from './stats';
26
32
 
27
33
  /* BundleRoot - An asset that is the main entry of a Bundle. */
28
34
  type BundleRoot = Asset;
@@ -108,6 +114,7 @@ export function createIdealGraph(
108
114
  Asset,
109
115
  Array<[Dependency, Bundle]>
110
116
  > = new DefaultMap(() => []);
117
+ let stats = new Stats(config.projectRoot);
111
118
 
112
119
  // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their
113
120
  // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role.
@@ -1206,14 +1213,14 @@ export function createIdealGraph(
1206
1213
 
1207
1214
  if (getFeatureFlag('supportWebpackChunkName')) {
1208
1215
  // Merge webpack chunk name bundles
1209
- let chunkNameBundles = new DefaultMap(() => new Set());
1216
+ let chunkNameBundles = new DefaultMap<string, Set<NodeId>>(() => new Set());
1210
1217
  for (let [nodeId, node] of dependencyBundleGraph.nodes.entries()) {
1211
1218
  // meta.chunkName is set by the Rust transformer, so we just need to find
1212
1219
  // bundles that have a chunkName set.
1213
1220
  if (
1214
1221
  !node ||
1215
1222
  node.type !== 'dependency' ||
1216
- node.value.meta.chunkName == null
1223
+ typeof node.value.meta.chunkName !== 'string'
1217
1224
  ) {
1218
1225
  continue;
1219
1226
  }
@@ -1272,16 +1279,19 @@ export function createIdealGraph(
1272
1279
  // Merge all bundles with the same chunk name into the first one.
1273
1280
  let [firstBundleId, ...rest] = Array.from(bundleIds);
1274
1281
  for (let bundleId of rest) {
1275
- // @ts-expect-error TS2345
1276
- mergeBundles(firstBundleId, bundleId);
1282
+ mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
1277
1283
  }
1278
1284
  }
1279
1285
  }
1280
1286
 
1281
- // Step merge shared bundles that meet the overlap threshold
1282
- // This step is skipped by default as the threshold defaults to 1
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
1283
1293
  if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
1284
- mergeOverlapBundles(config.sharedBundleMerge);
1294
+ mergeSharedBundles(config.sharedBundleMerge);
1285
1295
  }
1286
1296
 
1287
1297
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -1412,7 +1422,12 @@ export function createIdealGraph(
1412
1422
  }
1413
1423
  }
1414
1424
 
1415
- function mergeBundles(bundleToKeepId: NodeId, bundleToRemoveId: NodeId) {
1425
+ function mergeBundles(
1426
+ bundleToKeepId: NodeId,
1427
+ bundleToRemoveId: NodeId,
1428
+ reason: string,
1429
+ ) {
1430
+ stats.trackMerge(bundleToKeepId, bundleToRemoveId, reason);
1416
1431
  let bundleToKeep = isNonRootBundle(
1417
1432
  bundleGraph.getNode(bundleToKeepId),
1418
1433
  `Bundle ${bundleToKeepId} not found`,
@@ -1461,8 +1476,10 @@ export function createIdealGraph(
1461
1476
  continue;
1462
1477
  }
1463
1478
 
1464
- bundleToKeep.sourceBundles.add(sourceBundleId);
1465
- bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1479
+ if (sourceBundleId !== bundleToKeepId) {
1480
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1481
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1482
+ }
1466
1483
  }
1467
1484
 
1468
1485
  if (getFeatureFlag('supportWebpackChunkName')) {
@@ -1478,9 +1495,11 @@ export function createIdealGraph(
1478
1495
 
1479
1496
  // If the bundle is a source bundle, add it to the bundle to keep
1480
1497
  if (bundleNode.sourceBundles.has(bundleToRemoveId)) {
1481
- bundleNode.sourceBundles.add(bundleToKeepId);
1482
1498
  bundleNode.sourceBundles.delete(bundleToRemoveId);
1483
- bundleGraph.addEdge(bundleToKeepId, bundle);
1499
+ if (bundle !== bundleToKeepId) {
1500
+ bundleNode.sourceBundles.add(bundleToKeepId);
1501
+ bundleGraph.addEdge(bundleToKeepId, bundle);
1502
+ }
1484
1503
  }
1485
1504
  }
1486
1505
 
@@ -1496,11 +1515,48 @@ export function createIdealGraph(
1496
1515
  let bundlesInRemoveBundleGroup =
1497
1516
  getBundlesForBundleGroup(bundleToRemoveId);
1498
1517
 
1518
+ let removedBundleSharedBundles = new Set<NodeId>();
1499
1519
  for (let bundleIdInGroup of bundlesInRemoveBundleGroup) {
1500
1520
  if (bundleIdInGroup === bundleToRemoveId) {
1501
1521
  continue;
1502
1522
  }
1503
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
+ }
1504
1560
  }
1505
1561
 
1506
1562
  // Remove old bundle group
@@ -1566,7 +1622,7 @@ export function createIdealGraph(
1566
1622
  bundleGraph.removeNode(bundleToRemoveId);
1567
1623
  }
1568
1624
 
1569
- function mergeOverlapBundles(mergeConfig: MergeCandidates) {
1625
+ function mergeSharedBundles(mergeConfig: SharedBundleMergeCandidates) {
1570
1626
  // Find all shared bundles
1571
1627
  let sharedBundles = new Set<NodeId>();
1572
1628
  bundleGraph.traverse((nodeId) => {
@@ -1620,7 +1676,7 @@ export function createIdealGraph(
1620
1676
  let [mergeTarget, ...rest] = cluster;
1621
1677
 
1622
1678
  for (let bundleIdToMerge of rest) {
1623
- mergeBundles(mergeTarget, bundleIdToMerge);
1679
+ mergeBundles(mergeTarget, bundleIdToMerge, 'shared-merge');
1624
1680
  }
1625
1681
 
1626
1682
  mergedBundles.add(mergeTarget);
@@ -1631,6 +1687,150 @@ export function createIdealGraph(
1631
1687
  }
1632
1688
  }
1633
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
+
1634
1834
  function getBigIntFromContentKey(contentKey: string) {
1635
1835
  let b = Buffer.alloc(64);
1636
1836
  b.write(contentKey);
@@ -1668,6 +1868,78 @@ export function createIdealGraph(
1668
1868
  return bundlesInABundleGroup;
1669
1869
  }
1670
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
+
1671
1943
  function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle {
1672
1944
  let bundle = bundleGraph.getNode(
1673
1945
  nullthrows(bundleRoots.get(bundleRoot))[0],
@@ -1732,6 +2004,12 @@ export function createIdealGraph(
1732
2004
  bundleGraph.removeNode(bundleId);
1733
2005
  }
1734
2006
 
2007
+ stats.report((bundleId) => {
2008
+ let bundle = bundleGraph.getNode(bundleId);
2009
+ invariant(bundle !== 'root');
2010
+ return bundle;
2011
+ });
2012
+
1735
2013
  return {
1736
2014
  assets,
1737
2015
  bundleGraph,
package/src/stats.ts ADDED
@@ -0,0 +1,97 @@
1
+ import {relative} from 'path';
2
+ import {NodeId} from '@atlaspack/graph';
3
+ import {DefaultMap, debugTools} from '@atlaspack/utils';
4
+
5
+ import {Bundle} from './idealGraph';
6
+
7
+ interface MergedBundle {
8
+ id: NodeId;
9
+ reason: string;
10
+ }
11
+
12
+ export class Stats {
13
+ projectRoot: string;
14
+ merges: DefaultMap<NodeId, MergedBundle[]> = new DefaultMap(() => []);
15
+
16
+ constructor(projectRoot: string) {
17
+ this.projectRoot = projectRoot;
18
+ }
19
+
20
+ trackMerge(bundleToKeep: NodeId, bundleToRemove: NodeId, reason: string) {
21
+ if (!debugTools['bundle-stats']) {
22
+ return;
23
+ }
24
+
25
+ this.merges
26
+ .get(bundleToKeep)
27
+ .push(...this.merges.get(bundleToRemove), {id: bundleToRemove, reason});
28
+ this.merges.delete(bundleToRemove);
29
+ }
30
+
31
+ getBundleLabel(bundle: Bundle): string {
32
+ if (bundle.manualSharedBundle) {
33
+ return bundle.manualSharedBundle;
34
+ }
35
+
36
+ if (bundle.mainEntryAsset) {
37
+ let relativePath = relative(
38
+ this.projectRoot,
39
+ bundle.mainEntryAsset.filePath,
40
+ );
41
+
42
+ if (relativePath.length > 100) {
43
+ relativePath =
44
+ relativePath.slice(0, 50) + '...' + relativePath.slice(-50);
45
+ }
46
+
47
+ return relativePath;
48
+ }
49
+
50
+ return `shared`;
51
+ }
52
+
53
+ report(getBundle: (bundleId: NodeId) => Bundle | null | undefined): void {
54
+ if (!debugTools['bundle-stats']) {
55
+ return;
56
+ }
57
+
58
+ type MergeResult = Record<string, string | number>;
59
+ let mergeResults: Array<MergeResult> = [];
60
+
61
+ let totals: Record<string, string | number> = {
62
+ label: 'Totals',
63
+ merges: 0,
64
+ };
65
+
66
+ for (let [bundleId, mergedBundles] of this.merges) {
67
+ let bundle = getBundle(bundleId);
68
+ if (!bundle) {
69
+ continue;
70
+ }
71
+
72
+ let result: MergeResult = {
73
+ label: this.getBundleLabel(bundle),
74
+ size: bundle.size,
75
+ merges: mergedBundles.length,
76
+ };
77
+
78
+ for (let merged of mergedBundles) {
79
+ result[merged.reason] = ((result[merged.reason] as number) || 0) + 1;
80
+ totals[merged.reason] = ((totals[merged.reason] as number) || 0) + 1;
81
+ }
82
+
83
+ (totals.merges as number) += mergedBundles.length;
84
+ mergeResults.push(result);
85
+ }
86
+
87
+ mergeResults.sort((a, b) => {
88
+ // Sort by bundle size descending
89
+ return (b.size as number) - (a.size as number);
90
+ });
91
+
92
+ mergeResults.push(totals);
93
+
94
+ // eslint-disable-next-line no-console
95
+ console.table(mergeResults);
96
+ }
97
+ }