@atlaspack/bundler-default 2.14.5-dev.55 → 2.14.5-dev.72

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,136 @@
1
1
  # @atlaspack/bundler-default
2
2
 
3
+ ## 3.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ - Updated dependencies [[`73ea3c4`](https://github.com/atlassian-labs/atlaspack/commit/73ea3c4d85d4401fdd15abcbf988237e890e7ad3), [`b1b3693`](https://github.com/atlassian-labs/atlaspack/commit/b1b369317c66f8a431c170df2ebba4fa5b2e38ef)]:
10
+ - @atlaspack/feature-flags@2.17.0
11
+ - @atlaspack/graph@3.5.1
12
+ - @atlaspack/utils@2.14.11
13
+ - @atlaspack/plugin@2.14.11
14
+
15
+ ## 3.0.1
16
+
17
+ ### Patch Changes
18
+
19
+ - [#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
20
+
21
+ ## 3.0.0
22
+
23
+ ### Major Changes
24
+
25
+ - [#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
26
+
27
+ This new config replaces the previously released `sharedBundleMergeThreshold`.
28
+
29
+ The following options are available for each merge group.
30
+
31
+ ### Options
32
+
33
+ #### overlapThreshold
34
+
35
+ > The same as `sharedBundleMergeThreshold` from #535
36
+
37
+ Merge bundles share a percentage of source bundles
38
+
39
+ ```json
40
+ "@atlaspack/bundler-default": {
41
+ "sharedBundleMerge": [{
42
+ "overlapThreshold": 0.75
43
+ }]
44
+ }
45
+ ```
46
+
47
+ #### maxBundleSize
48
+
49
+ Merge bundles that are smaller than a configured amount of bytes.
50
+
51
+ > Keep in mind these bytes are pre-optimisation
52
+
53
+ ```json
54
+ "@atlaspack/bundler-default": {
55
+ "sharedBundleMerge": [{
56
+ "maxBundleSize": 20000
57
+ }]
58
+ }
59
+ ```
60
+
61
+ #### sourceBundles
62
+
63
+ Merge bundles that share a set of source bundles. The matching is relative to the project root, like how manual shared bundle roots work.
64
+
65
+ ```json
66
+ "@atlaspack/bundler-default": {
67
+ "sharedBundleMerge": [{
68
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
69
+ }]
70
+ }
71
+ ```
72
+
73
+ #### minBundlesInGroup
74
+
75
+ 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.
76
+
77
+ ```json
78
+ "@atlaspack/bundler-default": {
79
+ "maxParallelRequests": 30,
80
+ "sharedBundleMerge": [{
81
+ "minBundlesInGroup": 30
82
+ }]
83
+ }
84
+ ```
85
+
86
+ ## Combining options
87
+
88
+ When multiple options are provided, all must be true for a merge to be relevant.
89
+
90
+ For example, merge bundles that are smaller than 20kb and share at least 50% of the same source bundles.
91
+
92
+ ```json
93
+ "@atlaspack/bundler-default": {
94
+ "sharedBundleMerge": [{
95
+ "overlapThreshold": 0.5,
96
+ "maxBundleSize": 20000
97
+ }]
98
+ }
99
+ ```
100
+
101
+ ## Multiple merges
102
+
103
+ You can also have multiple merge configs.
104
+
105
+ ```json
106
+ "@atlaspack/bundler-default": {
107
+ "sharedBundleMerge": [
108
+ {
109
+ "overlapThreshold": 0.75,
110
+ "maxBundleSize": 20000
111
+ },
112
+ {
113
+ "minBundlesInGroup": 30
114
+ "sourceBundles": ["src/important-route", "src/important-route-2"]
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ ### Patch Changes
121
+
122
+ - Updated dependencies [[`1b52b99`](https://github.com/atlassian-labs/atlaspack/commit/1b52b99db4298b04c1a6eb0f97994d75a2d436f9)]:
123
+ - @atlaspack/graph@3.5.0
124
+
125
+ ## 2.16.3
126
+
127
+ ### Patch Changes
128
+
129
+ - Updated dependencies [[`35fdd4b`](https://github.com/atlassian-labs/atlaspack/commit/35fdd4b52da0af20f74667f7b8adfb2f90279b7c), [`6dd4ccb`](https://github.com/atlassian-labs/atlaspack/commit/6dd4ccb753541de32322d881f973d571dd57e4ca)]:
130
+ - @atlaspack/rust@3.3.5
131
+ - @atlaspack/plugin@2.14.10
132
+ - @atlaspack/utils@2.14.10
133
+
3
134
  ## 2.16.2
4
135
 
5
136
  ### Patch Changes
@@ -25,67 +25,134 @@ function _graph() {
25
25
  };
26
26
  return data;
27
27
  }
28
+ var _memoize = require("./memoize");
28
29
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
30
+ function getBundlesForBundleGroup(bundleGraph, bundleGroupId) {
31
+ let count = 0;
32
+ bundleGraph.traverse(nodeId => {
33
+ var _bundleGraph$getNode;
34
+ if (((_bundleGraph$getNode = bundleGraph.getNode(nodeId)) === null || _bundleGraph$getNode === void 0 ? void 0 : _bundleGraph$getNode.bundleBehavior) !== 'inline') {
35
+ count++;
36
+ }
37
+ }, bundleGroupId);
38
+ return count;
39
+ }
40
+ let getBundleOverlapBitSet = (sourceBundlesA, sourceBundlesB) => {
41
+ let allSourceBundles = _graph().BitSet.union(sourceBundlesA, sourceBundlesB);
42
+ let sharedSourceBundles = _graph().BitSet.intersect(sourceBundlesA, sourceBundlesB);
43
+ return sharedSourceBundles.size() / allSourceBundles.size();
44
+ };
45
+
29
46
  // Returns a decimal showing the proportion source bundles are common to
30
47
  // 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++;
48
+ function checkBundleThreshold(bundleA, bundleB, threshold) {
49
+ return getBundleOverlapBitSet(bundleA.sourceBundleBitSet, bundleB.sourceBundleBitSet) >= threshold;
50
+ }
51
+ let checkSharedSourceBundles = (0, _memoize.memoize)((bundle, importantAncestorBundles) => {
52
+ return importantAncestorBundles.every(ancestorId => bundle.sourceBundles.has(ancestorId));
53
+ });
54
+ let hasSuitableBundleGroup = (0, _memoize.memoize)((bundleGraph, bundle, minBundlesInGroup) => {
55
+ for (let sourceBundle of bundle.sourceBundles) {
56
+ let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
57
+ if (bundlesInGroup >= minBundlesInGroup) {
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ });
63
+ function validMerge(bundleGraph, config, bundleA, bundleB) {
64
+ if (config.maxBundleSize != null) {
65
+ if (bundleA.bundle.size > config.maxBundleSize || bundleB.bundle.size > config.maxBundleSize) {
66
+ return false;
67
+ }
68
+ }
69
+ if (config.overlapThreshold != null) {
70
+ if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
71
+ return false;
72
+ }
73
+ }
74
+ if (config.sourceBundles != null) {
75
+ if (!checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) || !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)) {
76
+ return false;
77
+ }
78
+ }
79
+ if (config.minBundlesInGroup != null) {
80
+ if (!hasSuitableBundleGroup(bundleGraph, bundleA.bundle, config.minBundlesInGroup) || !hasSuitableBundleGroup(bundleGraph, bundleB.bundle, config.minBundlesInGroup)) {
81
+ return false;
37
82
  }
38
83
  }
39
- return sharedSourceBundles / allSourceBundles.size;
84
+ return true;
40
85
  }
41
86
  function getMergeClusters(graph, candidates) {
42
87
  let clusters = [];
43
- for (let candidate of candidates) {
88
+ for (let [candidate, edgeType] of candidates.entries()) {
44
89
  let cluster = [];
45
90
  graph.traverse(nodeId => {
46
91
  cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
47
92
  // Remove node from candidates as it has already been processed
48
93
  candidates.delete(nodeId);
49
- }, candidate);
94
+ }, candidate, edgeType);
50
95
  clusters.push(cluster);
51
96
  }
52
97
  return clusters;
53
98
  }
54
- function findMergeCandidates(bundleGraph, bundles, threshold) {
55
- let graph = new (_graph().ContentGraph)();
56
- let seen = new Set();
57
- let candidates = new Set();
58
-
59
- // Build graph of clustered merge candidates
60
- for (let bundleId of bundles) {
99
+ function getPossibleMergeCandidates(bundleGraph, bundles) {
100
+ let mergeCandidates = bundles.map(bundleId => {
61
101
  let bundle = bundleGraph.getNode(bundleId);
62
- (0, _assert().default)(bundle && bundle !== 'root');
63
- if (bundle.type !== 'js') {
64
- continue;
102
+ (0, _assert().default)(bundle && bundle !== 'root', 'Bundle should exist');
103
+ let sourceBundleBitSet = new (_graph().BitSet)(bundleGraph.nodes.length);
104
+ for (let sourceBundle of bundle.sourceBundles) {
105
+ sourceBundleBitSet.add(sourceBundle);
65
106
  }
66
- for (let otherBundleId of bundles) {
67
- if (bundleId === otherBundleId) {
68
- continue;
107
+ return {
108
+ id: bundleId,
109
+ bundle,
110
+ sourceBundleBitSet,
111
+ contentKey: bundleId.toString()
112
+ };
113
+ });
114
+ const uniquePairs = [];
115
+ for (let i = 0; i < mergeCandidates.length; i++) {
116
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
117
+ let a = mergeCandidates[i];
118
+ let b = mergeCandidates[j];
119
+ if (
120
+ // $FlowFixMe both bundles will always have internalizedAssets
121
+ a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)) {
122
+ uniquePairs.push([a, b]);
69
123
  }
70
- let key = [bundleId, otherBundleId].sort().join(':');
71
- if (seen.has(key)) {
124
+ }
125
+ }
126
+ return uniquePairs;
127
+ }
128
+ function findMergeCandidates(bundleGraph, bundles, config) {
129
+ let graph = new (_graph().ContentGraph)();
130
+ let candidates = new Map();
131
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(bundleGraph, bundles);
132
+
133
+ // Build graph of clustered merge candidates
134
+ for (let i = 0; i < config.length; i++) {
135
+ // Ensure edge type coresponds to config index
136
+ let edgeType = i + 1;
137
+ for (let group of allPossibleMergeCandidates) {
138
+ let candidateA = group[0];
139
+ let candidateB = group[1];
140
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
72
141
  continue;
73
142
  }
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);
143
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(candidateA.contentKey, candidateA.id);
144
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(candidateB.contentKey, candidateB.id);
81
145
 
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
- }
146
+ // Add edge in both directions
147
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
148
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
149
+ candidates.set(bundleNode, edgeType);
150
+ candidates.set(otherBundleNode, edgeType);
88
151
  }
152
+
153
+ // Remove bundles that have been allocated to a higher priority merge
154
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(group => !graph.hasContentKey(group[0].contentKey) && !graph.hasContentKey(group[1].contentKey));
89
155
  }
156
+ (0, _memoize.clearCaches)();
90
157
  return getMergeClusters(graph, candidates);
91
158
  }
@@ -61,7 +61,7 @@ const HTTP_OPTIONS = {
61
61
  minBundleSize: 30000,
62
62
  maxParallelRequests: 6,
63
63
  disableSharedBundles: false,
64
- sharedBundleMergeThreshold: 1
64
+ sharedBundleMerge: []
65
65
  },
66
66
  '2': {
67
67
  minBundles: 1,
@@ -69,7 +69,7 @@ const HTTP_OPTIONS = {
69
69
  minBundleSize: 20000,
70
70
  maxParallelRequests: 25,
71
71
  disableSharedBundles: false,
72
- sharedBundleMergeThreshold: 1
72
+ sharedBundleMerge: []
73
73
  }
74
74
  };
75
75
  const CONFIG_SCHEMA = {
@@ -110,6 +110,30 @@ const CONFIG_SCHEMA = {
110
110
  additionalProperties: false
111
111
  }
112
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
+ },
113
137
  minBundles: {
114
138
  type: 'number'
115
139
  },
@@ -192,7 +216,7 @@ async function loadBundlerConfig(config, options, logger) {
192
216
  return {
193
217
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
194
218
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
195
- sharedBundleMergeThreshold: modeConfig.sharedBundleMergeThreshold ?? defaults.sharedBundleMergeThreshold,
219
+ sharedBundleMerge: modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
196
220
  maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
197
221
  projectRoot: options.projectRoot,
198
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();
@@ -108,7 +109,15 @@ function createIdealGraph(assetGraph, config, entries, logger) {
108
109
  }
109
110
  let assets = [];
110
111
  let assetToIndex = new Map();
111
- function makeManualAssetToConfigLookup() {
112
+ //Manual is a map of the user-given name to the bundle node Id that corresponds to ALL the assets that match any glob in that user-specified array
113
+ let manualSharedMap = new Map();
114
+ // May need a map to be able to look up NON- bundle root assets which need special case instructions
115
+ // Use this when placing assets into bundles, to avoid duplication
116
+ let manualAssetToBundle = new Map();
117
+ let {
118
+ manualAssetToConfig,
119
+ constantModuleToMSB
120
+ } = function makeManualAssetToConfigLookup() {
112
121
  let manualAssetToConfig = new Map();
113
122
  let constantModuleToMSB = new (_utils().DefaultMap)(() => []);
114
123
  if (config.manualSharedBundles.length === 0) {
@@ -152,17 +161,15 @@ function createIdealGraph(assetGraph, config, entries, logger) {
152
161
  assetGraph.traverse((node, _, actions) => {
153
162
  if (node.type === 'asset' && (!Array.isArray(c.types) || c.types.includes(node.value.type))) {
154
163
  let projectRelativePath = _path().default.relative(config.projectRoot, node.value.filePath);
155
- if (!assetRegexes.some(regex => regex.test(projectRelativePath))) {
156
- return;
157
- }
158
164
 
159
165
  // We track all matching MSB's for constant modules as they are never duplicated
160
166
  // and need to be assigned to all matching bundles
161
167
  if (node.value.meta.isConstantModule === true) {
162
168
  constantModuleToMSB.get(node.value).push(c);
163
169
  }
164
- manualAssetToConfig.set(node.value, c);
165
- return;
170
+ if (assetRegexes.some(regex => regex.test(projectRelativePath))) {
171
+ manualAssetToConfig.set(node.value, c);
172
+ }
166
173
  }
167
174
  if (node.type === 'dependency' && (node.value.priority === 'lazy' || (0, _featureFlags().getFeatureFlag)('conditionalBundlingApi') && node.value.priority === 'conditional') && parentAsset) {
168
175
  // Don't walk past the bundle group assets
@@ -174,18 +181,13 @@ function createIdealGraph(assetGraph, config, entries, logger) {
174
181
  manualAssetToConfig,
175
182
  constantModuleToMSB
176
183
  };
177
- }
178
-
179
- //Manual is a map of the user-given name to the bundle node Id that corresponds to ALL the assets that match any glob in that user-specified array
180
- let manualSharedMap = new Map();
181
- // May need a map to be able to look up NON- bundle root assets which need special case instructions
182
- // Use this when placing assets into bundles, to avoid duplication
183
- let manualAssetToBundle = new Map();
184
- let {
185
- manualAssetToConfig,
186
- constantModuleToMSB
187
- } = makeManualAssetToConfigLookup();
184
+ }();
188
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
+ }));
189
191
 
190
192
  /**
191
193
  * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles
@@ -252,6 +254,11 @@ function createIdealGraph(assetGraph, config, entries, logger) {
252
254
  bundleRoots.set(childAsset, [bundleId, bundleId]);
253
255
  bundleGroupBundleIds.add(bundleId);
254
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
+ }
255
262
  if (manualSharedObject) {
256
263
  // MSB Step 4: If this was the first instance of a match, mark mainAsset for internalization
257
264
  // since MSBs should not have main entry assets
@@ -853,8 +860,8 @@ function createIdealGraph(assetGraph, config, entries, logger) {
853
860
 
854
861
  // Step merge shared bundles that meet the overlap threshold
855
862
  // This step is skipped by default as the threshold defaults to 1
856
- if (config.sharedBundleMergeThreshold < 1) {
857
- mergeOverlapBundles();
863
+ if (config.sharedBundleMerge && config.sharedBundleMerge.length > 0) {
864
+ mergeOverlapBundles(config.sharedBundleMerge);
858
865
  }
859
866
 
860
867
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
@@ -963,6 +970,10 @@ function createIdealGraph(assetGraph, config, entries, logger) {
963
970
  let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
964
971
  assetReference.set(asset, newAssetReference);
965
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);
966
977
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
967
978
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
968
979
  continue;
@@ -970,18 +981,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
970
981
  bundleToKeep.sourceBundles.add(sourceBundleId);
971
982
  bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
972
983
  }
973
-
974
- // Merge any internalized assets
975
- if (bundleToRemove.internalizedAssets) {
976
- if (bundleToKeep.internalizedAssets) {
977
- bundleToKeep.internalizedAssets.union(bundleToRemove.internalizedAssets);
978
- } else {
979
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
980
- }
981
- }
982
984
  bundleGraph.removeNode(bundleToRemoveId);
983
985
  }
984
- function mergeOverlapBundles() {
986
+ function mergeOverlapBundles(mergeConfig) {
985
987
  // Find all shared bundles
986
988
  let sharedBundles = new Set();
987
989
  bundleGraph.traverse(nodeId => {
@@ -1000,7 +1002,19 @@ function createIdealGraph(assetGraph, config, entries, logger) {
1000
1002
  sharedBundles.add(nodeId);
1001
1003
  }
1002
1004
  });
1003
- 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
+ }));
1004
1018
  for (let cluster of clusters) {
1005
1019
  let [mergeTarget, ...rest] = cluster;
1006
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-dev.55+5a11f33c5",
3
+ "version": "2.14.5-dev.72+9840480de",
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-dev.123+5a11f33c5",
20
- "@atlaspack/feature-flags": "2.14.1-dev.123+5a11f33c5",
21
- "@atlaspack/graph": "3.4.1-dev.123+5a11f33c5",
22
- "@atlaspack/plugin": "2.14.5-dev.55+5a11f33c5",
23
- "@atlaspack/rust": "3.2.1-dev.55+5a11f33c5",
24
- "@atlaspack/utils": "2.14.5-dev.55+5a11f33c5",
19
+ "@atlaspack/diagnostic": "2.14.1-dev.140+9840480de",
20
+ "@atlaspack/feature-flags": "2.14.1-dev.140+9840480de",
21
+ "@atlaspack/graph": "3.4.1-dev.140+9840480de",
22
+ "@atlaspack/plugin": "2.14.5-dev.72+9840480de",
23
+ "@atlaspack/rust": "3.2.1-dev.72+9840480de",
24
+ "@atlaspack/utils": "2.14.5-dev.72+9840480de",
25
+ "many-keys-map": "^1.0.3",
25
26
  "nullthrows": "^1.1.1"
26
27
  },
27
- "gitHead": "5a11f33c51ff74d1cf8d4b72cfa0fda833aa980a"
28
+ "gitHead": "9840480de27511f558d8f5b8cf82220de977b4fb"
28
29
  }
@@ -2,102 +2,253 @@
2
2
 
3
3
  import invariant from 'assert';
4
4
  import nullthrows from 'nullthrows';
5
+ import {ContentGraph, BitSet} from '@atlaspack/graph';
5
6
  import type {NodeId} from '@atlaspack/graph';
6
7
  import type {Bundle, IdealBundleGraph} from './idealGraph';
7
- import {ContentGraph} from '@atlaspack/graph';
8
+ import {memoize, clearCaches} from './memoize';
9
+
10
+ function getBundlesForBundleGroup(
11
+ bundleGraph: IdealBundleGraph,
12
+ bundleGroupId: NodeId,
13
+ ): number {
14
+ let count = 0;
15
+ bundleGraph.traverse((nodeId) => {
16
+ if (bundleGraph.getNode(nodeId)?.bundleBehavior !== 'inline') {
17
+ count++;
18
+ }
19
+ }, bundleGroupId);
20
+ return count;
21
+ }
22
+
23
+ let getBundleOverlapBitSet = (
24
+ sourceBundlesA: BitSet,
25
+ sourceBundlesB: BitSet,
26
+ ): number => {
27
+ let allSourceBundles = BitSet.union(sourceBundlesA, sourceBundlesB);
28
+ let sharedSourceBundles = BitSet.intersect(sourceBundlesA, sourceBundlesB);
29
+
30
+ return sharedSourceBundles.size() / allSourceBundles.size();
31
+ };
8
32
 
9
33
  // Returns a decimal showing the proportion source bundles are common to
10
34
  // 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++;
35
+ function checkBundleThreshold(
36
+ bundleA: MergeCandidate,
37
+ bundleB: MergeCandidate,
38
+ threshold: number,
39
+ ): boolean {
40
+ return (
41
+ getBundleOverlapBitSet(
42
+ bundleA.sourceBundleBitSet,
43
+ bundleB.sourceBundleBitSet,
44
+ ) >= threshold
45
+ );
46
+ }
47
+
48
+ let checkSharedSourceBundles = memoize(
49
+ (bundle: Bundle, importantAncestorBundles: Array<NodeId>): boolean => {
50
+ return importantAncestorBundles.every((ancestorId) =>
51
+ bundle.sourceBundles.has(ancestorId),
52
+ );
53
+ },
54
+ );
55
+
56
+ let hasSuitableBundleGroup = memoize(
57
+ (
58
+ bundleGraph: IdealBundleGraph,
59
+ bundle: Bundle,
60
+ minBundlesInGroup: number,
61
+ ): boolean => {
62
+ for (let sourceBundle of bundle.sourceBundles) {
63
+ let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
64
+
65
+ if (bundlesInGroup >= minBundlesInGroup) {
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ },
71
+ );
72
+
73
+ function validMerge(
74
+ bundleGraph: IdealBundleGraph,
75
+ config: MergeGroup,
76
+ bundleA: MergeCandidate,
77
+ bundleB: MergeCandidate,
78
+ ): boolean {
79
+ if (config.maxBundleSize != null) {
80
+ if (
81
+ bundleA.bundle.size > config.maxBundleSize ||
82
+ bundleB.bundle.size > config.maxBundleSize
83
+ ) {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ if (config.overlapThreshold != null) {
89
+ if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
90
+ return false;
21
91
  }
22
92
  }
23
93
 
24
- return sharedSourceBundles / allSourceBundles.size;
94
+ if (config.sourceBundles != null) {
95
+ if (
96
+ !checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) ||
97
+ !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)
98
+ ) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ if (config.minBundlesInGroup != null) {
104
+ if (
105
+ !hasSuitableBundleGroup(
106
+ bundleGraph,
107
+ bundleA.bundle,
108
+ config.minBundlesInGroup,
109
+ ) ||
110
+ !hasSuitableBundleGroup(
111
+ bundleGraph,
112
+ bundleB.bundle,
113
+ config.minBundlesInGroup,
114
+ )
115
+ ) {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ return true;
25
121
  }
26
122
 
27
123
  function getMergeClusters(
28
- graph: ContentGraph<NodeId>,
29
- candidates: Set<NodeId>,
124
+ graph: ContentGraph<NodeId, EdgeType>,
125
+ candidates: Map<NodeId, EdgeType>,
30
126
  ): Array<Array<NodeId>> {
31
127
  let clusters = [];
32
128
 
33
- for (let candidate of candidates) {
129
+ for (let [candidate, edgeType] of candidates.entries()) {
34
130
  let cluster: Array<NodeId> = [];
35
131
 
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
-
132
+ graph.traverse(
133
+ (nodeId) => {
134
+ cluster.push(nullthrows(graph.getNode(nodeId)));
135
+ // Remove node from candidates as it has already been processed
136
+ candidates.delete(nodeId);
137
+ },
138
+ candidate,
139
+ edgeType,
140
+ );
42
141
  clusters.push(cluster);
43
142
  }
44
143
 
45
144
  return clusters;
46
145
  }
47
146
 
48
- export function findMergeCandidates(
147
+ type MergeCandidate = {|
148
+ bundle: Bundle,
149
+ id: NodeId,
150
+ sourceBundleBitSet: BitSet,
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;
159
+ invariant(bundle && bundle !== 'root', 'Bundle should exist');
160
+
161
+ let sourceBundleBitSet = new BitSet(bundleGraph.nodes.length);
162
+ for (let sourceBundle of bundle.sourceBundles) {
163
+ sourceBundleBitSet.add(sourceBundle);
63
164
  }
64
165
 
65
- for (let otherBundleId of bundles) {
66
- if (bundleId === otherBundleId) {
67
- continue;
166
+ return {
167
+ id: bundleId,
168
+ bundle,
169
+ sourceBundleBitSet,
170
+ contentKey: bundleId.toString(),
171
+ };
172
+ });
173
+
174
+ const uniquePairs = [];
175
+
176
+ for (let i = 0; i < mergeCandidates.length; i++) {
177
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
178
+ let a = mergeCandidates[i];
179
+ let b = mergeCandidates[j];
180
+
181
+ if (
182
+ // $FlowFixMe both bundles will always have internalizedAssets
183
+ a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)
184
+ ) {
185
+ uniquePairs.push([a, b]);
68
186
  }
187
+ }
188
+ }
189
+ return uniquePairs;
190
+ }
69
191
 
70
- let key = [bundleId, otherBundleId].sort().join(':');
71
-
72
- if (seen.has(key)) {
73
- continue;
74
- }
75
- seen.add(key);
192
+ export type MergeGroup = {|
193
+ overlapThreshold?: number,
194
+ maxBundleSize?: number,
195
+ sourceBundles?: Array<NodeId>,
196
+ minBundlesInGroup?: number,
197
+ |};
198
+ type EdgeType = number;
76
199
 
77
- let otherBundle = bundleGraph.getNode(otherBundleId);
78
- invariant(otherBundle && otherBundle !== 'root');
200
+ export function findMergeCandidates(
201
+ bundleGraph: IdealBundleGraph,
202
+ bundles: Array<NodeId>,
203
+ config: Array<MergeGroup>,
204
+ ): Array<Array<NodeId>> {
205
+ let graph = new ContentGraph<NodeId, EdgeType>();
206
+ let candidates = new Map<NodeId, EdgeType>();
79
207
 
80
- let score = scoreBundleMerge(bundle, otherBundle);
208
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(
209
+ bundleGraph,
210
+ bundles,
211
+ );
81
212
 
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
- );
213
+ // Build graph of clustered merge candidates
214
+ for (let i = 0; i < config.length; i++) {
215
+ // Ensure edge type coresponds to config index
216
+ let edgeType = i + 1;
91
217
 
92
- // Add edge in both directions
93
- graph.addEdge(bundleNode, otherBundleNode);
94
- graph.addEdge(otherBundleNode, bundleNode);
218
+ for (let group of allPossibleMergeCandidates) {
219
+ let candidateA = group[0];
220
+ let candidateB = group[1];
95
221
 
96
- candidates.add(bundleNode);
97
- candidates.add(otherBundleNode);
222
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
223
+ continue;
98
224
  }
225
+
226
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(
227
+ candidateA.contentKey,
228
+ candidateA.id,
229
+ );
230
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
231
+ candidateB.contentKey,
232
+ candidateB.id,
233
+ );
234
+
235
+ // Add edge in both directions
236
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
237
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
238
+
239
+ candidates.set(bundleNode, edgeType);
240
+ candidates.set(otherBundleNode, edgeType);
99
241
  }
242
+
243
+ // Remove bundles that have been allocated to a higher priority merge
244
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(
245
+ (group) =>
246
+ !graph.hasContentKey(group[0].contentKey) &&
247
+ !graph.hasContentKey(group[1].contentKey),
248
+ );
100
249
  }
101
250
 
251
+ clearCaches();
252
+
102
253
  return getMergeClusters(graph, candidates);
103
254
  }
@@ -21,6 +21,13 @@ type ManualSharedBundles = Array<{|
21
21
  split?: number,
22
22
  |}>;
23
23
 
24
+ export type MergeCandidates = Array<{|
25
+ overlapThreshold?: number,
26
+ maxBundleSize?: number,
27
+ sourceBundles?: Array<string>,
28
+ minBundlesInGroup?: number,
29
+ |}>;
30
+
24
31
  type BaseBundlerConfig = {|
25
32
  http?: number,
26
33
  minBundles?: number,
@@ -29,7 +36,7 @@ type BaseBundlerConfig = {|
29
36
  disableSharedBundles?: boolean,
30
37
  manualSharedBundles?: ManualSharedBundles,
31
38
  loadConditionalBundlesInParallel?: boolean,
32
- sharedBundleMergeThreshold?: number,
39
+ sharedBundleMerge?: MergeCandidates,
33
40
  |};
34
41
 
35
42
  type BundlerConfig = {|
@@ -44,7 +51,7 @@ export type ResolvedBundlerConfig = {|
44
51
  disableSharedBundles: boolean,
45
52
  manualSharedBundles: ManualSharedBundles,
46
53
  loadConditionalBundlesInParallel?: boolean,
47
- sharedBundleMergeThreshold: number,
54
+ sharedBundleMerge?: MergeCandidates,
48
55
  |};
49
56
 
50
57
  function resolveModeConfig(
@@ -79,7 +86,7 @@ const HTTP_OPTIONS = {
79
86
  minBundleSize: 30000,
80
87
  maxParallelRequests: 6,
81
88
  disableSharedBundles: false,
82
- sharedBundleMergeThreshold: 1,
89
+ sharedBundleMerge: [],
83
90
  },
84
91
  '2': {
85
92
  minBundles: 1,
@@ -87,7 +94,7 @@ const HTTP_OPTIONS = {
87
94
  minBundleSize: 20000,
88
95
  maxParallelRequests: 25,
89
96
  disableSharedBundles: false,
90
- sharedBundleMergeThreshold: 1,
97
+ sharedBundleMerge: [],
91
98
  },
92
99
  };
93
100
 
@@ -129,6 +136,30 @@ const CONFIG_SCHEMA: SchemaEntity = {
129
136
  additionalProperties: false,
130
137
  },
131
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
+ },
132
163
  minBundles: {
133
164
  type: 'number',
134
165
  },
@@ -240,9 +271,8 @@ export async function loadBundlerConfig(
240
271
  return {
241
272
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
242
273
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
243
- sharedBundleMergeThreshold:
244
- modeConfig.sharedBundleMergeThreshold ??
245
- defaults.sharedBundleMergeThreshold,
274
+ sharedBundleMerge:
275
+ modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
246
276
  maxParallelRequests:
247
277
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
248
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
@@ -1278,6 +1295,13 @@ export function createIdealGraph(
1278
1295
  assetReference.set(asset, newAssetReference);
1279
1296
  }
1280
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
+
1281
1305
  for (let sourceBundleId of bundleToRemove.sourceBundles) {
1282
1306
  if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1283
1307
  continue;
@@ -1287,21 +1311,10 @@ export function createIdealGraph(
1287
1311
  bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1288
1312
  }
1289
1313
 
1290
- // Merge any internalized assets
1291
- if (bundleToRemove.internalizedAssets) {
1292
- if (bundleToKeep.internalizedAssets) {
1293
- bundleToKeep.internalizedAssets.union(
1294
- bundleToRemove.internalizedAssets,
1295
- );
1296
- } else {
1297
- bundleToKeep.internalizedAssets = bundleToRemove.internalizedAssets;
1298
- }
1299
- }
1300
-
1301
1314
  bundleGraph.removeNode(bundleToRemoveId);
1302
1315
  }
1303
1316
 
1304
- function mergeOverlapBundles() {
1317
+ function mergeOverlapBundles(mergeConfig: MergeCandidates) {
1305
1318
  // Find all shared bundles
1306
1319
  let sharedBundles = new Set<NodeId>();
1307
1320
  bundleGraph.traverse((nodeId) => {
@@ -1331,7 +1344,20 @@ export function createIdealGraph(
1331
1344
  let clusters = findMergeCandidates(
1332
1345
  bundleGraph,
1333
1346
  Array.from(sharedBundles),
1334
- 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
+ })),
1335
1361
  );
1336
1362
 
1337
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
+ }