@atlaspack/bundler-default 2.14.5-canary.8 → 2.14.5-canary.81

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,227 @@
1
1
  # @atlaspack/bundler-default
2
2
 
3
+ ## 3.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
9
+ - 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)]:
10
+ - @atlaspack/feature-flags@2.18.0
11
+ - @atlaspack/utils@2.15.0
12
+ - @atlaspack/graph@3.5.2
13
+ - @atlaspack/plugin@2.14.12
14
+
15
+ ## 3.0.2
16
+
17
+ ### Patch Changes
18
+
19
+ - [#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.
20
+
21
+ - Updated dependencies [[`73ea3c4`](https://github.com/atlassian-labs/atlaspack/commit/73ea3c4d85d4401fdd15abcbf988237e890e7ad3), [`b1b3693`](https://github.com/atlassian-labs/atlaspack/commit/b1b369317c66f8a431c170df2ebba4fa5b2e38ef)]:
22
+ - @atlaspack/feature-flags@2.17.0
23
+ - @atlaspack/graph@3.5.1
24
+ - @atlaspack/utils@2.14.11
25
+ - @atlaspack/plugin@2.14.11
26
+
27
+ ## 3.0.1
28
+
29
+ ### Patch Changes
30
+
31
+ - [#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
32
+
33
+ ## 3.0.0
34
+
35
+ ### Major Changes
36
+
37
+ - [#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
38
+
39
+ This new config replaces the previously released `sharedBundleMergeThreshold`.
40
+
41
+ The following options are available for each merge group.
42
+
43
+ ### Options
44
+
45
+ #### overlapThreshold
46
+
47
+ > The same as `sharedBundleMergeThreshold` from #535
48
+
49
+ Merge bundles share a percentage of source bundles
50
+
51
+ ```json
52
+ "@atlaspack/bundler-default": {
53
+ "sharedBundleMerge": [{
54
+ "overlapThreshold": 0.75
55
+ }]
56
+ }
57
+ ```
58
+
59
+ #### maxBundleSize
60
+
61
+ Merge bundles that are smaller than a configured amount of bytes.
62
+
63
+ > Keep in mind these bytes are pre-optimisation
64
+
65
+ ```json
66
+ "@atlaspack/bundler-default": {
67
+ "sharedBundleMerge": [{
68
+ "maxBundleSize": 20000
69
+ }]
70
+ }
71
+ ```
72
+
73
+ #### sourceBundles
74
+
75
+ Merge bundles that share a set of source bundles. The matching is relative to the project root, like how manual shared bundle roots work.
76
+
77
+ ```json
78
+ "@atlaspack/bundler-default": {
79
+ "sharedBundleMerge": [{
80
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
81
+ }]
82
+ }
83
+ ```
84
+
85
+ #### minBundlesInGroup
86
+
87
+ 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.
88
+
89
+ ```json
90
+ "@atlaspack/bundler-default": {
91
+ "maxParallelRequests": 30,
92
+ "sharedBundleMerge": [{
93
+ "minBundlesInGroup": 30
94
+ }]
95
+ }
96
+ ```
97
+
98
+ ## Combining options
99
+
100
+ When multiple options are provided, all must be true for a merge to be relevant.
101
+
102
+ For example, merge bundles that are smaller than 20kb and share at least 50% of the same source bundles.
103
+
104
+ ```json
105
+ "@atlaspack/bundler-default": {
106
+ "sharedBundleMerge": [{
107
+ "overlapThreshold": 0.5,
108
+ "maxBundleSize": 20000
109
+ }]
110
+ }
111
+ ```
112
+
113
+ ## Multiple merges
114
+
115
+ You can also have multiple merge configs.
116
+
117
+ ```json
118
+ "@atlaspack/bundler-default": {
119
+ "sharedBundleMerge": [
120
+ {
121
+ "overlapThreshold": 0.75,
122
+ "maxBundleSize": 20000
123
+ },
124
+ {
125
+ "minBundlesInGroup": 30
126
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
127
+ }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ### Patch Changes
133
+
134
+ - Updated dependencies [[`1b52b99`](https://github.com/atlassian-labs/atlaspack/commit/1b52b99db4298b04c1a6eb0f97994d75a2d436f9)]:
135
+ - @atlaspack/graph@3.5.0
136
+
137
+ ## 2.16.3
138
+
139
+ ### Patch Changes
140
+
141
+ - Updated dependencies [[`35fdd4b`](https://github.com/atlassian-labs/atlaspack/commit/35fdd4b52da0af20f74667f7b8adfb2f90279b7c), [`6dd4ccb`](https://github.com/atlassian-labs/atlaspack/commit/6dd4ccb753541de32322d881f973d571dd57e4ca)]:
142
+ - @atlaspack/rust@3.3.5
143
+ - @atlaspack/plugin@2.14.10
144
+ - @atlaspack/utils@2.14.10
145
+
146
+ ## 2.16.2
147
+
148
+ ### Patch Changes
149
+
150
+ - 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)]:
151
+ - @atlaspack/rust@3.3.4
152
+ - @atlaspack/feature-flags@2.16.0
153
+ - @atlaspack/utils@2.14.9
154
+ - @atlaspack/graph@3.4.7
155
+ - @atlaspack/plugin@2.14.9
156
+
157
+ ## 2.16.1
158
+
159
+ ### Patch Changes
160
+
161
+ - 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)]:
162
+ - @atlaspack/feature-flags@2.15.1
163
+ - @atlaspack/rust@3.3.3
164
+ - @atlaspack/graph@3.4.6
165
+ - @atlaspack/utils@2.14.8
166
+ - @atlaspack/plugin@2.14.8
167
+
168
+ ## 2.16.0
169
+
170
+ ### Minor Changes
171
+
172
+ - [#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.
173
+
174
+ ### Patch Changes
175
+
176
+ - Updated dependencies [[`a1773d2`](https://github.com/atlassian-labs/atlaspack/commit/a1773d2a62d0ef7805ac7524621dcabcc1afe929), [`556d6ab`](https://github.com/atlassian-labs/atlaspack/commit/556d6ab8ede759fa7f37fcd3f4da336ef1c55e8f)]:
177
+ - @atlaspack/feature-flags@2.15.0
178
+ - @atlaspack/rust@3.3.2
179
+ - @atlaspack/graph@3.4.5
180
+ - @atlaspack/utils@2.14.7
181
+ - @atlaspack/plugin@2.14.7
182
+
183
+ ## 2.15.1
184
+
185
+ ### Patch Changes
186
+
187
+ - Updated dependencies [[`e0f5337`](https://github.com/atlassian-labs/atlaspack/commit/e0f533757bd1019dbd108a04952c87da15286e09)]:
188
+ - @atlaspack/feature-flags@2.14.4
189
+ - @atlaspack/rust@3.3.1
190
+ - @atlaspack/graph@3.4.4
191
+ - @atlaspack/utils@2.14.6
192
+ - @atlaspack/plugin@2.14.6
193
+
194
+ ## 2.15.0
195
+
196
+ ### Minor Changes
197
+
198
+ - [#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
199
+
200
+ In apps with lots of dynamic imports, many shared bundles are often removed
201
+ from the output to prevent an overload in network requests according to the
202
+ `maxParallelRequests` config. In these cases, setting `sharedBundleMergeThreshold` can
203
+ merge shared bundles with a high overlap in their source bundles (bundles that share the bundle).
204
+ This config trades-off potential overfetching to reduce asset duplication.
205
+
206
+ The following config would merge shared bundles that have a 75% or higher overlap in source bundles.
207
+
208
+ ```json
209
+ {
210
+ "@atlaspack/bundler-default": {
211
+ "sharedBundleMergeThreshold": 0.75
212
+ }
213
+ }
214
+ ```
215
+
216
+ ### Patch Changes
217
+
218
+ - 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)]:
219
+ - @atlaspack/feature-flags@2.14.3
220
+ - @atlaspack/rust@3.3.0
221
+ - @atlaspack/graph@3.4.3
222
+ - @atlaspack/utils@2.14.5
223
+ - @atlaspack/plugin@2.14.5
224
+
3
225
  ## 2.14.4
4
226
 
5
227
  ### Patch Changes
@@ -25,67 +25,136 @@ function _graph() {
25
25
  };
26
26
  return data;
27
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");
28
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
+
29
53
  // Returns a decimal showing the proportion source bundles are common to
30
54
  // both bundles versus the total number of source bundles.
31
- function scoreBundleMerge(bundleA, bundleB) {
32
- let sharedSourceBundles = 0;
33
- let allSourceBundles = new Set([...bundleA.sourceBundles, ...bundleB.sourceBundles]);
34
- for (let bundle of bundleB.sourceBundles) {
35
- if (bundleA.sourceBundles.has(bundle)) {
36
- sharedSourceBundles++;
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;
37
74
  }
38
75
  }
39
- return sharedSourceBundles / allSourceBundles.size;
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;
40
92
  }
41
93
  function getMergeClusters(graph, candidates) {
42
94
  let clusters = [];
43
- for (let candidate of candidates) {
95
+ for (let [candidate, edgeType] of candidates.entries()) {
44
96
  let cluster = [];
45
97
  graph.traverse(nodeId => {
46
98
  cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
47
99
  // Remove node from candidates as it has already been processed
48
100
  candidates.delete(nodeId);
49
- }, candidate);
101
+ }, candidate, edgeType);
50
102
  clusters.push(cluster);
51
103
  }
52
104
  return clusters;
53
105
  }
54
- function findMergeCandidates(bundleGraph, bundles, threshold) {
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) {
55
131
  let graph = new (_graph().ContentGraph)();
56
- let seen = new Set();
57
- let candidates = new Set();
132
+ let candidates = new Map();
133
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(bundleGraph, bundles);
58
134
 
59
135
  // Build graph of clustered merge candidates
60
- for (let bundleId of bundles) {
61
- let bundle = bundleGraph.getNode(bundleId);
62
- (0, _assert().default)(bundle && bundle !== 'root');
63
- if (bundle.type !== 'js') {
64
- continue;
65
- }
66
- for (let otherBundleId of bundles) {
67
- if (bundleId === otherBundleId) {
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)) {
68
143
  continue;
69
144
  }
70
- let key = [bundleId, otherBundleId].sort().join(':');
71
- if (seen.has(key)) {
72
- continue;
73
- }
74
- seen.add(key);
75
- let otherBundle = bundleGraph.getNode(otherBundleId);
76
- (0, _assert().default)(otherBundle && otherBundle !== 'root');
77
- let score = scoreBundleMerge(bundle, otherBundle);
78
- if (score >= threshold) {
79
- let bundleNode = graph.addNodeByContentKeyIfNeeded(bundleId.toString(), bundleId);
80
- let otherBundleNode = graph.addNodeByContentKeyIfNeeded(otherBundleId.toString(), otherBundleId);
145
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(candidateA.contentKey, candidateA.id);
146
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(candidateB.contentKey, candidateB.id);
81
147
 
82
- // Add edge in both directions
83
- graph.addEdge(bundleNode, otherBundleNode);
84
- graph.addEdge(otherBundleNode, bundleNode);
85
- candidates.add(bundleNode);
86
- candidates.add(otherBundleNode);
87
- }
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);
88
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));
89
157
  }
158
+ (0, _memoize.clearCaches)();
90
159
  return getMergeClusters(graph, candidates);
91
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 () {
@@ -54,7 +61,7 @@ const HTTP_OPTIONS = {
54
61
  minBundleSize: 30000,
55
62
  maxParallelRequests: 6,
56
63
  disableSharedBundles: false,
57
- sharedBundleMergeThreshold: 1
64
+ sharedBundleMerge: []
58
65
  },
59
66
  '2': {
60
67
  minBundles: 1,
@@ -62,7 +69,7 @@ const HTTP_OPTIONS = {
62
69
  minBundleSize: 20000,
63
70
  maxParallelRequests: 25,
64
71
  disableSharedBundles: false,
65
- sharedBundleMergeThreshold: 1
72
+ sharedBundleMerge: []
66
73
  }
67
74
  };
68
75
  const CONFIG_SCHEMA = {
@@ -103,6 +110,30 @@ const CONFIG_SCHEMA = {
103
110
  additionalProperties: false
104
111
  }
105
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
+ },
106
137
  minBundles: {
107
138
  type: 'number'
108
139
  },
@@ -125,9 +156,17 @@ const CONFIG_SCHEMA = {
125
156
  additionalProperties: false
126
157
  };
127
158
  async function loadBundlerConfig(config, options, logger) {
128
- let conf = await config.getConfig([], {
129
- packageKey: '@atlaspack/bundler-default'
130
- });
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
+ }
131
170
  if (!conf) {
132
171
  const modDefault = {
133
172
  ...HTTP_OPTIONS['2'],
@@ -135,7 +174,7 @@ async function loadBundlerConfig(config, options, logger) {
135
174
  };
136
175
  return modDefault;
137
176
  }
138
- (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);
139
178
  let modeConfig = resolveModeConfig(conf.contents, options.mode);
140
179
 
141
180
  // minBundles will be ignored if shared bundles are disabled
@@ -177,7 +216,7 @@ async function loadBundlerConfig(config, options, logger) {
177
216
  return {
178
217
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
179
218
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
180
- sharedBundleMergeThreshold: modeConfig.sharedBundleMergeThreshold ?? defaults.sharedBundleMergeThreshold,
219
+ sharedBundleMerge: modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
181
220
  maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
182
221
  projectRoot: options.projectRoot,
183
222
  disableSharedBundles: modeConfig.disableSharedBundles ?? defaults.disableSharedBundles,
package/lib/idealGraph.js CHANGED
@@ -66,6 +66,7 @@ const idealBundleGraphEdges = exports.idealBundleGraphEdges = Object.freeze({
66
66
  // expect from default bundler
67
67
 
68
68
  function createIdealGraph(assetGraph, config, entries, logger) {
69
+ var _config$sharedBundleM;
69
70
  // Asset to the bundle and group it's an entry of
70
71
  let bundleRoots = new Map();
71
72
  let bundles = new Map();
@@ -160,17 +161,15 @@ function createIdealGraph(assetGraph, config, entries, logger) {
160
161
  assetGraph.traverse((node, _, actions) => {
161
162
  if (node.type === 'asset' && (!Array.isArray(c.types) || c.types.includes(node.value.type))) {
162
163
  let projectRelativePath = _path().default.relative(config.projectRoot, node.value.filePath);
163
- if (!assetRegexes.some(regex => regex.test(projectRelativePath))) {
164
- return;
165
- }
166
164
 
167
165
  // We track all matching MSB's for constant modules as they are never duplicated
168
166
  // and need to be assigned to all matching bundles
169
167
  if (node.value.meta.isConstantModule === true) {
170
168
  constantModuleToMSB.get(node.value).push(c);
171
169
  }
172
- manualAssetToConfig.set(node.value, c);
173
- return;
170
+ if (assetRegexes.some(regex => regex.test(projectRelativePath))) {
171
+ manualAssetToConfig.set(node.value, c);
172
+ }
174
173
  }
175
174
  if (node.type === 'dependency' && (node.value.priority === 'lazy' || (0, _featureFlags().getFeatureFlag)('conditionalBundlingApi') && node.value.priority === 'conditional') && parentAsset) {
176
175
  // Don't walk past the bundle group assets
@@ -184,6 +183,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
184
183
  };
185
184
  }();
186
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
+ }));
187
191
 
188
192
  /**
189
193
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
@@ -250,6 +254,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
250
254
  bundleRoots.set(childAsset, [bundleId, bundleId]);
251
255
  bundleGroupBundleIds.add(bundleId);
252
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
+ }
253
262
  if (manualSharedObject) {
254
263
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
255
264
  // since MSBs should not have main entry assets
@@ -851,8 +860,8 @@ function createIdealGraph(assetGraph, config, entries, logger) {
851
860
 
852
861
  // Step merge shared bundles that meet the overlap threshold
853
862
  // This step is skipped by default as the threshold defaults to 1
854
- if (config.sharedBundleMergeThreshold < 1) {
855
- mergeOverlapBundles();
863
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
864
+ mergeOverlapBundles(config.sharedBundleMerge);
856
865
  }
857
866
 
858
867
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -952,8 +961,8 @@ function createIdealGraph(assetGraph, config, entries, logger) {
952
961
  }
953
962
  }
954
963
  function mergeBundles(bundleGraph, bundleToKeepId, bundleToRemoveId, assetReference) {
955
- let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId), '18');
956
- let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId), '19');
964
+ let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId));
965
+ let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId));
957
966
  (0, _assert().default)(bundleToKeep !== 'root' && bundleToRemove !== 'root');
958
967
  for (let asset of bundleToRemove.assets) {
959
968
  bundleToKeep.assets.add(asset);
@@ -961,6 +970,10 @@ function createIdealGraph(assetGraph, config, entries, logger) {
961
970
  let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
962
971
  assetReference.set(asset, newAssetReference);
963
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);
964
977
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
965
978
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
966
979
  continue;
@@ -968,18 +981,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
968
981
  bundleToKeep.sourceBundles.add(sourceBundleId);
969
982
  bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
970
983
  }
971
-
972
- // Merge any internalized assets
973
- if (bundleToRemove.internalizedAssets) {
974
- if (bundleToKeep.internalizedAssets) {
975
- bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
976
- } else {
977
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
978
- }
979
- }
980
984
  bundleGraph.removeNode(bundleToRemoveId);
981
985
  }
982
- function mergeOverlapBundles() {
986
+ function mergeOverlapBundles(mergeConfig) {
983
987
  // Find all shared bundles
984
988
  let sharedBundles = new Set();
985
989
  bundleGraph.traverse(nodeId => {
@@ -998,7 +1002,19 @@ function createIdealGraph(assetGraph, config, entries, logger) {
998
1002
  sharedBundles.add(nodeId);
999
1003
  }
1000
1004
  });
1001
- let clusters = (0, _bundleMerge.findMergeCandidates)(bundleGraph, Array.from(sharedBundles), config.sharedBundleMergeThreshold);
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
+ }));
1002
1018
  for (let cluster of clusters) {
1003
1019
  let [mergeTarget, ...rest] = cluster;
1004
1020
  for (let bundleIdToMerge of rest) {
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.8+46a90dccd",
3
+ "version": "2.14.5-canary.81+bb6785315",
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.76+46a90dccd",
20
- "@atlaspack/feature-flags": "2.14.1-canary.76+46a90dccd",
21
- "@atlaspack/graph": "3.4.1-canary.76+46a90dccd",
22
- "@atlaspack/plugin": "2.14.5-canary.8+46a90dccd",
23
- "@atlaspack/rust": "3.2.1-canary.8+46a90dccd",
24
- "@atlaspack/utils": "2.14.5-canary.8+46a90dccd",
19
+ "@atlaspack/diagnostic": "2.14.1-canary.149+bb6785315",
20
+ "@atlaspack/feature-flags": "2.14.1-canary.149+bb6785315",
21
+ "@atlaspack/graph": "3.4.1-canary.149+bb6785315",
22
+ "@atlaspack/plugin": "2.14.5-canary.81+bb6785315",
23
+ "@atlaspack/rust": "3.2.1-canary.81+bb6785315",
24
+ "@atlaspack/utils": "2.14.5-canary.81+bb6785315",
25
+ "many-keys-map": "^1.0.3",
25
26
  "nullthrows": "^1.1.1"
26
27
  },
27
- "gitHead": "46a90dccd019a26b222c878a92d23acc75dc67c5"
28
+ "gitHead": "bb6785315eea7df56b621d9e81cc106836c30d75"
28
29
  }
@@ -2,102 +2,247 @@
2
2
 
3
3
  import invariant from 'assert';
4
4
  import nullthrows from 'nullthrows';
5
+ import {ContentGraph} from '@atlaspack/graph';
5
6
  import type {NodeId} from '@atlaspack/graph';
7
+ import {setUnion, setIntersectStatic} from '@atlaspack/utils';
6
8
  import type {Bundle, IdealBundleGraph} from './idealGraph';
7
- import {ContentGraph} from '@atlaspack/graph';
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
+ };
8
33
 
9
34
  // Returns a decimal showing the proportion source bundles are common to
10
35
  // both bundles versus the total number of source bundles.
11
- function scoreBundleMerge(bundleA: Bundle, bundleB: Bundle): number {
12
- let sharedSourceBundles = 0;
13
- let allSourceBundles = new Set([
14
- ...bundleA.sourceBundles,
15
- ...bundleB.sourceBundles,
16
- ]);
17
-
18
- for (let bundle of bundleB.sourceBundles) {
19
- if (bundleA.sourceBundles.has(bundle)) {
20
- sharedSourceBundles++;
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;
21
118
  }
22
119
  }
23
120
 
24
- return sharedSourceBundles / allSourceBundles.size;
121
+ return true;
25
122
  }
26
123
 
27
124
  function getMergeClusters(
28
- graph: ContentGraph<NodeId>,
29
- candidates: Set<NodeId>,
125
+ graph: ContentGraph<NodeId, EdgeType>,
126
+ candidates: Map<NodeId, EdgeType>,
30
127
  ): Array<Array<NodeId>> {
31
128
  let clusters = [];
32
129
 
33
- for (let candidate of candidates) {
130
+ for (let [candidate, edgeType] of candidates.entries()) {
34
131
  let cluster: Array<NodeId> = [];
35
132
 
36
- graph.traverse((nodeId) => {
37
- cluster.push(nullthrows(graph.getNode(nodeId)));
38
- // Remove node from candidates as it has already been processed
39
- candidates.delete(nodeId);
40
- }, candidate);
41
-
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
+ );
42
142
  clusters.push(cluster);
43
143
  }
44
144
 
45
145
  return clusters;
46
146
  }
47
147
 
48
- export function findMergeCandidates(
148
+ type MergeCandidate = {|
149
+ bundle: Bundle,
150
+ id: NodeId,
151
+ contentKey: string,
152
+ |};
153
+ function getPossibleMergeCandidates(
49
154
  bundleGraph: IdealBundleGraph,
50
155
  bundles: Array<NodeId>,
51
- threshold: number,
52
- ): Array<Array<NodeId>> {
53
- let graph = new ContentGraph<NodeId>();
54
- let seen = new Set<string>();
55
- let candidates = new Set<NodeId>();
56
-
57
- // Build graph of clustered merge candidates
58
- for (let bundleId of bundles) {
156
+ ): Array<[MergeCandidate, MergeCandidate]> {
157
+ let mergeCandidates = bundles.map((bundleId) => {
59
158
  let bundle = bundleGraph.getNode(bundleId);
60
- invariant(bundle && bundle !== 'root');
61
- if (bundle.type !== 'js') {
62
- continue;
63
- }
64
-
65
- for (let otherBundleId of bundles) {
66
- if (bundleId === otherBundleId) {
67
- continue;
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]);
68
180
  }
181
+ }
182
+ }
183
+ return uniquePairs;
184
+ }
69
185
 
70
- let key = [bundleId, otherBundleId].sort().join(':');
71
-
72
- if (seen.has(key)) {
73
- continue;
74
- }
75
- seen.add(key);
186
+ export type MergeGroup = {|
187
+ overlapThreshold?: number,
188
+ maxBundleSize?: number,
189
+ sourceBundles?: Array<NodeId>,
190
+ minBundlesInGroup?: number,
191
+ |};
192
+ type EdgeType = number;
76
193
 
77
- let otherBundle = bundleGraph.getNode(otherBundleId);
78
- invariant(otherBundle && otherBundle !== 'root');
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>();
79
201
 
80
- let score = scoreBundleMerge(bundle, otherBundle);
202
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(
203
+ bundleGraph,
204
+ bundles,
205
+ );
81
206
 
82
- if (score >= threshold) {
83
- let bundleNode = graph.addNodeByContentKeyIfNeeded(
84
- bundleId.toString(),
85
- bundleId,
86
- );
87
- let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
88
- otherBundleId.toString(),
89
- otherBundleId,
90
- );
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;
91
211
 
92
- // Add edge in both directions
93
- graph.addEdge(bundleNode, otherBundleNode);
94
- graph.addEdge(otherBundleNode, bundleNode);
212
+ for (let group of allPossibleMergeCandidates) {
213
+ let candidateA = group[0];
214
+ let candidateB = group[1];
95
215
 
96
- candidates.add(bundleNode);
97
- candidates.add(otherBundleNode);
216
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
217
+ continue;
98
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);
99
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
+ );
100
243
  }
101
244
 
245
+ clearCaches();
246
+
102
247
  return getMergeClusters(graph, candidates);
103
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,7 +36,7 @@ type BaseBundlerConfig = {|
28
36
  disableSharedBundles?: boolean,
29
37
  manualSharedBundles?: ManualSharedBundles,
30
38
  loadConditionalBundlesInParallel?: boolean,
31
- sharedBundleMergeThreshold?: number,
39
+ sharedBundleMerge?: MergeCandidates,
32
40
  |};
33
41
 
34
42
  type BundlerConfig = {|
@@ -43,7 +51,7 @@ export type ResolvedBundlerConfig = {|
43
51
  disableSharedBundles: boolean,
44
52
  manualSharedBundles: ManualSharedBundles,
45
53
  loadConditionalBundlesInParallel?: boolean,
46
- sharedBundleMergeThreshold: number,
54
+ sharedBundleMerge?: MergeCandidates,
47
55
  |};
48
56
 
49
57
  function resolveModeConfig(
@@ -78,7 +86,7 @@ const HTTP_OPTIONS = {
78
86
  minBundleSize: 30000,
79
87
  maxParallelRequests: 6,
80
88
  disableSharedBundles: false,
81
- sharedBundleMergeThreshold: 1,
89
+ sharedBundleMerge: [],
82
90
  },
83
91
  '2': {
84
92
  minBundles: 1,
@@ -86,7 +94,7 @@ const HTTP_OPTIONS = {
86
94
  minBundleSize: 20000,
87
95
  maxParallelRequests: 25,
88
96
  disableSharedBundles: false,
89
- sharedBundleMergeThreshold: 1,
97
+ sharedBundleMerge: [],
90
98
  },
91
99
  };
92
100
 
@@ -128,6 +136,30 @@ const CONFIG_SCHEMA: SchemaEntity = {
128
136
  additionalProperties: false,
129
137
  },
130
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
+ },
131
163
  minBundles: {
132
164
  type: 'number',
133
165
  },
@@ -155,9 +187,17 @@ export async function loadBundlerConfig(
155
187
  options: PluginOptions,
156
188
  logger: PluginLogger,
157
189
  ): Promise<ResolvedBundlerConfig> {
158
- let conf = await config.getConfig<BundlerConfig>([], {
159
- packageKey: '@atlaspack/bundler-default',
160
- });
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
+ }
161
201
 
162
202
  if (!conf) {
163
203
  const modDefault = {
@@ -231,9 +271,8 @@ export async function loadBundlerConfig(
231
271
  return {
232
272
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
233
273
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
234
- sharedBundleMergeThreshold:
235
- modeConfig.sharedBundleMergeThreshold ??
236
- defaults.sharedBundleMergeThreshold,
274
+ sharedBundleMerge:
275
+ modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
237
276
  maxParallelRequests:
238
277
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
239
278
  projectRoot: options.projectRoot,
package/src/idealGraph.js CHANGED
@@ -23,8 +23,8 @@ import {DefaultMap, globToRegex} from '@atlaspack/utils';
23
23
  import invariant from 'assert';
24
24
  import nullthrows from 'nullthrows';
25
25
 
26
- import {findMergeCandidates} from './bundleMerge';
27
- import type {ResolvedBundlerConfig} from './bundlerConfig';
26
+ import {findMergeCandidates, type MergeGroup} from './bundleMerge';
27
+ import type {ResolvedBundlerConfig, MergeCandidates} from './bundlerConfig';
28
28
 
29
29
  /* BundleRoot - An asset that is the main entry of a Bundle. */
30
30
  type BundleRoot = Asset;
@@ -204,17 +204,16 @@ export function createIdealGraph(
204
204
  config.projectRoot,
205
205
  node.value.filePath,
206
206
  );
207
- if (!assetRegexes.some((regex) => regex.test(projectRelativePath))) {
208
- return;
209
- }
210
207
 
211
208
  // We track all matching MSB's for constant modules as they are never duplicated
212
209
  // and need to be assigned to all matching bundles
213
210
  if (node.value.meta.isConstantModule === true) {
214
211
  constantModuleToMSB.get(node.value).push(c);
215
212
  }
216
- manualAssetToConfig.set(node.value, c);
217
- return;
213
+
214
+ if (assetRegexes.some((regex) => regex.test(projectRelativePath))) {
215
+ manualAssetToConfig.set(node.value, c);
216
+ }
218
217
  }
219
218
 
220
219
  if (
@@ -245,6 +244,16 @@ export function createIdealGraph(
245
244
  Array<Asset>,
246
245
  > = new DefaultMap(() => []);
247
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
+
248
257
  /**
249
258
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
250
259
  * for asset type changes, parallel, inline, and async or lazy dependencies,
@@ -335,6 +344,14 @@ export function createIdealGraph(
335
344
  bundleRoots.set(childAsset, [bundleId, bundleId]);
336
345
  bundleGroupBundleIds.add(bundleId);
337
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
+ }
338
355
  if (manualSharedObject) {
339
356
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
340
357
  // since MSBs should not have main entry assets
@@ -1131,8 +1148,8 @@ export function createIdealGraph(
1131
1148
 
1132
1149
  // Step merge shared bundles that meet the overlap threshold
1133
1150
  // This step is skipped by default as the threshold defaults to 1
1134
- if (config.sharedBundleMergeThreshold < 1) {
1135
- mergeOverlapBundles();
1151
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
1152
+ mergeOverlapBundles(config.sharedBundleMerge);
1136
1153
  }
1137
1154
 
1138
1155
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -1262,11 +1279,8 @@ export function createIdealGraph(
1262
1279
  bundleToRemoveId: NodeId,
1263
1280
  assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1264
1281
  ) {
1265
- let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId), '18');
1266
- let bundleToRemove = nullthrows(
1267
- bundleGraph.getNode(bundleToRemoveId),
1268
- '19',
1269
- );
1282
+ let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
1283
+ let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
1270
1284
  invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1271
1285
  for (let asset of bundleToRemove.assets) {
1272
1286
  bundleToKeep.assets.add(asset);
@@ -1281,6 +1295,13 @@ export function createIdealGraph(
1281
1295
  assetReference.set(asset, newAssetReference);
1282
1296
  }
1283
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
+
1284
1305
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
1285
1306
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1286
1307
  continue;
@@ -1290,21 +1311,10 @@ export function createIdealGraph(
1290
1311
  bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1291
1312
  }
1292
1313
 
1293
- // Merge any internalized assets
1294
- if (bundleToRemove.internalizedAssets) {
1295
- if (bundleToKeep.internalizedAssets) {
1296
- bundleToKeep.internalizedAssets.union(
1297
- bundleToRemove.internalizedAssets,
1298
- );
1299
- } else {
1300
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
1301
- }
1302
- }
1303
-
1304
1314
  bundleGraph.removeNode(bundleToRemoveId);
1305
1315
  }
1306
1316
 
1307
- function mergeOverlapBundles() {
1317
+ function mergeOverlapBundles(mergeConfig: MergeCandidates) {
1308
1318
  // Find all shared bundles
1309
1319
  let sharedBundles = new Set<NodeId>();
1310
1320
  bundleGraph.traverse((nodeId) => {
@@ -1334,7 +1344,20 @@ export function createIdealGraph(
1334
1344
  let clusters = findMergeCandidates(
1335
1345
  bundleGraph,
1336
1346
  Array.from(sharedBundles),
1337
- config.sharedBundleMergeThreshold,
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
+ })),
1338
1361
  );
1339
1362
 
1340
1363
  for (let cluster of clusters) {
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
+ }