@atlaspack/bundler-default 2.14.5-canary.6 → 2.14.5-canary.61

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