@atlaspack/bundler-default 2.14.5-canary.0 → 2.14.5-canary.100

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 CHANGED
@@ -1,5 +1,257 @@
1
1
  # @atlaspack/bundler-default
2
2
 
3
+ ## 3.0.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`5ded263`](https://github.com/atlassian-labs/atlaspack/commit/5ded263c7f11b866e8885b81c73e20dd060b25be)]:
8
+ - @atlaspack/feature-flags@2.18.3
9
+ - @atlaspack/graph@3.5.5
10
+ - @atlaspack/utils@2.15.3
11
+ - @atlaspack/plugin@2.14.15
12
+
13
+ ## 3.0.5
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [[`644b157`](https://github.com/atlassian-labs/atlaspack/commit/644b157dee72a871acc2d0facf0b87b8eea51956)]:
18
+ - @atlaspack/feature-flags@2.18.2
19
+ - @atlaspack/graph@3.5.4
20
+ - @atlaspack/utils@2.15.2
21
+ - @atlaspack/plugin@2.14.14
22
+
23
+ ## 3.0.4
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [[`26aa9c5`](https://github.com/atlassian-labs/atlaspack/commit/26aa9c599d2be45ce1438a74c5fa22f39b9b554b), [`0501255`](https://github.com/atlassian-labs/atlaspack/commit/05012550da35b05ce7d356a8cc29311e7f9afdca)]:
28
+ - @atlaspack/feature-flags@2.18.1
29
+ - @atlaspack/utils@2.15.1
30
+ - @atlaspack/graph@3.5.3
31
+ - @atlaspack/plugin@2.14.13
32
+
33
+ ## 3.0.3
34
+
35
+ ### Patch Changes
36
+
37
+ - [#622](https://github.com/atlassian-labs/atlaspack/pull/622) [`e39c6cf`](https://github.com/atlassian-labs/atlaspack/commit/e39c6cf05f7e95ce5420dbcea66f401b1cbd397c) Thanks [@benjervis](https://github.com/benjervis)! - Change the overlap calculation system in findMergeCandidates to improve performance
38
+
39
+ - Updated dependencies [[`10fbcfb`](https://github.com/atlassian-labs/atlaspack/commit/10fbcfbfa49c7a83da5d7c40983e36e87f524a75), [`85c52d3`](https://github.com/atlassian-labs/atlaspack/commit/85c52d3f7717b3c84a118d18ab98cfbfd71dcbd2), [`e39c6cf`](https://github.com/atlassian-labs/atlaspack/commit/e39c6cf05f7e95ce5420dbcea66f401b1cbd397c)]:
40
+ - @atlaspack/feature-flags@2.18.0
41
+ - @atlaspack/utils@2.15.0
42
+ - @atlaspack/graph@3.5.2
43
+ - @atlaspack/plugin@2.14.12
44
+
45
+ ## 3.0.2
46
+
47
+ ### Patch Changes
48
+
49
+ - [#613](https://github.com/atlassian-labs/atlaspack/pull/613) [`4ca19d8`](https://github.com/atlassian-labs/atlaspack/commit/4ca19d8060dfcd279183e4039f2ecb43334ac44c) Thanks [@marcins](https://github.com/marcins)! - Ensure that constant modules are correctly included in MSBs even if they wouldn't otherwise be.
50
+
51
+ - Updated dependencies [[`73ea3c4`](https://github.com/atlassian-labs/atlaspack/commit/73ea3c4d85d4401fdd15abcbf988237e890e7ad3), [`b1b3693`](https://github.com/atlassian-labs/atlaspack/commit/b1b369317c66f8a431c170df2ebba4fa5b2e38ef)]:
52
+ - @atlaspack/feature-flags@2.17.0
53
+ - @atlaspack/graph@3.5.1
54
+ - @atlaspack/utils@2.14.11
55
+ - @atlaspack/plugin@2.14.11
56
+
57
+ ## 3.0.1
58
+
59
+ ### Patch Changes
60
+
61
+ - [#608](https://github.com/atlassian-labs/atlaspack/pull/608) [`471b99e`](https://github.com/atlassian-labs/atlaspack/commit/471b99e41b4d97328c88f65e90bea284372cb1b0) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Fix require of ES Module error
62
+
63
+ ## 3.0.0
64
+
65
+ ### Major Changes
66
+
67
+ - [#600](https://github.com/atlassian-labs/atlaspack/pull/600) [`1b52b99`](https://github.com/atlassian-labs/atlaspack/commit/1b52b99db4298b04c1a6eb0f97994d75a2d436f9) Thanks [@mattcompiles](https://github.com/mattcompiles)! - ### Breaking change
68
+
69
+ This new config replaces the previously released `sharedBundleMergeThreshold`.
70
+
71
+ The following options are available for each merge group.
72
+
73
+ ### Options
74
+
75
+ #### overlapThreshold
76
+
77
+ > The same as `sharedBundleMergeThreshold` from #535
78
+
79
+ Merge bundles share a percentage of source bundles
80
+
81
+ ```json
82
+ "@atlaspack/bundler-default": {
83
+ "sharedBundleMerge": [{
84
+ "overlapThreshold": 0.75
85
+ }]
86
+ }
87
+ ```
88
+
89
+ #### maxBundleSize
90
+
91
+ Merge bundles that are smaller than a configured amount of bytes.
92
+
93
+ > Keep in mind these bytes are pre-optimisation
94
+
95
+ ```json
96
+ "@atlaspack/bundler-default": {
97
+ "sharedBundleMerge": [{
98
+ "maxBundleSize": 20000
99
+ }]
100
+ }
101
+ ```
102
+
103
+ #### sourceBundles
104
+
105
+ Merge bundles that share a set of source bundles. The matching is relative to the project root, like how manual shared bundle roots work.
106
+
107
+ ```json
108
+ "@atlaspack/bundler-default": {
109
+ "sharedBundleMerge": [{
110
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
111
+ }]
112
+ }
113
+ ```
114
+
115
+ #### minBundlesInGroup
116
+
117
+ Merge bundles that belong to a bundle group that's larger than a set amount. This is useful for targetting bundles that would be deleted by the `maxParallelRequests` option.
118
+
119
+ ```json
120
+ "@atlaspack/bundler-default": {
121
+ "maxParallelRequests": 30,
122
+ "sharedBundleMerge": [{
123
+ "minBundlesInGroup": 30
124
+ }]
125
+ }
126
+ ```
127
+
128
+ ## Combining options
129
+
130
+ When multiple options are provided, all must be true for a merge to be relevant.
131
+
132
+ For example, merge bundles that are smaller than 20kb and share at least 50% of the same source bundles.
133
+
134
+ ```json
135
+ "@atlaspack/bundler-default": {
136
+ "sharedBundleMerge": [{
137
+ "overlapThreshold": 0.5,
138
+ "maxBundleSize": 20000
139
+ }]
140
+ }
141
+ ```
142
+
143
+ ## Multiple merges
144
+
145
+ You can also have multiple merge configs.
146
+
147
+ ```json
148
+ "@atlaspack/bundler-default": {
149
+ "sharedBundleMerge": [
150
+ {
151
+ "overlapThreshold": 0.75,
152
+ "maxBundleSize": 20000
153
+ },
154
+ {
155
+ "minBundlesInGroup": 30
156
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
157
+ }
158
+ ]
159
+ }
160
+ ```
161
+
162
+ ### Patch Changes
163
+
164
+ - Updated dependencies [[`1b52b99`](https://github.com/atlassian-labs/atlaspack/commit/1b52b99db4298b04c1a6eb0f97994d75a2d436f9)]:
165
+ - @atlaspack/graph@3.5.0
166
+
167
+ ## 2.16.3
168
+
169
+ ### Patch Changes
170
+
171
+ - Updated dependencies [[`35fdd4b`](https://github.com/atlassian-labs/atlaspack/commit/35fdd4b52da0af20f74667f7b8adfb2f90279b7c), [`6dd4ccb`](https://github.com/atlassian-labs/atlaspack/commit/6dd4ccb753541de32322d881f973d571dd57e4ca)]:
172
+ - @atlaspack/rust@3.3.5
173
+ - @atlaspack/plugin@2.14.10
174
+ - @atlaspack/utils@2.14.10
175
+
176
+ ## 2.16.2
177
+
178
+ ### Patch Changes
179
+
180
+ - 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)]:
181
+ - @atlaspack/rust@3.3.4
182
+ - @atlaspack/feature-flags@2.16.0
183
+ - @atlaspack/utils@2.14.9
184
+ - @atlaspack/graph@3.4.7
185
+ - @atlaspack/plugin@2.14.9
186
+
187
+ ## 2.16.1
188
+
189
+ ### Patch Changes
190
+
191
+ - 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)]:
192
+ - @atlaspack/feature-flags@2.15.1
193
+ - @atlaspack/rust@3.3.3
194
+ - @atlaspack/graph@3.4.6
195
+ - @atlaspack/utils@2.14.8
196
+ - @atlaspack/plugin@2.14.8
197
+
198
+ ## 2.16.0
199
+
200
+ ### Minor Changes
201
+
202
+ - [#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.
203
+
204
+ ### Patch Changes
205
+
206
+ - Updated dependencies [[`a1773d2`](https://github.com/atlassian-labs/atlaspack/commit/a1773d2a62d0ef7805ac7524621dcabcc1afe929), [`556d6ab`](https://github.com/atlassian-labs/atlaspack/commit/556d6ab8ede759fa7f37fcd3f4da336ef1c55e8f)]:
207
+ - @atlaspack/feature-flags@2.15.0
208
+ - @atlaspack/rust@3.3.2
209
+ - @atlaspack/graph@3.4.5
210
+ - @atlaspack/utils@2.14.7
211
+ - @atlaspack/plugin@2.14.7
212
+
213
+ ## 2.15.1
214
+
215
+ ### Patch Changes
216
+
217
+ - Updated dependencies [[`e0f5337`](https://github.com/atlassian-labs/atlaspack/commit/e0f533757bd1019dbd108a04952c87da15286e09)]:
218
+ - @atlaspack/feature-flags@2.14.4
219
+ - @atlaspack/rust@3.3.1
220
+ - @atlaspack/graph@3.4.4
221
+ - @atlaspack/utils@2.14.6
222
+ - @atlaspack/plugin@2.14.6
223
+
224
+ ## 2.15.0
225
+
226
+ ### Minor Changes
227
+
228
+ - [#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
229
+
230
+ In apps with lots of dynamic imports, many shared bundles are often removed
231
+ from the output to prevent an overload in network requests according to the
232
+ `maxParallelRequests` config. In these cases, setting `sharedBundleMergeThreshold` can
233
+ merge shared bundles with a high overlap in their source bundles (bundles that share the bundle).
234
+ This config trades-off potential overfetching to reduce asset duplication.
235
+
236
+ The following config would merge shared bundles that have a 75% or higher overlap in source bundles.
237
+
238
+ ```json
239
+ {
240
+ "@atlaspack/bundler-default": {
241
+ "sharedBundleMergeThreshold": 0.75
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Patch Changes
247
+
248
+ - 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)]:
249
+ - @atlaspack/feature-flags@2.14.3
250
+ - @atlaspack/rust@3.3.0
251
+ - @atlaspack/graph@3.4.3
252
+ - @atlaspack/utils@2.14.5
253
+ - @atlaspack/plugin@2.14.5
254
+
3
255
  ## 2.14.4
4
256
 
5
257
  ### Patch Changes
@@ -0,0 +1,160 @@
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
+ function _utils() {
29
+ const data = require("@atlaspack/utils");
30
+ _utils = function () {
31
+ return data;
32
+ };
33
+ return data;
34
+ }
35
+ var _memoize = require("./memoize");
36
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
37
+ function getBundlesForBundleGroup(bundleGraph, bundleGroupId) {
38
+ let count = 0;
39
+ bundleGraph.traverse(nodeId => {
40
+ var _bundleGraph$getNode;
41
+ if (((_bundleGraph$getNode = bundleGraph.getNode(nodeId)) === null || _bundleGraph$getNode === void 0 ? void 0 : _bundleGraph$getNode.bundleBehavior) !== 'inline') {
42
+ count++;
43
+ }
44
+ }, bundleGroupId);
45
+ return count;
46
+ }
47
+ let getBundleOverlap = (sourceBundlesA, sourceBundlesB) => {
48
+ let allSourceBundles = (0, _utils().setUnion)(sourceBundlesA, sourceBundlesB);
49
+ let sharedSourceBundles = (0, _utils().setIntersectStatic)(sourceBundlesA, sourceBundlesB);
50
+ return sharedSourceBundles.size / allSourceBundles.size;
51
+ };
52
+
53
+ // Returns a decimal showing the proportion source bundles are common to
54
+ // both bundles versus the total number of source bundles.
55
+ function checkBundleThreshold(bundleA, bundleB, threshold) {
56
+ return getBundleOverlap(bundleA.bundle.sourceBundles, bundleB.bundle.sourceBundles) >= threshold;
57
+ }
58
+ let checkSharedSourceBundles = (0, _memoize.memoize)((bundle, importantAncestorBundles) => {
59
+ return importantAncestorBundles.every(ancestorId => bundle.sourceBundles.has(ancestorId));
60
+ });
61
+ let hasSuitableBundleGroup = (0, _memoize.memoize)((bundleGraph, bundle, minBundlesInGroup) => {
62
+ for (let sourceBundle of bundle.sourceBundles) {
63
+ let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
64
+ if (bundlesInGroup >= minBundlesInGroup) {
65
+ return true;
66
+ }
67
+ }
68
+ return false;
69
+ });
70
+ function validMerge(bundleGraph, config, bundleA, bundleB) {
71
+ if (config.maxBundleSize != null) {
72
+ if (bundleA.bundle.size > config.maxBundleSize || bundleB.bundle.size > config.maxBundleSize) {
73
+ return false;
74
+ }
75
+ }
76
+ if (config.overlapThreshold != null) {
77
+ if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
78
+ return false;
79
+ }
80
+ }
81
+ if (config.sourceBundles != null) {
82
+ if (!checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) || !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)) {
83
+ return false;
84
+ }
85
+ }
86
+ if (config.minBundlesInGroup != null) {
87
+ if (!hasSuitableBundleGroup(bundleGraph, bundleA.bundle, config.minBundlesInGroup) || !hasSuitableBundleGroup(bundleGraph, bundleB.bundle, config.minBundlesInGroup)) {
88
+ return false;
89
+ }
90
+ }
91
+ return true;
92
+ }
93
+ function getMergeClusters(graph, candidates) {
94
+ let clusters = [];
95
+ for (let [candidate, edgeType] of candidates.entries()) {
96
+ let cluster = [];
97
+ graph.traverse(nodeId => {
98
+ cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
99
+ // Remove node from candidates as it has already been processed
100
+ candidates.delete(nodeId);
101
+ }, candidate, edgeType);
102
+ clusters.push(cluster);
103
+ }
104
+ return clusters;
105
+ }
106
+ function getPossibleMergeCandidates(bundleGraph, bundles) {
107
+ let mergeCandidates = bundles.map(bundleId => {
108
+ let bundle = bundleGraph.getNode(bundleId);
109
+ (0, _assert().default)(bundle && bundle !== 'root', 'Bundle should exist');
110
+ return {
111
+ id: bundleId,
112
+ bundle,
113
+ contentKey: bundleId.toString()
114
+ };
115
+ });
116
+ const uniquePairs = [];
117
+ for (let i = 0; i < mergeCandidates.length; i++) {
118
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
119
+ let a = mergeCandidates[i];
120
+ let b = mergeCandidates[j];
121
+ if (
122
+ // $FlowFixMe both bundles will always have internalizedAssets
123
+ a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)) {
124
+ uniquePairs.push([a, b]);
125
+ }
126
+ }
127
+ }
128
+ return uniquePairs;
129
+ }
130
+ function findMergeCandidates(bundleGraph, bundles, config) {
131
+ let graph = new (_graph().ContentGraph)();
132
+ let candidates = new Map();
133
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(bundleGraph, bundles);
134
+
135
+ // Build graph of clustered merge candidates
136
+ for (let i = 0; i < config.length; i++) {
137
+ // Ensure edge type coresponds to config index
138
+ let edgeType = i + 1;
139
+ for (let group of allPossibleMergeCandidates) {
140
+ let candidateA = group[0];
141
+ let candidateB = group[1];
142
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
143
+ continue;
144
+ }
145
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(candidateA.contentKey, candidateA.id);
146
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(candidateB.contentKey, candidateB.id);
147
+
148
+ // Add edge in both directions
149
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
150
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
151
+ candidates.set(bundleNode, edgeType);
152
+ candidates.set(otherBundleNode, edgeType);
153
+ }
154
+
155
+ // Remove bundles that have been allocated to a higher priority merge
156
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(group => !graph.hasContentKey(group[0].contentKey) && !graph.hasContentKey(group[1].contentKey));
157
+ }
158
+ (0, _memoize.clearCaches)();
159
+ return getMergeClusters(graph, candidates);
160
+ }
@@ -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
- let conf = await config.getConfig([], {
124
- packageKey: '@atlaspack/bundler-default'
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 || conf === void 0 ? void 0 : conf.contents) != 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();
@@ -159,17 +161,15 @@ function createIdealGraph(assetGraph, config, entries, logger) {
159
161
  assetGraph.traverse((node, _, actions) => {
160
162
  if (node.type === 'asset' && (!Array.isArray(c.types) || c.types.includes(node.value.type))) {
161
163
  let projectRelativePath = _path().default.relative(config.projectRoot, node.value.filePath);
162
- if (!assetRegexes.some(regex => regex.test(projectRelativePath))) {
163
- return;
164
- }
165
164
 
166
165
  // We track all matching MSB's for constant modules as they are never duplicated
167
166
  // and need to be assigned to all matching bundles
168
167
  if (node.value.meta.isConstantModule === true) {
169
168
  constantModuleToMSB.get(node.value).push(c);
170
169
  }
171
- manualAssetToConfig.set(node.value, c);
172
- return;
170
+ if (assetRegexes.some(regex => regex.test(projectRelativePath))) {
171
+ manualAssetToConfig.set(node.value, c);
172
+ }
173
173
  }
174
174
  if (node.type === 'dependency' && (node.value.priority === 'lazy' || (0, _featureFlags().getFeatureFlag)('conditionalBundlingApi') && node.value.priority === 'conditional') && parentAsset) {
175
175
  // Don't walk past the bundle group assets
@@ -183,6 +183,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
183
183
  };
184
184
  }();
185
185
  let manualBundleToInternalizedAsset = new (_utils().DefaultMap)(() => []);
186
+ let mergeSourceBundleLookup = new Map();
187
+ let mergeSourceBundleAssets = new Set((_config$sharedBundleM = config.sharedBundleMerge) === null || _config$sharedBundleM === void 0 ? void 0 : _config$sharedBundleM.flatMap(c => {
188
+ var _c$sourceBundles;
189
+ return ((_c$sourceBundles = c.sourceBundles) === null || _c$sourceBundles === void 0 ? void 0 : _c$sourceBundles.map(assetMatch => _path().default.join(config.projectRoot, assetMatch))) ?? [];
190
+ }));
186
191
 
187
192
  /**
188
193
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
@@ -249,6 +254,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
249
254
  bundleRoots.set(childAsset, [bundleId, bundleId]);
250
255
  bundleGroupBundleIds.add(bundleId);
251
256
  bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
257
+ // If this asset is relevant for merging then track it's source
258
+ // bundle id for later
259
+ if (mergeSourceBundleAssets.has(childAsset.filePath)) {
260
+ mergeSourceBundleLookup.set(_path().default.relative(config.projectRoot, childAsset.filePath), bundleId);
261
+ }
252
262
  if (manualSharedObject) {
253
263
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
254
264
  // since MSBs should not have main entry assets
@@ -848,6 +858,12 @@ function createIdealGraph(assetGraph, config, entries, logger) {
848
858
  }
849
859
  }
850
860
 
861
+ // Step merge shared bundles that meet the overlap threshold
862
+ // This step is skipped by default as the threshold defaults to 1
863
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
864
+ mergeOverlapBundles(config.sharedBundleMerge);
865
+ }
866
+
851
867
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
852
868
  // their source bundles, and remove the bundle.
853
869
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -857,9 +873,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
857
873
  removeBundle(bundleGraph, bundleNodeId, assetReference);
858
874
  }
859
875
  }
860
- let modifiedSourceBundles = new Set();
861
876
 
862
877
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
878
+ let modifiedSourceBundles = new Set();
863
879
  if (config.disableSharedBundles === false) {
864
880
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
865
881
  // Find shared bundles in this bundle group.
@@ -944,6 +960,68 @@ function createIdealGraph(assetGraph, config, entries, logger) {
944
960
  }
945
961
  }
946
962
  }
963
+ function mergeBundles(bundleGraph, bundleToKeepId, bundleToRemoveId, assetReference) {
964
+ let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId));
965
+ let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId));
966
+ (0, _assert().default)(bundleToKeep !== 'root' && bundleToRemove !== 'root');
967
+ for (let asset of bundleToRemove.assets) {
968
+ bundleToKeep.assets.add(asset);
969
+ bundleToKeep.size += asset.stats.size;
970
+ let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
971
+ assetReference.set(asset, newAssetReference);
972
+ }
973
+
974
+ // Merge any internalized assets
975
+ (0, _assert().default)(bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets, 'All shared bundles should have internalized assets');
976
+ bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
977
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
978
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
979
+ continue;
980
+ }
981
+ bundleToKeep.sourceBundles.add(sourceBundleId);
982
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
983
+ }
984
+ bundleGraph.removeNode(bundleToRemoveId);
985
+ }
986
+ function mergeOverlapBundles(mergeConfig) {
987
+ // Find all shared bundles
988
+ let sharedBundles = new Set();
989
+ bundleGraph.traverse(nodeId => {
990
+ let bundle = bundleGraph.getNode(nodeId);
991
+ if (!bundle) {
992
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
993
+ }
994
+ if (bundle === 'root') {
995
+ return;
996
+ }
997
+
998
+ // Only consider JS shared bundles and non-reused bundles.
999
+ // These count potentially be considered for merging in future but they're
1000
+ // more complicated to merge
1001
+ if (bundle.sourceBundles.size > 0 && bundle.manualSharedBundle == null && !bundle.mainEntryAsset && bundle.type === 'js') {
1002
+ sharedBundles.add(nodeId);
1003
+ }
1004
+ });
1005
+ let clusters = (0, _bundleMerge.findMergeCandidates)(bundleGraph, Array.from(sharedBundles), mergeConfig.map(config => {
1006
+ var _config$sourceBundles;
1007
+ return {
1008
+ ...config,
1009
+ sourceBundles: (_config$sourceBundles = config.sourceBundles) === null || _config$sourceBundles === void 0 ? void 0 : _config$sourceBundles.map(assetMatch => {
1010
+ let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
1011
+ if (sourceBundleNodeId == null) {
1012
+ throw new Error(`Source bundle ${assetMatch} not found in merge source bundle lookup`);
1013
+ }
1014
+ return sourceBundleNodeId;
1015
+ })
1016
+ };
1017
+ }));
1018
+ for (let cluster of clusters) {
1019
+ let [mergeTarget, ...rest] = cluster;
1020
+ for (let bundleIdToMerge of rest) {
1021
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1022
+ }
1023
+ }
1024
+ }
947
1025
  function getBigIntFromContentKey(contentKey) {
948
1026
  let b = Buffer.alloc(64);
949
1027
  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.0+56fd82cd5",
3
+ "version": "2.14.5-canary.100+f609bf49f",
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.68+56fd82cd5",
20
- "@atlaspack/feature-flags": "2.14.1-canary.68+56fd82cd5",
21
- "@atlaspack/graph": "3.4.1-canary.68+56fd82cd5",
22
- "@atlaspack/plugin": "2.14.5-canary.0+56fd82cd5",
23
- "@atlaspack/rust": "3.2.1-canary.0+56fd82cd5",
24
- "@atlaspack/utils": "2.14.5-canary.0+56fd82cd5",
19
+ "@atlaspack/diagnostic": "2.14.1-canary.168+f609bf49f",
20
+ "@atlaspack/feature-flags": "2.14.1-canary.168+f609bf49f",
21
+ "@atlaspack/graph": "3.4.1-canary.168+f609bf49f",
22
+ "@atlaspack/plugin": "2.14.5-canary.100+f609bf49f",
23
+ "@atlaspack/rust": "3.2.1-canary.100+f609bf49f",
24
+ "@atlaspack/utils": "2.14.5-canary.100+f609bf49f",
25
+ "many-keys-map": "^1.0.3",
25
26
  "nullthrows": "^1.1.1"
26
27
  },
27
- "gitHead": "56fd82cd544c4c9096be6ebea8261e714435de09"
28
+ "gitHead": "f609bf49ffa3984c0ff81d4853a5c850aaee5fce"
28
29
  }
@@ -0,0 +1,248 @@
1
+ // @flow strict-local
2
+
3
+ import invariant from 'assert';
4
+ import nullthrows from 'nullthrows';
5
+ import {ContentGraph} from '@atlaspack/graph';
6
+ import type {NodeId} from '@atlaspack/graph';
7
+ import {setUnion, setIntersectStatic} from '@atlaspack/utils';
8
+ import type {Bundle, IdealBundleGraph} from './idealGraph';
9
+ import {memoize, clearCaches} from './memoize';
10
+
11
+ function getBundlesForBundleGroup(
12
+ bundleGraph: IdealBundleGraph,
13
+ bundleGroupId: NodeId,
14
+ ): number {
15
+ let count = 0;
16
+ bundleGraph.traverse((nodeId) => {
17
+ if (bundleGraph.getNode(nodeId)?.bundleBehavior !== 'inline') {
18
+ count++;
19
+ }
20
+ }, bundleGroupId);
21
+ return count;
22
+ }
23
+
24
+ let getBundleOverlap = (
25
+ sourceBundlesA: Set<NodeId>,
26
+ sourceBundlesB: Set<NodeId>,
27
+ ): number => {
28
+ let allSourceBundles = setUnion(sourceBundlesA, sourceBundlesB);
29
+ let sharedSourceBundles = setIntersectStatic(sourceBundlesA, sourceBundlesB);
30
+
31
+ return sharedSourceBundles.size / allSourceBundles.size;
32
+ };
33
+
34
+ // Returns a decimal showing the proportion source bundles are common to
35
+ // both bundles versus the total number of source bundles.
36
+ function checkBundleThreshold(
37
+ bundleA: MergeCandidate,
38
+ bundleB: MergeCandidate,
39
+ threshold: number,
40
+ ): boolean {
41
+ return (
42
+ getBundleOverlap(
43
+ bundleA.bundle.sourceBundles,
44
+ bundleB.bundle.sourceBundles,
45
+ ) >= threshold
46
+ );
47
+ }
48
+
49
+ let checkSharedSourceBundles = memoize(
50
+ (bundle: Bundle, importantAncestorBundles: Array<NodeId>): boolean => {
51
+ return importantAncestorBundles.every((ancestorId) =>
52
+ bundle.sourceBundles.has(ancestorId),
53
+ );
54
+ },
55
+ );
56
+
57
+ let hasSuitableBundleGroup = memoize(
58
+ (
59
+ bundleGraph: IdealBundleGraph,
60
+ bundle: Bundle,
61
+ minBundlesInGroup: number,
62
+ ): boolean => {
63
+ for (let sourceBundle of bundle.sourceBundles) {
64
+ let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
65
+
66
+ if (bundlesInGroup >= minBundlesInGroup) {
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ },
72
+ );
73
+
74
+ function validMerge(
75
+ bundleGraph: IdealBundleGraph,
76
+ config: MergeGroup,
77
+ bundleA: MergeCandidate,
78
+ bundleB: MergeCandidate,
79
+ ): boolean {
80
+ if (config.maxBundleSize != null) {
81
+ if (
82
+ bundleA.bundle.size > config.maxBundleSize ||
83
+ bundleB.bundle.size > config.maxBundleSize
84
+ ) {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ if (config.overlapThreshold != null) {
90
+ if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ if (config.sourceBundles != null) {
96
+ if (
97
+ !checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) ||
98
+ !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)
99
+ ) {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ if (config.minBundlesInGroup != null) {
105
+ if (
106
+ !hasSuitableBundleGroup(
107
+ bundleGraph,
108
+ bundleA.bundle,
109
+ config.minBundlesInGroup,
110
+ ) ||
111
+ !hasSuitableBundleGroup(
112
+ bundleGraph,
113
+ bundleB.bundle,
114
+ config.minBundlesInGroup,
115
+ )
116
+ ) {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ return true;
122
+ }
123
+
124
+ function getMergeClusters(
125
+ graph: ContentGraph<NodeId, EdgeType>,
126
+ candidates: Map<NodeId, EdgeType>,
127
+ ): Array<Array<NodeId>> {
128
+ let clusters = [];
129
+
130
+ for (let [candidate, edgeType] of candidates.entries()) {
131
+ let cluster: Array<NodeId> = [];
132
+
133
+ graph.traverse(
134
+ (nodeId) => {
135
+ cluster.push(nullthrows(graph.getNode(nodeId)));
136
+ // Remove node from candidates as it has already been processed
137
+ candidates.delete(nodeId);
138
+ },
139
+ candidate,
140
+ edgeType,
141
+ );
142
+ clusters.push(cluster);
143
+ }
144
+
145
+ return clusters;
146
+ }
147
+
148
+ type MergeCandidate = {|
149
+ bundle: Bundle,
150
+ id: NodeId,
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
+ return {
162
+ id: bundleId,
163
+ bundle,
164
+ contentKey: bundleId.toString(),
165
+ };
166
+ });
167
+
168
+ const uniquePairs = [];
169
+
170
+ for (let i = 0; i < mergeCandidates.length; i++) {
171
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
172
+ let a = mergeCandidates[i];
173
+ let b = mergeCandidates[j];
174
+
175
+ if (
176
+ // $FlowFixMe both bundles will always have internalizedAssets
177
+ a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)
178
+ ) {
179
+ uniquePairs.push([a, b]);
180
+ }
181
+ }
182
+ }
183
+ return uniquePairs;
184
+ }
185
+
186
+ export type MergeGroup = {|
187
+ overlapThreshold?: number,
188
+ maxBundleSize?: number,
189
+ sourceBundles?: Array<NodeId>,
190
+ minBundlesInGroup?: number,
191
+ |};
192
+ type EdgeType = number;
193
+
194
+ export function findMergeCandidates(
195
+ bundleGraph: IdealBundleGraph,
196
+ bundles: Array<NodeId>,
197
+ config: Array<MergeGroup>,
198
+ ): Array<Array<NodeId>> {
199
+ let graph = new ContentGraph<NodeId, EdgeType>();
200
+ let candidates = new Map<NodeId, EdgeType>();
201
+
202
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(
203
+ bundleGraph,
204
+ bundles,
205
+ );
206
+
207
+ // Build graph of clustered merge candidates
208
+ for (let i = 0; i < config.length; i++) {
209
+ // Ensure edge type coresponds to config index
210
+ let edgeType = i + 1;
211
+
212
+ for (let group of allPossibleMergeCandidates) {
213
+ let candidateA = group[0];
214
+ let candidateB = group[1];
215
+
216
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
217
+ continue;
218
+ }
219
+
220
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(
221
+ candidateA.contentKey,
222
+ candidateA.id,
223
+ );
224
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
225
+ candidateB.contentKey,
226
+ candidateB.id,
227
+ );
228
+
229
+ // Add edge in both directions
230
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
231
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
232
+
233
+ candidates.set(bundleNode, edgeType);
234
+ candidates.set(otherBundleNode, edgeType);
235
+ }
236
+
237
+ // Remove bundles that have been allocated to a higher priority merge
238
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(
239
+ (group) =>
240
+ !graph.hasContentKey(group[0].contentKey) &&
241
+ !graph.hasContentKey(group[1].contentKey),
242
+ );
243
+ }
244
+
245
+ clearCaches();
246
+
247
+ return getMergeClusters(graph, candidates);
248
+ }
@@ -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 = await config.getConfig<BundlerConfig>([], {
152
- packageKey: '@atlaspack/bundler-default',
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 {ResolvedBundlerConfig} from './bundlerConfig';
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
  >;
@@ -203,17 +204,16 @@ export function createIdealGraph(
203
204
  config.projectRoot,
204
205
  node.value.filePath,
205
206
  );
206
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
207
- return;
208
- }
209
207
 
210
208
  // We track all matching MSB's for constant modules as they are never duplicated
211
209
  // and need to be assigned to all matching bundles
212
210
  if (node.value.meta.isConstantModule === true) {
213
211
  constantModuleToMSB.get(node.value).push(c);
214
212
  }
215
- manualAssetToConfig.set(node.value, c);
216
- return;
213
+
214
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
215
+ manualAssetToConfig.set(node.value, c);
216
+ }
217
217
  }
218
218
 
219
219
  if (
@@ -244,6 +244,16 @@ export function createIdealGraph(
244
244
  Array<Asset>,
245
245
  > = new DefaultMap(() => []);
246
246
 
247
+ let mergeSourceBundleLookup = new Map<string, NodeId>();
248
+ let mergeSourceBundleAssets = new Set(
249
+ config.sharedBundleMerge?.flatMap(
250
+ (c) =>
251
+ c.sourceBundles?.map((assetMatch) =>
252
+ path.join(config.projectRoot, assetMatch),
253
+ ) ?? [],
254
+ ),
255
+ );
256
+
247
257
  /**
248
258
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
249
259
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -334,6 +344,14 @@ export function createIdealGraph(
334
344
  bundleRoots.set(childAsset, [bundleId, bundleId]);
335
345
  bundleGroupBundleIds.add(bundleId);
336
346
  bundleGraph.addEdge(bundleGraphRootNodeId, bundleId);
347
+ // If this asset is relevant for merging then track it's source
348
+ // bundle id for later
349
+ if (mergeSourceBundleAssets.has(childAsset.filePath)) {
350
+ mergeSourceBundleLookup.set(
351
+ path.relative(config.projectRoot, childAsset.filePath),
352
+ bundleId,
353
+ );
354
+ }
337
355
  if (manualSharedObject) {
338
356
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
339
357
  // since MSBs should not have main entry assets
@@ -1128,6 +1146,12 @@ export function createIdealGraph(
1128
1146
  }
1129
1147
  }
1130
1148
 
1149
+ // Step merge shared bundles that meet the overlap threshold
1150
+ // This step is skipped by default as the threshold defaults to 1
1151
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
1152
+ mergeOverlapBundles(config.sharedBundleMerge);
1153
+ }
1154
+
1131
1155
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
1132
1156
  // their source bundles, and remove the bundle.
1133
1157
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -1143,9 +1167,9 @@ export function createIdealGraph(
1143
1167
  }
1144
1168
  }
1145
1169
 
1170
+ // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1146
1171
  let modifiedSourceBundles = new Set();
1147
1172
 
1148
- // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1149
1173
  if (config.disableSharedBundles === false) {
1150
1174
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
1151
1175
  // Find shared bundles in this bundle group.
@@ -1249,6 +1273,102 @@ export function createIdealGraph(
1249
1273
  }
1250
1274
  }
1251
1275
 
1276
+ function mergeBundles(
1277
+ bundleGraph: IdealBundleGraph,
1278
+ bundleToKeepId: NodeId,
1279
+ bundleToRemoveId: NodeId,
1280
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1281
+ ) {
1282
+ let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
1283
+ let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
1284
+ invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1285
+ for (let asset of bundleToRemove.assets) {
1286
+ bundleToKeep.assets.add(asset);
1287
+ bundleToKeep.size += asset.stats.size;
1288
+
1289
+ let newAssetReference = assetReference
1290
+ .get(asset)
1291
+ .map(([dep, bundle]) =>
1292
+ bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1293
+ );
1294
+
1295
+ assetReference.set(asset, newAssetReference);
1296
+ }
1297
+
1298
+ // Merge any internalized assets
1299
+ invariant(
1300
+ bundleToKeep.internalizedAssets && bundleToRemove.internalizedAssets,
1301
+ 'All shared bundles should have internalized assets',
1302
+ );
1303
+ bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
1304
+
1305
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
1306
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1307
+ continue;
1308
+ }
1309
+
1310
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1311
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1312
+ }
1313
+
1314
+ bundleGraph.removeNode(bundleToRemoveId);
1315
+ }
1316
+
1317
+ function mergeOverlapBundles(mergeConfig: MergeCandidates) {
1318
+ // Find all shared bundles
1319
+ let sharedBundles = new Set<NodeId>();
1320
+ bundleGraph.traverse((nodeId) => {
1321
+ let bundle = bundleGraph.getNode(nodeId);
1322
+
1323
+ if (!bundle) {
1324
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
1325
+ }
1326
+
1327
+ if (bundle === 'root') {
1328
+ return;
1329
+ }
1330
+
1331
+ // Only consider JS shared bundles and non-reused bundles.
1332
+ // These count potentially be considered for merging in future but they're
1333
+ // more complicated to merge
1334
+ if (
1335
+ bundle.sourceBundles.size > 0 &&
1336
+ bundle.manualSharedBundle == null &&
1337
+ !bundle.mainEntryAsset &&
1338
+ bundle.type === 'js'
1339
+ ) {
1340
+ sharedBundles.add(nodeId);
1341
+ }
1342
+ });
1343
+
1344
+ let clusters = findMergeCandidates(
1345
+ bundleGraph,
1346
+ Array.from(sharedBundles),
1347
+ mergeConfig.map((config): MergeGroup => ({
1348
+ ...config,
1349
+ sourceBundles: config.sourceBundles?.map((assetMatch) => {
1350
+ let sourceBundleNodeId = mergeSourceBundleLookup.get(assetMatch);
1351
+
1352
+ if (sourceBundleNodeId == null) {
1353
+ throw new Error(
1354
+ `Source bundle ${assetMatch} not found in merge source bundle lookup`,
1355
+ );
1356
+ }
1357
+
1358
+ return sourceBundleNodeId;
1359
+ }),
1360
+ })),
1361
+ );
1362
+
1363
+ for (let cluster of clusters) {
1364
+ let [mergeTarget, ...rest] = cluster;
1365
+
1366
+ for (let bundleIdToMerge of rest) {
1367
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1368
+ }
1369
+ }
1370
+ }
1371
+
1252
1372
  function getBigIntFromContentKey(contentKey) {
1253
1373
  let b = Buffer.alloc(64);
1254
1374
  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
+ }