@atlaspack/bundler-default 2.14.5-canary.170 → 2.14.5-canary.172
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.
- package/lib/MonolithicBundler.js +9 -1
- package/lib/bundlerConfig.js +21 -0
- package/lib/idealGraph.js +208 -13
- package/lib/stats.js +85 -0
- package/lib/types/bundlerConfig.d.ts +11 -2
- package/lib/types/stats.d.ts +16 -0
- package/package.json +12 -10
- package/src/MonolithicBundler.ts +7 -1
- package/src/bundlerConfig.ts +35 -3
- package/src/idealGraph.ts +293 -15
- package/src/stats.ts +97 -0
package/lib/MonolithicBundler.js
CHANGED
|
@@ -4,6 +4,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.addJSMonolithBundle = addJSMonolithBundle;
|
|
7
|
+
function _featureFlags() {
|
|
8
|
+
const data = require("@atlaspack/feature-flags");
|
|
9
|
+
_featureFlags = function () {
|
|
10
|
+
return data;
|
|
11
|
+
};
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
7
14
|
function _nullthrows() {
|
|
8
15
|
const data = _interopRequireDefault(require("nullthrows"));
|
|
9
16
|
_nullthrows = function () {
|
|
@@ -18,7 +25,8 @@ function addJSMonolithBundle(bundleGraph, entryAsset, entryDep) {
|
|
|
18
25
|
// Create a single bundle to hold all JS assets
|
|
19
26
|
let bundle = bundleGraph.createBundle({
|
|
20
27
|
entryAsset,
|
|
21
|
-
target
|
|
28
|
+
target,
|
|
29
|
+
needsStableName: (0, _featureFlags().getFeatureFlag)('singleFileOutputStableName')
|
|
22
30
|
});
|
|
23
31
|
bundleGraph.traverse((node, _, actions) => {
|
|
24
32
|
// JS assets can be added to the bundle, but the rest are ignored
|
package/lib/bundlerConfig.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
939
|
-
mergeBundles(firstBundleId, bundleId);
|
|
947
|
+
mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
|
|
940
948
|
}
|
|
941
949
|
}
|
|
942
950
|
}
|
|
943
951
|
|
|
944
|
-
// Step merge
|
|
945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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?:
|
|
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.
|
|
3
|
+
"version": "2.14.5-canary.172+88439807b",
|
|
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.
|
|
21
|
-
"@atlaspack/feature-flags": "2.14.1-canary.
|
|
22
|
-
"@atlaspack/graph": "3.4.1-canary.
|
|
23
|
-
"@atlaspack/plugin": "2.14.5-canary.
|
|
24
|
-
"@atlaspack/rust": "3.2.1-canary.
|
|
25
|
-
"@atlaspack/types-internal": "2.14.1-canary.
|
|
26
|
-
"@atlaspack/utils": "2.14.5-canary.
|
|
20
|
+
"@atlaspack/diagnostic": "2.14.1-canary.240+88439807b",
|
|
21
|
+
"@atlaspack/feature-flags": "2.14.1-canary.240+88439807b",
|
|
22
|
+
"@atlaspack/graph": "3.4.1-canary.240+88439807b",
|
|
23
|
+
"@atlaspack/plugin": "2.14.5-canary.172+88439807b",
|
|
24
|
+
"@atlaspack/rust": "3.2.1-canary.172+88439807b",
|
|
25
|
+
"@atlaspack/types-internal": "2.14.1-canary.240+88439807b",
|
|
26
|
+
"@atlaspack/utils": "2.14.5-canary.172+88439807b",
|
|
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": "
|
|
36
|
+
"gitHead": "88439807be20025fd3433380204ff1205079729e"
|
|
35
37
|
}
|
package/src/MonolithicBundler.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type {
|
|
|
3
3
|
Dependency,
|
|
4
4
|
MutableBundleGraph,
|
|
5
5
|
} from '@atlaspack/types-internal';
|
|
6
|
+
import {getFeatureFlag} from '@atlaspack/feature-flags';
|
|
7
|
+
|
|
6
8
|
import nullthrows from 'nullthrows';
|
|
7
9
|
|
|
8
10
|
export function addJSMonolithBundle(
|
|
@@ -16,7 +18,11 @@ export function addJSMonolithBundle(
|
|
|
16
18
|
);
|
|
17
19
|
|
|
18
20
|
// Create a single bundle to hold all JS assets
|
|
19
|
-
let bundle = bundleGraph.createBundle({
|
|
21
|
+
let bundle = bundleGraph.createBundle({
|
|
22
|
+
entryAsset,
|
|
23
|
+
target,
|
|
24
|
+
needsStableName: getFeatureFlag('singleFileOutputStableName'),
|
|
25
|
+
});
|
|
20
26
|
|
|
21
27
|
bundleGraph.traverse(
|
|
22
28
|
(node, _, actions) => {
|
package/src/bundlerConfig.ts
CHANGED
|
@@ -19,13 +19,22 @@ type ManualSharedBundles = Array<{
|
|
|
19
19
|
split?: number;
|
|
20
20
|
}>;
|
|
21
21
|
|
|
22
|
-
export type
|
|
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?:
|
|
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?:
|
|
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 {
|
|
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
|
|
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
|
-
|
|
1276
|
-
mergeBundles(firstBundleId, bundleId);
|
|
1282
|
+
mergeBundles(firstBundleId, bundleId, 'webpack-chunk-name');
|
|
1277
1283
|
}
|
|
1278
1284
|
}
|
|
1279
1285
|
}
|
|
1280
1286
|
|
|
1281
|
-
// Step merge
|
|
1282
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1465
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|