@atlaspack/bundler-default 2.14.5-canary.6 → 2.14.5-canary.61
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/CHANGELOG.md +88 -0
- package/lib/bundleMerge.js +158 -0
- package/lib/bundlerConfig.js +51 -6
- package/lib/idealGraph.js +81 -1
- package/lib/memoize.js +40 -0
- package/package.json +9 -8
- package/src/bundleMerge.js +254 -0
- package/src/bundlerConfig.js +52 -3
- package/src/idealGraph.js +124 -3
- package/src/memoize.js +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# @atlaspack/bundler-default
|
|
2
2
|
|
|
3
|
+
## 2.16.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`35fdd4b`](https://github.com/atlassian-labs/atlaspack/commit/35fdd4b52da0af20f74667f7b8adfb2f90279b7c), [`6dd4ccb`](https://github.com/atlassian-labs/atlaspack/commit/6dd4ccb753541de32322d881f973d571dd57e4ca)]:
|
|
8
|
+
- @atlaspack/rust@3.3.5
|
|
9
|
+
- @atlaspack/plugin@2.14.10
|
|
10
|
+
- @atlaspack/utils@2.14.10
|
|
11
|
+
|
|
12
|
+
## 2.16.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [[`124b7ff`](https://github.com/atlassian-labs/atlaspack/commit/124b7fff44f71aac9fbad289a9a9509b3dfc9aaa), [`e052521`](https://github.com/atlassian-labs/atlaspack/commit/e0525210850ed1606146eb86991049cf567c5dec), [`15c6d70`](https://github.com/atlassian-labs/atlaspack/commit/15c6d7000bd89da876bc590aa75b17a619a41896), [`e4d966c`](https://github.com/atlassian-labs/atlaspack/commit/e4d966c3c9c4292c5013372ae65b10d19d4bacc6), [`209692f`](https://github.com/atlassian-labs/atlaspack/commit/209692ffb11eae103a0d65c5e1118a5aa1625818), [`42a775d`](https://github.com/atlassian-labs/atlaspack/commit/42a775de8eec638ad188f3271964170d8c04d84b), [`29c2f10`](https://github.com/atlassian-labs/atlaspack/commit/29c2f106de9679adfb5afa04e1910471dc65a427), [`f4da1e1`](https://github.com/atlassian-labs/atlaspack/commit/f4da1e120e73eeb5e8b8927f05e88f04d6148c7b), [`1ef91fc`](https://github.com/atlassian-labs/atlaspack/commit/1ef91fcc863fdd2831511937083dbbc1263b3d9d)]:
|
|
17
|
+
- @atlaspack/rust@3.3.4
|
|
18
|
+
- @atlaspack/feature-flags@2.16.0
|
|
19
|
+
- @atlaspack/utils@2.14.9
|
|
20
|
+
- @atlaspack/graph@3.4.7
|
|
21
|
+
- @atlaspack/plugin@2.14.9
|
|
22
|
+
|
|
23
|
+
## 2.16.1
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- Updated dependencies [[`30f6017`](https://github.com/atlassian-labs/atlaspack/commit/30f60175ba4d272c5fc193973c63bc298584775b), [`1ab0a27`](https://github.com/atlassian-labs/atlaspack/commit/1ab0a275aeca40350415e2b03e7440d1dddc6228), [`b8a4ae8`](https://github.com/atlassian-labs/atlaspack/commit/b8a4ae8f83dc0a83d8b145c5f729936ce52080a3)]:
|
|
28
|
+
- @atlaspack/feature-flags@2.15.1
|
|
29
|
+
- @atlaspack/rust@3.3.3
|
|
30
|
+
- @atlaspack/graph@3.4.6
|
|
31
|
+
- @atlaspack/utils@2.14.8
|
|
32
|
+
- @atlaspack/plugin@2.14.8
|
|
33
|
+
|
|
34
|
+
## 2.16.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- [#547](https://github.com/atlassian-labs/atlaspack/pull/547) [`a1773d2`](https://github.com/atlassian-labs/atlaspack/commit/a1773d2a62d0ef7805ac7524621dcabcc1afe929) Thanks [@benjervis](https://github.com/benjervis)! - Add a feature flag for resolving the configuration for `@atlaspack/bundler-default` from CWD, rather than exclusively from the project root.
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- Updated dependencies [[`a1773d2`](https://github.com/atlassian-labs/atlaspack/commit/a1773d2a62d0ef7805ac7524621dcabcc1afe929), [`556d6ab`](https://github.com/atlassian-labs/atlaspack/commit/556d6ab8ede759fa7f37fcd3f4da336ef1c55e8f)]:
|
|
43
|
+
- @atlaspack/feature-flags@2.15.0
|
|
44
|
+
- @atlaspack/rust@3.3.2
|
|
45
|
+
- @atlaspack/graph@3.4.5
|
|
46
|
+
- @atlaspack/utils@2.14.7
|
|
47
|
+
- @atlaspack/plugin@2.14.7
|
|
48
|
+
|
|
49
|
+
## 2.15.1
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- Updated dependencies [[`e0f5337`](https://github.com/atlassian-labs/atlaspack/commit/e0f533757bd1019dbd108a04952c87da15286e09)]:
|
|
54
|
+
- @atlaspack/feature-flags@2.14.4
|
|
55
|
+
- @atlaspack/rust@3.3.1
|
|
56
|
+
- @atlaspack/graph@3.4.4
|
|
57
|
+
- @atlaspack/utils@2.14.6
|
|
58
|
+
- @atlaspack/plugin@2.14.6
|
|
59
|
+
|
|
60
|
+
## 2.15.0
|
|
61
|
+
|
|
62
|
+
### Minor Changes
|
|
63
|
+
|
|
64
|
+
- [#535](https://github.com/atlassian-labs/atlaspack/pull/535) [`a4bc259`](https://github.com/atlassian-labs/atlaspack/commit/a4bc2590196b6c1e743e4edcb0337e8c4c240ab4) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Add `sharedBundleMergeThreshold` config option
|
|
65
|
+
|
|
66
|
+
In apps with lots of dynamic imports, many shared bundles are often removed
|
|
67
|
+
from the output to prevent an overload in network requests according to the
|
|
68
|
+
`maxParallelRequests` config. In these cases, setting `sharedBundleMergeThreshold` can
|
|
69
|
+
merge shared bundles with a high overlap in their source bundles (bundles that share the bundle).
|
|
70
|
+
This config trades-off potential overfetching to reduce asset duplication.
|
|
71
|
+
|
|
72
|
+
The following config would merge shared bundles that have a 75% or higher overlap in source bundles.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"@atlaspack/bundler-default": {
|
|
77
|
+
"sharedBundleMergeThreshold": 0.75
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Patch Changes
|
|
83
|
+
|
|
84
|
+
- Updated dependencies [[`11d6f16`](https://github.com/atlassian-labs/atlaspack/commit/11d6f16b6397dee2f217167e5c98b39edb63f7a7), [`e2ba0f6`](https://github.com/atlassian-labs/atlaspack/commit/e2ba0f69702656f3d1ce95ab1454e35062b13b39), [`d2c50c2`](https://github.com/atlassian-labs/atlaspack/commit/d2c50c2c020888b33bb25b8690d9320c2b69e2a6), [`46a90dc`](https://github.com/atlassian-labs/atlaspack/commit/46a90dccd019a26b222c878a92d23acc75dc67c5)]:
|
|
85
|
+
- @atlaspack/feature-flags@2.14.3
|
|
86
|
+
- @atlaspack/rust@3.3.0
|
|
87
|
+
- @atlaspack/graph@3.4.3
|
|
88
|
+
- @atlaspack/utils@2.14.5
|
|
89
|
+
- @atlaspack/plugin@2.14.5
|
|
90
|
+
|
|
3
91
|
## 2.14.4
|
|
4
92
|
|
|
5
93
|
### Patch Changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.findMergeCandidates = findMergeCandidates;
|
|
7
|
+
function _assert() {
|
|
8
|
+
const data = _interopRequireDefault(require("assert"));
|
|
9
|
+
_assert = function () {
|
|
10
|
+
return data;
|
|
11
|
+
};
|
|
12
|
+
return data;
|
|
13
|
+
}
|
|
14
|
+
function _nullthrows() {
|
|
15
|
+
const data = _interopRequireDefault(require("nullthrows"));
|
|
16
|
+
_nullthrows = function () {
|
|
17
|
+
return data;
|
|
18
|
+
};
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
function _graph() {
|
|
22
|
+
const data = require("@atlaspack/graph");
|
|
23
|
+
_graph = function () {
|
|
24
|
+
return data;
|
|
25
|
+
};
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
var _memoize = require("./memoize");
|
|
29
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
30
|
+
function getBundlesForBundleGroup(bundleGraph, bundleGroupId) {
|
|
31
|
+
let count = 0;
|
|
32
|
+
bundleGraph.traverse(nodeId => {
|
|
33
|
+
var _bundleGraph$getNode;
|
|
34
|
+
if (((_bundleGraph$getNode = bundleGraph.getNode(nodeId)) === null || _bundleGraph$getNode === void 0 ? void 0 : _bundleGraph$getNode.bundleBehavior) !== 'inline') {
|
|
35
|
+
count++;
|
|
36
|
+
}
|
|
37
|
+
}, bundleGroupId);
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
let getBundleOverlapBitSet = (sourceBundlesA, sourceBundlesB) => {
|
|
41
|
+
let allSourceBundles = _graph().BitSet.union(sourceBundlesA, sourceBundlesB);
|
|
42
|
+
let sharedSourceBundles = _graph().BitSet.intersect(sourceBundlesA, sourceBundlesB);
|
|
43
|
+
return sharedSourceBundles.size() / allSourceBundles.size();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Returns a decimal showing the proportion source bundles are common to
|
|
47
|
+
// both bundles versus the total number of source bundles.
|
|
48
|
+
function checkBundleThreshold(bundleA, bundleB, threshold) {
|
|
49
|
+
return getBundleOverlapBitSet(bundleA.sourceBundleBitSet, bundleB.sourceBundleBitSet) >= threshold;
|
|
50
|
+
}
|
|
51
|
+
let checkSharedSourceBundles = (0, _memoize.memoize)((bundle, importantAncestorBundles) => {
|
|
52
|
+
return importantAncestorBundles.every(ancestorId => bundle.sourceBundles.has(ancestorId));
|
|
53
|
+
});
|
|
54
|
+
let hasSuitableBundleGroup = (0, _memoize.memoize)((bundleGraph, bundle, minBundlesInGroup) => {
|
|
55
|
+
for (let sourceBundle of bundle.sourceBundles) {
|
|
56
|
+
let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
|
|
57
|
+
if (bundlesInGroup >= minBundlesInGroup) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
function validMerge(bundleGraph, config, bundleA, bundleB) {
|
|
64
|
+
if (config.maxBundleSize != null) {
|
|
65
|
+
if (bundleA.bundle.size > config.maxBundleSize || bundleB.bundle.size > config.maxBundleSize) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (config.overlapThreshold != null) {
|
|
70
|
+
if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (config.sourceBundles != null) {
|
|
75
|
+
if (!checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) || !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (config.minBundlesInGroup != null) {
|
|
80
|
+
if (!hasSuitableBundleGroup(bundleGraph, bundleA.bundle, config.minBundlesInGroup) || !hasSuitableBundleGroup(bundleGraph, bundleB.bundle, config.minBundlesInGroup)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
function getMergeClusters(graph, candidates) {
|
|
87
|
+
let clusters = [];
|
|
88
|
+
for (let [candidate, edgeType] of candidates.entries()) {
|
|
89
|
+
let cluster = [];
|
|
90
|
+
graph.traverse(nodeId => {
|
|
91
|
+
cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
|
|
92
|
+
// Remove node from candidates as it has already been processed
|
|
93
|
+
candidates.delete(nodeId);
|
|
94
|
+
}, candidate, edgeType);
|
|
95
|
+
clusters.push(cluster);
|
|
96
|
+
}
|
|
97
|
+
return clusters;
|
|
98
|
+
}
|
|
99
|
+
function getPossibleMergeCandidates(bundleGraph, bundles) {
|
|
100
|
+
let mergeCandidates = bundles.map(bundleId => {
|
|
101
|
+
let bundle = bundleGraph.getNode(bundleId);
|
|
102
|
+
(0, _assert().default)(bundle && bundle !== 'root', 'Bundle should exist');
|
|
103
|
+
let sourceBundleBitSet = new (_graph().BitSet)(bundleGraph.nodes.length);
|
|
104
|
+
for (let sourceBundle of bundle.sourceBundles) {
|
|
105
|
+
sourceBundleBitSet.add(sourceBundle);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: bundleId,
|
|
109
|
+
bundle,
|
|
110
|
+
sourceBundleBitSet,
|
|
111
|
+
contentKey: bundleId.toString()
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
const uniquePairs = [];
|
|
115
|
+
for (let i = 0; i < mergeCandidates.length; i++) {
|
|
116
|
+
for (let j = i + 1; j < mergeCandidates.length; j++) {
|
|
117
|
+
let a = mergeCandidates[i];
|
|
118
|
+
let b = mergeCandidates[j];
|
|
119
|
+
if (
|
|
120
|
+
// $FlowFixMe both bundles will always have internalizedAssets
|
|
121
|
+
a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)) {
|
|
122
|
+
uniquePairs.push([a, b]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return uniquePairs;
|
|
127
|
+
}
|
|
128
|
+
function findMergeCandidates(bundleGraph, bundles, config) {
|
|
129
|
+
let graph = new (_graph().ContentGraph)();
|
|
130
|
+
let candidates = new Map();
|
|
131
|
+
let allPossibleMergeCandidates = getPossibleMergeCandidates(bundleGraph, bundles);
|
|
132
|
+
|
|
133
|
+
// Build graph of clustered merge candidates
|
|
134
|
+
for (let i = 0; i < config.length; i++) {
|
|
135
|
+
// Ensure edge type coresponds to config index
|
|
136
|
+
let edgeType = i + 1;
|
|
137
|
+
for (let group of allPossibleMergeCandidates) {
|
|
138
|
+
let candidateA = group[0];
|
|
139
|
+
let candidateB = group[1];
|
|
140
|
+
if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
let bundleNode = graph.addNodeByContentKeyIfNeeded(candidateA.contentKey, candidateA.id);
|
|
144
|
+
let otherBundleNode = graph.addNodeByContentKeyIfNeeded(candidateB.contentKey, candidateB.id);
|
|
145
|
+
|
|
146
|
+
// Add edge in both directions
|
|
147
|
+
graph.addEdge(bundleNode, otherBundleNode, edgeType);
|
|
148
|
+
graph.addEdge(otherBundleNode, bundleNode, edgeType);
|
|
149
|
+
candidates.set(bundleNode, edgeType);
|
|
150
|
+
candidates.set(otherBundleNode, edgeType);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Remove bundles that have been allocated to a higher priority merge
|
|
154
|
+
allPossibleMergeCandidates = allPossibleMergeCandidates.filter(group => !graph.hasContentKey(group[0].contentKey) && !graph.hasContentKey(group[1].contentKey));
|
|
155
|
+
}
|
|
156
|
+
(0, _memoize.clearCaches)();
|
|
157
|
+
return getMergeClusters(graph, candidates);
|
|
158
|
+
}
|
package/lib/bundlerConfig.js
CHANGED
|
@@ -11,6 +11,13 @@ function _diagnostic() {
|
|
|
11
11
|
};
|
|
12
12
|
return data;
|
|
13
13
|
}
|
|
14
|
+
function _featureFlags() {
|
|
15
|
+
const data = require("@atlaspack/feature-flags");
|
|
16
|
+
_featureFlags = function () {
|
|
17
|
+
return data;
|
|
18
|
+
};
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
14
21
|
function _utils() {
|
|
15
22
|
const data = require("@atlaspack/utils");
|
|
16
23
|
_utils = function () {
|
|
@@ -53,14 +60,16 @@ const HTTP_OPTIONS = {
|
|
|
53
60
|
manualSharedBundles: [],
|
|
54
61
|
minBundleSize: 30000,
|
|
55
62
|
maxParallelRequests: 6,
|
|
56
|
-
disableSharedBundles: false
|
|
63
|
+
disableSharedBundles: false,
|
|
64
|
+
sharedBundleMerge: []
|
|
57
65
|
},
|
|
58
66
|
'2': {
|
|
59
67
|
minBundles: 1,
|
|
60
68
|
manualSharedBundles: [],
|
|
61
69
|
minBundleSize: 20000,
|
|
62
70
|
maxParallelRequests: 25,
|
|
63
|
-
disableSharedBundles: false
|
|
71
|
+
disableSharedBundles: false,
|
|
72
|
+
sharedBundleMerge: []
|
|
64
73
|
}
|
|
65
74
|
};
|
|
66
75
|
const CONFIG_SCHEMA = {
|
|
@@ -101,6 +110,30 @@ const CONFIG_SCHEMA = {
|
|
|
101
110
|
additionalProperties: false
|
|
102
111
|
}
|
|
103
112
|
},
|
|
113
|
+
sharedBundleMerge: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
items: {
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
overlapThreshold: {
|
|
119
|
+
type: 'number'
|
|
120
|
+
},
|
|
121
|
+
maxBundleSize: {
|
|
122
|
+
type: 'number'
|
|
123
|
+
},
|
|
124
|
+
sourceBundles: {
|
|
125
|
+
type: 'array',
|
|
126
|
+
items: {
|
|
127
|
+
type: 'string'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
minBundlesInGroup: {
|
|
131
|
+
type: 'number'
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
additionalProperties: false
|
|
135
|
+
}
|
|
136
|
+
},
|
|
104
137
|
minBundles: {
|
|
105
138
|
type: 'number'
|
|
106
139
|
},
|
|
@@ -115,14 +148,25 @@ const CONFIG_SCHEMA = {
|
|
|
115
148
|
},
|
|
116
149
|
loadConditionalBundlesInParallel: {
|
|
117
150
|
type: 'boolean'
|
|
151
|
+
},
|
|
152
|
+
sharedBundleMergeThreshold: {
|
|
153
|
+
type: 'number'
|
|
118
154
|
}
|
|
119
155
|
},
|
|
120
156
|
additionalProperties: false
|
|
121
157
|
};
|
|
122
158
|
async function loadBundlerConfig(config, options, logger) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
var _conf;
|
|
160
|
+
let conf;
|
|
161
|
+
if ((0, _featureFlags().getFeatureFlag)('resolveBundlerConfigFromCwd')) {
|
|
162
|
+
conf = await config.getConfigFrom(`${process.cwd()}/index`, [], {
|
|
163
|
+
packageKey: '@atlaspack/bundler-default'
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
conf = await config.getConfig([], {
|
|
167
|
+
packageKey: '@atlaspack/bundler-default'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
126
170
|
if (!conf) {
|
|
127
171
|
const modDefault = {
|
|
128
172
|
...HTTP_OPTIONS['2'],
|
|
@@ -130,7 +174,7 @@ async function loadBundlerConfig(config, options, logger) {
|
|
|
130
174
|
};
|
|
131
175
|
return modDefault;
|
|
132
176
|
}
|
|
133
|
-
(0, _assert().default)((conf === null ||
|
|
177
|
+
(0, _assert().default)(((_conf = conf) === null || _conf === void 0 ? void 0 : _conf.contents) != null);
|
|
134
178
|
let modeConfig = resolveModeConfig(conf.contents, options.mode);
|
|
135
179
|
|
|
136
180
|
// minBundles will be ignored if shared bundles are disabled
|
|
@@ -172,6 +216,7 @@ async function loadBundlerConfig(config, options, logger) {
|
|
|
172
216
|
return {
|
|
173
217
|
minBundles: modeConfig.minBundles ?? defaults.minBundles,
|
|
174
218
|
minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
|
|
219
|
+
sharedBundleMerge: modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
|
|
175
220
|
maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
|
|
176
221
|
projectRoot: options.projectRoot,
|
|
177
222
|
disableSharedBundles: modeConfig.disableSharedBundles ?? defaults.disableSharedBundles,
|
package/lib/idealGraph.js
CHANGED
|
@@ -47,6 +47,7 @@ function _nullthrows() {
|
|
|
47
47
|
};
|
|
48
48
|
return data;
|
|
49
49
|
}
|
|
50
|
+
var _bundleMerge = require("./bundleMerge");
|
|
50
51
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
51
52
|
/* BundleRoot - An asset that is the main entry of a Bundle. */
|
|
52
53
|
const dependencyPriorityEdges = {
|
|
@@ -65,6 +66,7 @@ const idealBundleGraphEdges = exports.idealBundleGraphEdges = Object.freeze({
|
|
|
65
66
|
// expect from default bundler
|
|
66
67
|
|
|
67
68
|
function createIdealGraph(assetGraph, config, entries, logger) {
|
|
69
|
+
var _config$sharedBundleM;
|
|
68
70
|
// Asset to the bundle and group it's an entry of
|
|
69
71
|
let bundleRoots = new Map();
|
|
70
72
|
let bundles = new Map();
|
|
@@ -183,6 +185,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
|
|
|
183
185
|
};
|
|
184
186
|
}();
|
|
185
187
|
let manualBundleToInternalizedAsset = new (_utils().DefaultMap)(() => []);
|
|
188
|
+
let mergeSourceBundleLookup = new Map();
|
|
189
|
+
let mergeSourceBundleAssets = new Set((_config$sharedBundleM = config.sharedBundleMerge) === null || _config$sharedBundleM === void 0 ? void 0 : _config$sharedBundleM.flatMap(c => {
|
|
190
|
+
var _c$sourceBundles;
|
|
191
|
+
return ((_c$sourceBundles = c.sourceBundles) === null || _c$sourceBundles === void 0 ? void 0 : _c$sourceBundles.map(assetMatch => _path().default.join(config.projectRoot, assetMatch))) ?? [];
|
|
192
|
+
}));
|
|
186
193
|
|
|
187
194
|
/**
|
|
188
195
|
* Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
|
|
@@ -249,6 +256,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
|
|
|
249
256
|
bundleRoots.set(childAsset, [bundleId, bundleId]);
|
|
250
257
|
bundleGroupBundleIds.add(bundleId);
|
|
251
258
|
bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
|
|
259
|
+
// If this asset is relevant for merging then track it's source
|
|
260
|
+
// bundle id for later
|
|
261
|
+
if (mergeSourceBundleAssets.has(childAsset.filePath)) {
|
|
262
|
+
mergeSourceBundleLookup.set(_path().default.relative(config.projectRoot, childAsset.filePath), bundleId);
|
|
263
|
+
}
|
|
252
264
|
if (manualSharedObject) {
|
|
253
265
|
// MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
|
|
254
266
|
// since MSBs should not have main entry assets
|
|
@@ -848,6 +860,12 @@ function createIdealGraph(assetGraph, config, entries, logger) {
|
|
|
848
860
|
}
|
|
849
861
|
}
|
|
850
862
|
|
|
863
|
+
// Step merge shared bundles that meet the overlap threshold
|
|
864
|
+
// This step is skipped by default as the threshold defaults to 1
|
|
865
|
+
if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
|
|
866
|
+
mergeOverlapBundles(config.sharedBundleMerge);
|
|
867
|
+
}
|
|
868
|
+
|
|
851
869
|
// Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
|
|
852
870
|
// their source bundles, and remove the bundle.
|
|
853
871
|
// We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
|
|
@@ -857,9 +875,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
|
|
|
857
875
|
removeBundle(bundleGraph, bundleNodeId, assetReference);
|
|
858
876
|
}
|
|
859
877
|
}
|
|
860
|
-
let modifiedSourceBundles = new Set();
|
|
861
878
|
|
|
862
879
|
// Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
|
|
880
|
+
let modifiedSourceBundles = new Set();
|
|
863
881
|
if (config.disableSharedBundles === false) {
|
|
864
882
|
for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
|
|
865
883
|
// Find shared bundles in this bundle group.
|
|
@@ -944,6 +962,68 @@ function createIdealGraph(assetGraph, config, entries, logger) {
|
|
|
944
962
|
}
|
|
945
963
|
}
|
|
946
964
|
}
|
|
965
|
+
function mergeBundles(bundleGraph, bundleToKeepId, bundleToRemoveId, assetReference) {
|
|
966
|
+
let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId));
|
|
967
|
+
let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId));
|
|
968
|
+
(0, _assert().default)(bundleToKeep !== 'root' && bundleToRemove !== 'root');
|
|
969
|
+
for (let asset of bundleToRemove.assets) {
|
|
970
|
+
bundleToKeep.assets.add(asset);
|
|
971
|
+
bundleToKeep.size += asset.stats.size;
|
|
972
|
+
let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
|
|
973
|
+
assetReference.set(asset, newAssetReference);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Merge any internalized assets
|
|
977
|
+
(0, _assert().default)(bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets, 'All shared bundles should have internalized assets');
|
|
978
|
+
bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
|
|
979
|
+
for (let sourceBundleId of bundleToRemove.sourceBundles) {
|
|
980
|
+
if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
bundleToKeep.sourceBundles.add(sourceBundleId);
|
|
984
|
+
bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
|
|
985
|
+
}
|
|
986
|
+
bundleGraph.removeNode(bundleToRemoveId);
|
|
987
|
+
}
|
|
988
|
+
function mergeOverlapBundles(mergeConfig) {
|
|
989
|
+
// Find all shared bundles
|
|
990
|
+
let sharedBundles = new Set();
|
|
991
|
+
bundleGraph.traverse(nodeId => {
|
|
992
|
+
let bundle = bundleGraph.getNode(nodeId);
|
|
993
|
+
if (!bundle) {
|
|
994
|
+
throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
|
|
995
|
+
}
|
|
996
|
+
if (bundle === 'root') {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Only consider JS shared bundles and non-reused bundles.
|
|
1001
|
+
// These count potentially be considered for merging in future but they're
|
|
1002
|
+
// more complicated to merge
|
|
1003
|
+
if (bundle.sourceBundles.size > 0 && bundle.manualSharedBundle == null && !bundle.mainEntryAsset && bundle.type === 'js') {
|
|
1004
|
+
sharedBundles.add(nodeId);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
let clusters = (0, _bundleMerge.findMergeCandidates)(bundleGraph, Array.from(sharedBundles), mergeConfig.map(config => {
|
|
1008
|
+
var _config$sourceBundles;
|
|
1009
|
+
return {
|
|
1010
|
+
...config,
|
|
1011
|
+
sourceBundles: (_config$sourceBundles = config.sourceBundles) === null || _config$sourceBundles === void 0 ? void 0 : _config$sourceBundles.map(assetMatch => {
|
|
1012
|
+
let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
|
|
1013
|
+
if (sourceBundleNodeId == null) {
|
|
1014
|
+
throw new Error(`Source bundle ${assetMatch} not found in merge source bundle lookup`);
|
|
1015
|
+
}
|
|
1016
|
+
return sourceBundleNodeId;
|
|
1017
|
+
})
|
|
1018
|
+
};
|
|
1019
|
+
}));
|
|
1020
|
+
for (let cluster of clusters) {
|
|
1021
|
+
let [mergeTarget, ...rest] = cluster;
|
|
1022
|
+
for (let bundleIdToMerge of rest) {
|
|
1023
|
+
mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
947
1027
|
function getBigIntFromContentKey(contentKey) {
|
|
948
1028
|
let b = Buffer.alloc(64);
|
|
949
1029
|
b.write(contentKey);
|
package/lib/memoize.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.clearCaches = clearCaches;
|
|
7
|
+
exports.memoize = memoize;
|
|
8
|
+
function _manyKeysMap() {
|
|
9
|
+
const data = _interopRequireDefault(require("many-keys-map"));
|
|
10
|
+
_manyKeysMap = function () {
|
|
11
|
+
return data;
|
|
12
|
+
};
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
16
|
+
// $FlowFixMe
|
|
17
|
+
let caches = [];
|
|
18
|
+
function clearCaches() {
|
|
19
|
+
for (let cache of caches) {
|
|
20
|
+
cache.clear();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function memoize(fn) {
|
|
24
|
+
let cache = new (_manyKeysMap().default)();
|
|
25
|
+
caches.push(cache);
|
|
26
|
+
return function (...args) {
|
|
27
|
+
// Navigate through the cache hierarchy
|
|
28
|
+
let cached = cache.get(args);
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
// If the result is cached, return it
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Calculate the result and cache it
|
|
35
|
+
const result = fn.apply(this, args);
|
|
36
|
+
// $FlowFixMe
|
|
37
|
+
cache.set(args, result);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
}
|
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.61+1b52b99db",
|
|
4
4
|
"license": "(MIT OR Apache-2.0)",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"publishConfig": {
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"node": ">= 16.0.0"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@atlaspack/diagnostic": "2.14.1-canary.
|
|
20
|
-
"@atlaspack/feature-flags": "2.14.1-canary.
|
|
21
|
-
"@atlaspack/graph": "3.4.1-canary.
|
|
22
|
-
"@atlaspack/plugin": "2.14.5-canary.
|
|
23
|
-
"@atlaspack/rust": "3.2.1-canary.
|
|
24
|
-
"@atlaspack/utils": "2.14.5-canary.
|
|
19
|
+
"@atlaspack/diagnostic": "2.14.1-canary.129+1b52b99db",
|
|
20
|
+
"@atlaspack/feature-flags": "2.14.1-canary.129+1b52b99db",
|
|
21
|
+
"@atlaspack/graph": "3.4.1-canary.129+1b52b99db",
|
|
22
|
+
"@atlaspack/plugin": "2.14.5-canary.61+1b52b99db",
|
|
23
|
+
"@atlaspack/rust": "3.2.1-canary.61+1b52b99db",
|
|
24
|
+
"@atlaspack/utils": "2.14.5-canary.61+1b52b99db",
|
|
25
|
+
"many-keys-map": "^2.0.1",
|
|
25
26
|
"nullthrows": "^1.1.1"
|
|
26
27
|
},
|
|
27
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "1b52b99db4298b04c1a6eb0f97994d75a2d436f9"
|
|
28
29
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// @flow strict-local
|
|
2
|
+
|
|
3
|
+
import invariant from 'assert';
|
|
4
|
+
import nullthrows from 'nullthrows';
|
|
5
|
+
import {ContentGraph, BitSet} from '@atlaspack/graph';
|
|
6
|
+
import type {NodeId} from '@atlaspack/graph';
|
|
7
|
+
import type {Bundle, IdealBundleGraph} from './idealGraph';
|
|
8
|
+
import {memoize, clearCaches} from './memoize';
|
|
9
|
+
|
|
10
|
+
function getBundlesForBundleGroup(
|
|
11
|
+
bundleGraph: IdealBundleGraph,
|
|
12
|
+
bundleGroupId: NodeId,
|
|
13
|
+
): number {
|
|
14
|
+
let count = 0;
|
|
15
|
+
bundleGraph.traverse((nodeId) => {
|
|
16
|
+
if (bundleGraph.getNode(nodeId)?.bundleBehavior !== 'inline') {
|
|
17
|
+
count++;
|
|
18
|
+
}
|
|
19
|
+
}, bundleGroupId);
|
|
20
|
+
return count;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let getBundleOverlapBitSet = (
|
|
24
|
+
sourceBundlesA: BitSet,
|
|
25
|
+
sourceBundlesB: BitSet,
|
|
26
|
+
): number => {
|
|
27
|
+
let allSourceBundles = BitSet.union(sourceBundlesA, sourceBundlesB);
|
|
28
|
+
let sharedSourceBundles = BitSet.intersect(sourceBundlesA, sourceBundlesB);
|
|
29
|
+
|
|
30
|
+
return sharedSourceBundles.size() / allSourceBundles.size();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Returns a decimal showing the proportion source bundles are common to
|
|
34
|
+
// both bundles versus the total number of source bundles.
|
|
35
|
+
function checkBundleThreshold(
|
|
36
|
+
bundleA: MergeCandidate,
|
|
37
|
+
bundleB: MergeCandidate,
|
|
38
|
+
threshold: number,
|
|
39
|
+
): boolean {
|
|
40
|
+
return (
|
|
41
|
+
getBundleOverlapBitSet(
|
|
42
|
+
bundleA.sourceBundleBitSet,
|
|
43
|
+
bundleB.sourceBundleBitSet,
|
|
44
|
+
) >= threshold
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let checkSharedSourceBundles = memoize(
|
|
49
|
+
(bundle: Bundle, importantAncestorBundles: Array<NodeId>): boolean => {
|
|
50
|
+
return importantAncestorBundles.every((ancestorId) =>
|
|
51
|
+
bundle.sourceBundles.has(ancestorId),
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
let hasSuitableBundleGroup = memoize(
|
|
57
|
+
(
|
|
58
|
+
bundleGraph: IdealBundleGraph,
|
|
59
|
+
bundle: Bundle,
|
|
60
|
+
minBundlesInGroup: number,
|
|
61
|
+
): boolean => {
|
|
62
|
+
for (let sourceBundle of bundle.sourceBundles) {
|
|
63
|
+
let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
|
|
64
|
+
|
|
65
|
+
if (bundlesInGroup >= minBundlesInGroup) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
function validMerge(
|
|
74
|
+
bundleGraph: IdealBundleGraph,
|
|
75
|
+
config: MergeGroup,
|
|
76
|
+
bundleA: MergeCandidate,
|
|
77
|
+
bundleB: MergeCandidate,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (config.maxBundleSize != null) {
|
|
80
|
+
if (
|
|
81
|
+
bundleA.bundle.size > config.maxBundleSize ||
|
|
82
|
+
bundleB.bundle.size > config.maxBundleSize
|
|
83
|
+
) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (config.overlapThreshold != null) {
|
|
89
|
+
if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.sourceBundles != null) {
|
|
95
|
+
if (
|
|
96
|
+
!checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) ||
|
|
97
|
+
!checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)
|
|
98
|
+
) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (config.minBundlesInGroup != null) {
|
|
104
|
+
if (
|
|
105
|
+
!hasSuitableBundleGroup(
|
|
106
|
+
bundleGraph,
|
|
107
|
+
bundleA.bundle,
|
|
108
|
+
config.minBundlesInGroup,
|
|
109
|
+
) ||
|
|
110
|
+
!hasSuitableBundleGroup(
|
|
111
|
+
bundleGraph,
|
|
112
|
+
bundleB.bundle,
|
|
113
|
+
config.minBundlesInGroup,
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getMergeClusters(
|
|
124
|
+
graph: ContentGraph<NodeId, EdgeType>,
|
|
125
|
+
candidates: Map<NodeId, EdgeType>,
|
|
126
|
+
): Array<Array<NodeId>> {
|
|
127
|
+
let clusters = [];
|
|
128
|
+
|
|
129
|
+
for (let [candidate, edgeType] of candidates.entries()) {
|
|
130
|
+
let cluster: Array<NodeId> = [];
|
|
131
|
+
|
|
132
|
+
graph.traverse(
|
|
133
|
+
(nodeId) => {
|
|
134
|
+
cluster.push(nullthrows(graph.getNode(nodeId)));
|
|
135
|
+
// Remove node from candidates as it has already been processed
|
|
136
|
+
candidates.delete(nodeId);
|
|
137
|
+
},
|
|
138
|
+
candidate,
|
|
139
|
+
edgeType,
|
|
140
|
+
);
|
|
141
|
+
clusters.push(cluster);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return clusters;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
type MergeCandidate = {|
|
|
148
|
+
bundle: Bundle,
|
|
149
|
+
id: NodeId,
|
|
150
|
+
sourceBundleBitSet: BitSet,
|
|
151
|
+
contentKey: string,
|
|
152
|
+
|};
|
|
153
|
+
function getPossibleMergeCandidates(
|
|
154
|
+
bundleGraph: IdealBundleGraph,
|
|
155
|
+
bundles: Array<NodeId>,
|
|
156
|
+
): Array<[MergeCandidate, MergeCandidate]> {
|
|
157
|
+
let mergeCandidates = bundles.map((bundleId) => {
|
|
158
|
+
let bundle = bundleGraph.getNode(bundleId);
|
|
159
|
+
invariant(bundle && bundle !== 'root', 'Bundle should exist');
|
|
160
|
+
|
|
161
|
+
let sourceBundleBitSet = new BitSet(bundleGraph.nodes.length);
|
|
162
|
+
for (let sourceBundle of bundle.sourceBundles) {
|
|
163
|
+
sourceBundleBitSet.add(sourceBundle);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
id: bundleId,
|
|
168
|
+
bundle,
|
|
169
|
+
sourceBundleBitSet,
|
|
170
|
+
contentKey: bundleId.toString(),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const uniquePairs = [];
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < mergeCandidates.length; i++) {
|
|
177
|
+
for (let j = i + 1; j < mergeCandidates.length; j++) {
|
|
178
|
+
let a = mergeCandidates[i];
|
|
179
|
+
let b = mergeCandidates[j];
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
// $FlowFixMe both bundles will always have internalizedAssets
|
|
183
|
+
a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)
|
|
184
|
+
) {
|
|
185
|
+
uniquePairs.push([a, b]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return uniquePairs;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type MergeGroup = {|
|
|
193
|
+
overlapThreshold?: number,
|
|
194
|
+
maxBundleSize?: number,
|
|
195
|
+
sourceBundles?: Array<NodeId>,
|
|
196
|
+
minBundlesInGroup?: number,
|
|
197
|
+
|};
|
|
198
|
+
type EdgeType = number;
|
|
199
|
+
|
|
200
|
+
export function findMergeCandidates(
|
|
201
|
+
bundleGraph: IdealBundleGraph,
|
|
202
|
+
bundles: Array<NodeId>,
|
|
203
|
+
config: Array<MergeGroup>,
|
|
204
|
+
): Array<Array<NodeId>> {
|
|
205
|
+
let graph = new ContentGraph<NodeId, EdgeType>();
|
|
206
|
+
let candidates = new Map<NodeId, EdgeType>();
|
|
207
|
+
|
|
208
|
+
let allPossibleMergeCandidates = getPossibleMergeCandidates(
|
|
209
|
+
bundleGraph,
|
|
210
|
+
bundles,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Build graph of clustered merge candidates
|
|
214
|
+
for (let i = 0; i < config.length; i++) {
|
|
215
|
+
// Ensure edge type coresponds to config index
|
|
216
|
+
let edgeType = i + 1;
|
|
217
|
+
|
|
218
|
+
for (let group of allPossibleMergeCandidates) {
|
|
219
|
+
let candidateA = group[0];
|
|
220
|
+
let candidateB = group[1];
|
|
221
|
+
|
|
222
|
+
if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let bundleNode = graph.addNodeByContentKeyIfNeeded(
|
|
227
|
+
candidateA.contentKey,
|
|
228
|
+
candidateA.id,
|
|
229
|
+
);
|
|
230
|
+
let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
|
|
231
|
+
candidateB.contentKey,
|
|
232
|
+
candidateB.id,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Add edge in both directions
|
|
236
|
+
graph.addEdge(bundleNode, otherBundleNode, edgeType);
|
|
237
|
+
graph.addEdge(otherBundleNode, bundleNode, edgeType);
|
|
238
|
+
|
|
239
|
+
candidates.set(bundleNode, edgeType);
|
|
240
|
+
candidates.set(otherBundleNode, edgeType);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Remove bundles that have been allocated to a higher priority merge
|
|
244
|
+
allPossibleMergeCandidates = allPossibleMergeCandidates.filter(
|
|
245
|
+
(group) =>
|
|
246
|
+
!graph.hasContentKey(group[0].contentKey) &&
|
|
247
|
+
!graph.hasContentKey(group[1].contentKey),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
clearCaches();
|
|
252
|
+
|
|
253
|
+
return getMergeClusters(graph, candidates);
|
|
254
|
+
}
|
package/src/bundlerConfig.js
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
BuildMode,
|
|
8
8
|
PluginLogger,
|
|
9
9
|
} from '@atlaspack/types';
|
|
10
|
+
import {getFeatureFlag} from '@atlaspack/feature-flags';
|
|
10
11
|
import {type SchemaEntity, validateSchema} from '@atlaspack/utils';
|
|
11
12
|
import invariant from 'assert';
|
|
12
13
|
|
|
@@ -20,6 +21,13 @@ type ManualSharedBundles = Array<{|
|
|
|
20
21
|
split?: number,
|
|
21
22
|
|}>;
|
|
22
23
|
|
|
24
|
+
export type MergeCandidates = Array<{|
|
|
25
|
+
overlapThreshold?: number,
|
|
26
|
+
maxBundleSize?: number,
|
|
27
|
+
sourceBundles?: Array<string>,
|
|
28
|
+
minBundlesInGroup?: number,
|
|
29
|
+
|}>;
|
|
30
|
+
|
|
23
31
|
type BaseBundlerConfig = {|
|
|
24
32
|
http?: number,
|
|
25
33
|
minBundles?: number,
|
|
@@ -28,6 +36,7 @@ type BaseBundlerConfig = {|
|
|
|
28
36
|
disableSharedBundles?: boolean,
|
|
29
37
|
manualSharedBundles?: ManualSharedBundles,
|
|
30
38
|
loadConditionalBundlesInParallel?: boolean,
|
|
39
|
+
sharedBundleMerge?: MergeCandidates,
|
|
31
40
|
|};
|
|
32
41
|
|
|
33
42
|
type BundlerConfig = {|
|
|
@@ -42,6 +51,7 @@ export type ResolvedBundlerConfig = {|
|
|
|
42
51
|
disableSharedBundles: boolean,
|
|
43
52
|
manualSharedBundles: ManualSharedBundles,
|
|
44
53
|
loadConditionalBundlesInParallel?: boolean,
|
|
54
|
+
sharedBundleMerge?: MergeCandidates,
|
|
45
55
|
|};
|
|
46
56
|
|
|
47
57
|
function resolveModeConfig(
|
|
@@ -76,6 +86,7 @@ const HTTP_OPTIONS = {
|
|
|
76
86
|
minBundleSize: 30000,
|
|
77
87
|
maxParallelRequests: 6,
|
|
78
88
|
disableSharedBundles: false,
|
|
89
|
+
sharedBundleMerge: [],
|
|
79
90
|
},
|
|
80
91
|
'2': {
|
|
81
92
|
minBundles: 1,
|
|
@@ -83,6 +94,7 @@ const HTTP_OPTIONS = {
|
|
|
83
94
|
minBundleSize: 20000,
|
|
84
95
|
maxParallelRequests: 25,
|
|
85
96
|
disableSharedBundles: false,
|
|
97
|
+
sharedBundleMerge: [],
|
|
86
98
|
},
|
|
87
99
|
};
|
|
88
100
|
|
|
@@ -124,6 +136,30 @@ const CONFIG_SCHEMA: SchemaEntity = {
|
|
|
124
136
|
additionalProperties: false,
|
|
125
137
|
},
|
|
126
138
|
},
|
|
139
|
+
sharedBundleMerge: {
|
|
140
|
+
type: 'array',
|
|
141
|
+
items: {
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
overlapThreshold: {
|
|
145
|
+
type: 'number',
|
|
146
|
+
},
|
|
147
|
+
maxBundleSize: {
|
|
148
|
+
type: 'number',
|
|
149
|
+
},
|
|
150
|
+
sourceBundles: {
|
|
151
|
+
type: 'array',
|
|
152
|
+
items: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
minBundlesInGroup: {
|
|
157
|
+
type: 'number',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
127
163
|
minBundles: {
|
|
128
164
|
type: 'number',
|
|
129
165
|
},
|
|
@@ -139,6 +175,9 @@ const CONFIG_SCHEMA: SchemaEntity = {
|
|
|
139
175
|
loadConditionalBundlesInParallel: {
|
|
140
176
|
type: 'boolean',
|
|
141
177
|
},
|
|
178
|
+
sharedBundleMergeThreshold: {
|
|
179
|
+
type: 'number',
|
|
180
|
+
},
|
|
142
181
|
},
|
|
143
182
|
additionalProperties: false,
|
|
144
183
|
};
|
|
@@ -148,9 +187,17 @@ export async function loadBundlerConfig(
|
|
|
148
187
|
options: PluginOptions,
|
|
149
188
|
logger: PluginLogger,
|
|
150
189
|
): Promise<ResolvedBundlerConfig> {
|
|
151
|
-
let conf
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
let conf;
|
|
191
|
+
|
|
192
|
+
if (getFeatureFlag('resolveBundlerConfigFromCwd')) {
|
|
193
|
+
conf = await config.getConfigFrom(`${process.cwd()}/index`, [], {
|
|
194
|
+
packageKey: '@atlaspack/bundler-default',
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
conf = await config.getConfig<BundlerConfig>([], {
|
|
198
|
+
packageKey: '@atlaspack/bundler-default',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
154
201
|
|
|
155
202
|
if (!conf) {
|
|
156
203
|
const modDefault = {
|
|
@@ -224,6 +271,8 @@ export async function loadBundlerConfig(
|
|
|
224
271
|
return {
|
|
225
272
|
minBundles: modeConfig.minBundles ?? defaults.minBundles,
|
|
226
273
|
minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
|
|
274
|
+
sharedBundleMerge:
|
|
275
|
+
modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
|
|
227
276
|
maxParallelRequests:
|
|
228
277
|
modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
|
|
229
278
|
projectRoot: options.projectRoot,
|
package/src/idealGraph.js
CHANGED
|
@@ -23,7 +23,8 @@ import {DefaultMap, globToRegex} from '@atlaspack/utils';
|
|
|
23
23
|
import invariant from 'assert';
|
|
24
24
|
import nullthrows from 'nullthrows';
|
|
25
25
|
|
|
26
|
-
import type
|
|
26
|
+
import {findMergeCandidates, type MergeGroup} from './bundleMerge';
|
|
27
|
+
import type {ResolvedBundlerConfig, MergeCandidates} from './bundlerConfig';
|
|
27
28
|
|
|
28
29
|
/* BundleRoot - An asset that is the main entry of a Bundle. */
|
|
29
30
|
type BundleRoot = Asset;
|
|
@@ -67,7 +68,7 @@ export const idealBundleGraphEdges = Object.freeze({
|
|
|
67
68
|
conditional: 2,
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
type IdealBundleGraph = Graph<
|
|
71
|
+
export type IdealBundleGraph = Graph<
|
|
71
72
|
Bundle | 'root',
|
|
72
73
|
$Values<typeof idealBundleGraphEdges>,
|
|
73
74
|
>;
|
|
@@ -244,6 +245,16 @@ export function createIdealGraph(
|
|
|
244
245
|
Array<Asset>,
|
|
245
246
|
> = new DefaultMap(() => []);
|
|
246
247
|
|
|
248
|
+
let mergeSourceBundleLookup = new Map<string, NodeId>();
|
|
249
|
+
let mergeSourceBundleAssets = new Set(
|
|
250
|
+
config.sharedBundleMerge?.flatMap(
|
|
251
|
+
(c) =>
|
|
252
|
+
c.sourceBundles?.map((assetMatch) =>
|
|
253
|
+
path.join(config.projectRoot, assetMatch),
|
|
254
|
+
) ?? [],
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
|
|
247
258
|
/**
|
|
248
259
|
* Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
|
|
249
260
|
* for asset type changes, parallel, inline, and async or lazy dependencies,
|
|
@@ -334,6 +345,14 @@ export function createIdealGraph(
|
|
|
334
345
|
bundleRoots.set(childAsset, [bundleId, bundleId]);
|
|
335
346
|
bundleGroupBundleIds.add(bundleId);
|
|
336
347
|
bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
|
|
348
|
+
// If this asset is relevant for merging then track it's source
|
|
349
|
+
// bundle id for later
|
|
350
|
+
if (mergeSourceBundleAssets.has(childAsset.filePath)) {
|
|
351
|
+
mergeSourceBundleLookup.set(
|
|
352
|
+
path.relative(config.projectRoot, childAsset.filePath),
|
|
353
|
+
bundleId,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
337
356
|
if (manualSharedObject) {
|
|
338
357
|
// MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
|
|
339
358
|
// since MSBs should not have main entry assets
|
|
@@ -1128,6 +1147,12 @@ export function createIdealGraph(
|
|
|
1128
1147
|
}
|
|
1129
1148
|
}
|
|
1130
1149
|
|
|
1150
|
+
// Step merge shared bundles that meet the overlap threshold
|
|
1151
|
+
// This step is skipped by default as the threshold defaults to 1
|
|
1152
|
+
if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
|
|
1153
|
+
mergeOverlapBundles(config.sharedBundleMerge);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1131
1156
|
// Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
|
|
1132
1157
|
// their source bundles, and remove the bundle.
|
|
1133
1158
|
// We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
|
|
@@ -1143,9 +1168,9 @@ export function createIdealGraph(
|
|
|
1143
1168
|
}
|
|
1144
1169
|
}
|
|
1145
1170
|
|
|
1171
|
+
// Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
|
|
1146
1172
|
let modifiedSourceBundles = new Set();
|
|
1147
1173
|
|
|
1148
|
-
// Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
|
|
1149
1174
|
if (config.disableSharedBundles === false) {
|
|
1150
1175
|
for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
|
|
1151
1176
|
// Find shared bundles in this bundle group.
|
|
@@ -1249,6 +1274,102 @@ export function createIdealGraph(
|
|
|
1249
1274
|
}
|
|
1250
1275
|
}
|
|
1251
1276
|
|
|
1277
|
+
function mergeBundles(
|
|
1278
|
+
bundleGraph: IdealBundleGraph,
|
|
1279
|
+
bundleToKeepId: NodeId,
|
|
1280
|
+
bundleToRemoveId: NodeId,
|
|
1281
|
+
assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
|
|
1282
|
+
) {
|
|
1283
|
+
let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
|
|
1284
|
+
let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
|
|
1285
|
+
invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
|
|
1286
|
+
for (let asset of bundleToRemove.assets) {
|
|
1287
|
+
bundleToKeep.assets.add(asset);
|
|
1288
|
+
bundleToKeep.size += asset.stats.size;
|
|
1289
|
+
|
|
1290
|
+
let newAssetReference = assetReference
|
|
1291
|
+
.get(asset)
|
|
1292
|
+
.map(([dep, bundle]) =>
|
|
1293
|
+
bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
assetReference.set(asset, newAssetReference);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Merge any internalized assets
|
|
1300
|
+
invariant(
|
|
1301
|
+
bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets,
|
|
1302
|
+
'All shared bundles should have internalized assets',
|
|
1303
|
+
);
|
|
1304
|
+
bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
|
|
1305
|
+
|
|
1306
|
+
for (let sourceBundleId of bundleToRemove.sourceBundles) {
|
|
1307
|
+
if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
bundleToKeep.sourceBundles.add(sourceBundleId);
|
|
1312
|
+
bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
bundleGraph.removeNode(bundleToRemoveId);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function mergeOverlapBundles(mergeConfig: MergeCandidates) {
|
|
1319
|
+
// Find all shared bundles
|
|
1320
|
+
let sharedBundles = new Set<NodeId>();
|
|
1321
|
+
bundleGraph.traverse((nodeId) => {
|
|
1322
|
+
let bundle = bundleGraph.getNode(nodeId);
|
|
1323
|
+
|
|
1324
|
+
if (!bundle) {
|
|
1325
|
+
throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (bundle === 'root') {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Only consider JS shared bundles and non-reused bundles.
|
|
1333
|
+
// These count potentially be considered for merging in future but they're
|
|
1334
|
+
// more complicated to merge
|
|
1335
|
+
if (
|
|
1336
|
+
bundle.sourceBundles.size > 0 &&
|
|
1337
|
+
bundle.manualSharedBundle == null &&
|
|
1338
|
+
!bundle.mainEntryAsset &&
|
|
1339
|
+
bundle.type === 'js'
|
|
1340
|
+
) {
|
|
1341
|
+
sharedBundles.add(nodeId);
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
let clusters = findMergeCandidates(
|
|
1346
|
+
bundleGraph,
|
|
1347
|
+
Array.from(sharedBundles),
|
|
1348
|
+
mergeConfig.map((config): MergeGroup => ({
|
|
1349
|
+
...config,
|
|
1350
|
+
sourceBundles: config.sourceBundles?.map((assetMatch) => {
|
|
1351
|
+
let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
|
|
1352
|
+
|
|
1353
|
+
if (sourceBundleNodeId == null) {
|
|
1354
|
+
throw new Error(
|
|
1355
|
+
`Source bundle ${assetMatch} not found in merge source bundle lookup`,
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return sourceBundleNodeId;
|
|
1360
|
+
}),
|
|
1361
|
+
})),
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
for (let cluster of clusters) {
|
|
1365
|
+
let [mergeTarget, ...rest] = cluster;
|
|
1366
|
+
|
|
1367
|
+
for (let bundleIdToMerge of rest) {
|
|
1368
|
+
mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1252
1373
|
function getBigIntFromContentKey(contentKey) {
|
|
1253
1374
|
let b = Buffer.alloc(64);
|
|
1254
1375
|
b.write(contentKey);
|
package/src/memoize.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @flow strict
|
|
2
|
+
|
|
3
|
+
// $FlowFixMe
|
|
4
|
+
import ManyKeysMap from 'many-keys-map';
|
|
5
|
+
|
|
6
|
+
let caches = [];
|
|
7
|
+
|
|
8
|
+
export function clearCaches() {
|
|
9
|
+
for (let cache of caches) {
|
|
10
|
+
cache.clear();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function memoize<Args: Array<mixed>, Return>(
|
|
15
|
+
fn: (...args: Args) => Return,
|
|
16
|
+
): (...args: Args) => Return {
|
|
17
|
+
let cache = new ManyKeysMap();
|
|
18
|
+
caches.push(cache);
|
|
19
|
+
|
|
20
|
+
return function (...args: Args): Return {
|
|
21
|
+
// Navigate through the cache hierarchy
|
|
22
|
+
let cached = cache.get(args);
|
|
23
|
+
if (cached !== undefined) {
|
|
24
|
+
// If the result is cached, return it
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Calculate the result and cache it
|
|
29
|
+
const result = fn.apply(this, args);
|
|
30
|
+
// $FlowFixMe
|
|
31
|
+
cache.set(args, result);
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
}
|