@atlaspack/bundler-default 2.14.5-canary.5 → 2.14.5-canary.51

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,73 @@
1
1
  # @atlaspack/bundler-default
2
2
 
3
+ ## 2.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 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)]:
8
+ - @atlaspack/feature-flags@2.15.1
9
+ - @atlaspack/rust@3.3.3
10
+ - @atlaspack/graph@3.4.6
11
+ - @atlaspack/utils@2.14.8
12
+ - @atlaspack/plugin@2.14.8
13
+
14
+ ## 2.16.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [#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.
19
+
20
+ ### Patch Changes
21
+
22
+ - Updated dependencies [[`a1773d2`](https://github.com/atlassian-labs/atlaspack/commit/a1773d2a62d0ef7805ac7524621dcabcc1afe929), [`556d6ab`](https://github.com/atlassian-labs/atlaspack/commit/556d6ab8ede759fa7f37fcd3f4da336ef1c55e8f)]:
23
+ - @atlaspack/feature-flags@2.15.0
24
+ - @atlaspack/rust@3.3.2
25
+ - @atlaspack/graph@3.4.5
26
+ - @atlaspack/utils@2.14.7
27
+ - @atlaspack/plugin@2.14.7
28
+
29
+ ## 2.15.1
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [[`e0f5337`](https://github.com/atlassian-labs/atlaspack/commit/e0f533757bd1019dbd108a04952c87da15286e09)]:
34
+ - @atlaspack/feature-flags@2.14.4
35
+ - @atlaspack/rust@3.3.1
36
+ - @atlaspack/graph@3.4.4
37
+ - @atlaspack/utils@2.14.6
38
+ - @atlaspack/plugin@2.14.6
39
+
40
+ ## 2.15.0
41
+
42
+ ### Minor Changes
43
+
44
+ - [#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
45
+
46
+ In apps with lots of dynamic imports, many shared bundles are often removed
47
+ from the output to prevent an overload in network requests according to the
48
+ `maxParallelRequests` config. In these cases, setting `sharedBundleMergeThreshold` can
49
+ merge shared bundles with a high overlap in their source bundles (bundles that share the bundle).
50
+ This config trades-off potential overfetching to reduce asset duplication.
51
+
52
+ The following config would merge shared bundles that have a 75% or higher overlap in source bundles.
53
+
54
+ ```json
55
+ {
56
+ "@atlaspack/bundler-default": {
57
+ "sharedBundleMergeThreshold": 0.75
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Patch Changes
63
+
64
+ - 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)]:
65
+ - @atlaspack/feature-flags@2.14.3
66
+ - @atlaspack/rust@3.3.0
67
+ - @atlaspack/graph@3.4.3
68
+ - @atlaspack/utils@2.14.5
69
+ - @atlaspack/plugin@2.14.5
70
+
3
71
  ## 2.14.4
4
72
 
5
73
  ### Patch Changes
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.findMergeCandidates = findMergeCandidates;
7
+ function _assert() {
8
+ const data = _interopRequireDefault(require("assert"));
9
+ _assert = function () {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function _nullthrows() {
15
+ const data = _interopRequireDefault(require("nullthrows"));
16
+ _nullthrows = function () {
17
+ return data;
18
+ };
19
+ return data;
20
+ }
21
+ function _graph() {
22
+ const data = require("@atlaspack/graph");
23
+ _graph = function () {
24
+ return data;
25
+ };
26
+ return data;
27
+ }
28
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
29
+ // Returns a decimal showing the proportion source bundles are common to
30
+ // 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++;
37
+ }
38
+ }
39
+ return sharedSourceBundles / allSourceBundles.size;
40
+ }
41
+ function getMergeClusters(graph, candidates) {
42
+ let clusters = [];
43
+ for (let candidate of candidates) {
44
+ let cluster = [];
45
+ graph.traverse(nodeId => {
46
+ cluster.push((0, _nullthrows().default)(graph.getNode(nodeId)));
47
+ // Remove node from candidates as it has already been processed
48
+ candidates.delete(nodeId);
49
+ }, candidate);
50
+ clusters.push(cluster);
51
+ }
52
+ return clusters;
53
+ }
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) {
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) {
68
+ continue;
69
+ }
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);
81
+
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
+ }
88
+ }
89
+ }
90
+ return getMergeClusters(graph, candidates);
91
+ }
@@ -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
+ sharedBundleMergeThreshold: 1
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
+ sharedBundleMergeThreshold: 1
64
73
  }
65
74
  };
66
75
  const CONFIG_SCHEMA = {
@@ -115,14 +124,25 @@ const CONFIG_SCHEMA = {
115
124
  },
116
125
  loadConditionalBundlesInParallel: {
117
126
  type: 'boolean'
127
+ },
128
+ sharedBundleMergeThreshold: {
129
+ type: 'number'
118
130
  }
119
131
  },
120
132
  additionalProperties: false
121
133
  };
122
134
  async function loadBundlerConfig(config, options, logger) {
123
- let conf = await config.getConfig([], {
124
- packageKey: '@atlaspack/bundler-default'
125
- });
135
+ var _conf;
136
+ let conf;
137
+ if ((0, _featureFlags().getFeatureFlag)('resolveBundlerConfigFromCwd')) {
138
+ conf = await config.getConfigFrom(`${process.cwd()}/index`, [], {
139
+ packageKey: '@atlaspack/bundler-default'
140
+ });
141
+ } else {
142
+ conf = await config.getConfig([], {
143
+ packageKey: '@atlaspack/bundler-default'
144
+ });
145
+ }
126
146
  if (!conf) {
127
147
  const modDefault = {
128
148
  ...HTTP_OPTIONS['2'],
@@ -130,7 +150,7 @@ async function loadBundlerConfig(config, options, logger) {
130
150
  };
131
151
  return modDefault;
132
152
  }
133
- (0, _assert().default)((conf === null || conf === void 0 ? void 0 : conf.contents) != null);
153
+ (0, _assert().default)(((_conf = conf) === null || _conf === void 0 ? void 0 : _conf.contents) != null);
134
154
  let modeConfig = resolveModeConfig(conf.contents, options.mode);
135
155
 
136
156
  // minBundles will be ignored if shared bundles are disabled
@@ -172,6 +192,7 @@ async function loadBundlerConfig(config, options, logger) {
172
192
  return {
173
193
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
174
194
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
195
+ sharedBundleMergeThreshold: modeConfig.sharedBundleMergeThreshold ?? defaults.sharedBundleMergeThreshold,
175
196
  maxParallelRequests: modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
176
197
  projectRoot: options.projectRoot,
177
198
  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 = {
@@ -848,6 +849,12 @@ function createIdealGraph(assetGraph, config, entries, logger) {
848
849
  }
849
850
  }
850
851
 
852
+ // Step merge shared bundles that meet the overlap threshold
853
+ // This step is skipped by default as the threshold defaults to 1
854
+ if (config.sharedBundleMergeThreshold < 1) {
855
+ mergeOverlapBundles();
856
+ }
857
+
851
858
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
852
859
  // their source bundles, and remove the bundle.
853
860
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -857,9 +864,9 @@ function createIdealGraph(assetGraph, config, entries, logger) {
857
864
  removeBundle(bundleGraph, bundleNodeId, assetReference);
858
865
  }
859
866
  }
860
- let modifiedSourceBundles = new Set();
861
867
 
862
868
  // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
869
+ let modifiedSourceBundles = new Set();
863
870
  if (config.disableSharedBundles === false) {
864
871
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
865
872
  // Find shared bundles in this bundle group.
@@ -944,6 +951,61 @@ function createIdealGraph(assetGraph, config, entries, logger) {
944
951
  }
945
952
  }
946
953
  }
954
+ function mergeBundles(bundleGraph, bundleToKeepId, bundleToRemoveId, assetReference) {
955
+ let bundleToKeep = (0, _nullthrows().default)(bundleGraph.getNode(bundleToKeepId));
956
+ let bundleToRemove = (0, _nullthrows().default)(bundleGraph.getNode(bundleToRemoveId));
957
+ (0, _assert().default)(bundleToKeep !== 'root' && bundleToRemove !== 'root');
958
+ for (let asset of bundleToRemove.assets) {
959
+ bundleToKeep.assets.add(asset);
960
+ bundleToKeep.size += asset.stats.size;
961
+ let newAssetReference = assetReference.get(asset).map(([dep, bundle]) => bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle]);
962
+ assetReference.set(asset, newAssetReference);
963
+ }
964
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
965
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
966
+ continue;
967
+ }
968
+ bundleToKeep.sourceBundles.add(sourceBundleId);
969
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
970
+ }
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
+ bundleGraph.removeNode(bundleToRemoveId);
981
+ }
982
+ function mergeOverlapBundles() {
983
+ // Find all shared bundles
984
+ let sharedBundles = new Set();
985
+ bundleGraph.traverse(nodeId => {
986
+ let bundle = bundleGraph.getNode(nodeId);
987
+ if (!bundle) {
988
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
989
+ }
990
+ if (bundle === 'root') {
991
+ return;
992
+ }
993
+
994
+ // Only consider JS shared bundles and non-reused bundles.
995
+ // These count potentially be considered for merging in future but they're
996
+ // more complicated to merge
997
+ if (bundle.sourceBundles.size > 0 && bundle.manualSharedBundle == null && !bundle.mainEntryAsset && bundle.type === 'js') {
998
+ sharedBundles.add(nodeId);
999
+ }
1000
+ });
1001
+ let clusters = (0, _bundleMerge.findMergeCandidates)(bundleGraph, Array.from(sharedBundles), config.sharedBundleMergeThreshold);
1002
+ for (let cluster of clusters) {
1003
+ let [mergeTarget, ...rest] = cluster;
1004
+ for (let bundleIdToMerge of rest) {
1005
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1006
+ }
1007
+ }
1008
+ }
947
1009
  function getBigIntFromContentKey(contentKey) {
948
1010
  let b = Buffer.alloc(64);
949
1011
  b.write(contentKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/bundler-default",
3
- "version": "2.14.5-canary.5+11d6f16b6",
3
+ "version": "2.14.5-canary.51+15b61556e",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -16,13 +16,13 @@
16
16
  "node": ">= 16.0.0"
17
17
  },
18
18
  "dependencies": {
19
- "@atlaspack/diagnostic": "2.14.1-canary.73+11d6f16b6",
20
- "@atlaspack/feature-flags": "2.14.1-canary.73+11d6f16b6",
21
- "@atlaspack/graph": "3.4.1-canary.73+11d6f16b6",
22
- "@atlaspack/plugin": "2.14.5-canary.5+11d6f16b6",
23
- "@atlaspack/rust": "3.2.1-canary.5+11d6f16b6",
24
- "@atlaspack/utils": "2.14.5-canary.5+11d6f16b6",
19
+ "@atlaspack/diagnostic": "2.14.1-canary.119+15b61556e",
20
+ "@atlaspack/feature-flags": "2.14.1-canary.119+15b61556e",
21
+ "@atlaspack/graph": "3.4.1-canary.119+15b61556e",
22
+ "@atlaspack/plugin": "2.14.5-canary.51+15b61556e",
23
+ "@atlaspack/rust": "3.2.1-canary.51+15b61556e",
24
+ "@atlaspack/utils": "2.14.5-canary.51+15b61556e",
25
25
  "nullthrows": "^1.1.1"
26
26
  },
27
- "gitHead": "11d6f16b6397dee2f217167e5c98b39edb63f7a7"
27
+ "gitHead": "15b61556e9114203ebbc9de94b864118ca764598"
28
28
  }
@@ -0,0 +1,103 @@
1
+ // @flow strict-local
2
+
3
+ import invariant from 'assert';
4
+ import nullthrows from 'nullthrows';
5
+ import type {NodeId} from '@atlaspack/graph';
6
+ import type {Bundle, IdealBundleGraph} from './idealGraph';
7
+ import {ContentGraph} from '@atlaspack/graph';
8
+
9
+ // Returns a decimal showing the proportion source bundles are common to
10
+ // 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++;
21
+ }
22
+ }
23
+
24
+ return sharedSourceBundles / allSourceBundles.size;
25
+ }
26
+
27
+ function getMergeClusters(
28
+ graph: ContentGraph<NodeId>,
29
+ candidates: Set<NodeId>,
30
+ ): Array<Array<NodeId>> {
31
+ let clusters = [];
32
+
33
+ for (let candidate of candidates) {
34
+ let cluster: Array<NodeId> = [];
35
+
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
+
42
+ clusters.push(cluster);
43
+ }
44
+
45
+ return clusters;
46
+ }
47
+
48
+ export function findMergeCandidates(
49
+ bundleGraph: IdealBundleGraph,
50
+ 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) {
59
+ 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;
68
+ }
69
+
70
+ let key = [bundleId, otherBundleId].sort().join(':');
71
+
72
+ if (seen.has(key)) {
73
+ continue;
74
+ }
75
+ seen.add(key);
76
+
77
+ let otherBundle = bundleGraph.getNode(otherBundleId);
78
+ invariant(otherBundle && otherBundle !== 'root');
79
+
80
+ let score = scoreBundleMerge(bundle, otherBundle);
81
+
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
+ );
91
+
92
+ // Add edge in both directions
93
+ graph.addEdge(bundleNode, otherBundleNode);
94
+ graph.addEdge(otherBundleNode, bundleNode);
95
+
96
+ candidates.add(bundleNode);
97
+ candidates.add(otherBundleNode);
98
+ }
99
+ }
100
+ }
101
+
102
+ return getMergeClusters(graph, candidates);
103
+ }
@@ -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
 
@@ -28,6 +29,7 @@ type BaseBundlerConfig = {|
28
29
  disableSharedBundles?: boolean,
29
30
  manualSharedBundles?: ManualSharedBundles,
30
31
  loadConditionalBundlesInParallel?: boolean,
32
+ sharedBundleMergeThreshold?: number,
31
33
  |};
32
34
 
33
35
  type BundlerConfig = {|
@@ -42,6 +44,7 @@ export type ResolvedBundlerConfig = {|
42
44
  disableSharedBundles: boolean,
43
45
  manualSharedBundles: ManualSharedBundles,
44
46
  loadConditionalBundlesInParallel?: boolean,
47
+ sharedBundleMergeThreshold: number,
45
48
  |};
46
49
 
47
50
  function resolveModeConfig(
@@ -76,6 +79,7 @@ const HTTP_OPTIONS = {
76
79
  minBundleSize: 30000,
77
80
  maxParallelRequests: 6,
78
81
  disableSharedBundles: false,
82
+ sharedBundleMergeThreshold: 1,
79
83
  },
80
84
  '2': {
81
85
  minBundles: 1,
@@ -83,6 +87,7 @@ const HTTP_OPTIONS = {
83
87
  minBundleSize: 20000,
84
88
  maxParallelRequests: 25,
85
89
  disableSharedBundles: false,
90
+ sharedBundleMergeThreshold: 1,
86
91
  },
87
92
  };
88
93
 
@@ -139,6 +144,9 @@ const CONFIG_SCHEMA: SchemaEntity = {
139
144
  loadConditionalBundlesInParallel: {
140
145
  type: 'boolean',
141
146
  },
147
+ sharedBundleMergeThreshold: {
148
+ type: 'number',
149
+ },
142
150
  },
143
151
  additionalProperties: false,
144
152
  };
@@ -148,9 +156,17 @@ export async function loadBundlerConfig(
148
156
  options: PluginOptions,
149
157
  logger: PluginLogger,
150
158
  ): Promise<ResolvedBundlerConfig> {
151
- let conf = await config.getConfig<BundlerConfig>([], {
152
- packageKey: '@atlaspack/bundler-default',
153
- });
159
+ let conf;
160
+
161
+ if (getFeatureFlag('resolveBundlerConfigFromCwd')) {
162
+ conf = await config.getConfigFrom(`${process.cwd()}/index`, [], {
163
+ packageKey: '@atlaspack/bundler-default',
164
+ });
165
+ } else {
166
+ conf = await config.getConfig<BundlerConfig>([], {
167
+ packageKey: '@atlaspack/bundler-default',
168
+ });
169
+ }
154
170
 
155
171
  if (!conf) {
156
172
  const modDefault = {
@@ -224,6 +240,9 @@ export async function loadBundlerConfig(
224
240
  return {
225
241
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
226
242
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
243
+ sharedBundleMergeThreshold:
244
+ modeConfig.sharedBundleMergeThreshold ??
245
+ defaults.sharedBundleMergeThreshold,
227
246
  maxParallelRequests:
228
247
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
229
248
  projectRoot: options.projectRoot,
package/src/idealGraph.js CHANGED
@@ -23,6 +23,7 @@ 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';
26
27
  import type {ResolvedBundlerConfig} from './bundlerConfig';
27
28
 
28
29
  /* BundleRoot - An asset that is the main entry of a Bundle. */
@@ -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
  >;
@@ -1128,6 +1129,12 @@ export function createIdealGraph(
1128
1129
  }
1129
1130
  }
1130
1131
 
1132
+ // Step merge shared bundles that meet the overlap threshold
1133
+ // This step is skipped by default as the threshold defaults to 1
1134
+ if (config.sharedBundleMergeThreshold < 1) {
1135
+ mergeOverlapBundles();
1136
+ }
1137
+
1131
1138
  // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into
1132
1139
  // their source bundles, and remove the bundle.
1133
1140
  // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained
@@ -1143,9 +1150,9 @@ export function createIdealGraph(
1143
1150
  }
1144
1151
  }
1145
1152
 
1153
+ // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1146
1154
  let modifiedSourceBundles = new Set();
1147
1155
 
1148
- // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit.
1149
1156
  if (config.disableSharedBundles === false) {
1150
1157
  for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) {
1151
1158
  // Find shared bundles in this bundle group.
@@ -1249,6 +1256,93 @@ export function createIdealGraph(
1249
1256
  }
1250
1257
  }
1251
1258
 
1259
+ function mergeBundles(
1260
+ bundleGraph: IdealBundleGraph,
1261
+ bundleToKeepId: NodeId,
1262
+ bundleToRemoveId: NodeId,
1263
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>,
1264
+ ) {
1265
+ let bundleToKeep = nullthrows(bundleGraph.getNode(bundleToKeepId));
1266
+ let bundleToRemove = nullthrows(bundleGraph.getNode(bundleToRemoveId));
1267
+ invariant(bundleToKeep !== 'root' && bundleToRemove !== 'root');
1268
+ for (let asset of bundleToRemove.assets) {
1269
+ bundleToKeep.assets.add(asset);
1270
+ bundleToKeep.size += asset.stats.size;
1271
+
1272
+ let newAssetReference = assetReference
1273
+ .get(asset)
1274
+ .map(([dep, bundle]) =>
1275
+ bundle === bundleToRemove ? [dep, bundleToKeep] : [dep, bundle],
1276
+ );
1277
+
1278
+ assetReference.set(asset, newAssetReference);
1279
+ }
1280
+
1281
+ for (let sourceBundleId of bundleToRemove.sourceBundles) {
1282
+ if (bundleToKeep.sourceBundles.has(sourceBundleId)) {
1283
+ continue;
1284
+ }
1285
+
1286
+ bundleToKeep.sourceBundles.add(sourceBundleId);
1287
+ bundleGraph.addEdge(sourceBundleId, bundleToKeepId);
1288
+ }
1289
+
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
+ bundleGraph.removeNode(bundleToRemoveId);
1302
+ }
1303
+
1304
+ function mergeOverlapBundles() {
1305
+ // Find all shared bundles
1306
+ let sharedBundles = new Set<NodeId>();
1307
+ bundleGraph.traverse((nodeId) => {
1308
+ let bundle = bundleGraph.getNode(nodeId);
1309
+
1310
+ if (!bundle) {
1311
+ throw new Error(`Unable to find bundle ${nodeId} in bundle graph`);
1312
+ }
1313
+
1314
+ if (bundle === 'root') {
1315
+ return;
1316
+ }
1317
+
1318
+ // Only consider JS shared bundles and non-reused bundles.
1319
+ // These count potentially be considered for merging in future but they're
1320
+ // more complicated to merge
1321
+ if (
1322
+ bundle.sourceBundles.size > 0 &&
1323
+ bundle.manualSharedBundle == null &&
1324
+ !bundle.mainEntryAsset &&
1325
+ bundle.type === 'js'
1326
+ ) {
1327
+ sharedBundles.add(nodeId);
1328
+ }
1329
+ });
1330
+
1331
+ let clusters = findMergeCandidates(
1332
+ bundleGraph,
1333
+ Array.from(sharedBundles),
1334
+ config.sharedBundleMergeThreshold,
1335
+ );
1336
+
1337
+ for (let cluster of clusters) {
1338
+ let [mergeTarget, ...rest] = cluster;
1339
+
1340
+ for (let bundleIdToMerge of rest) {
1341
+ mergeBundles(bundleGraph, mergeTarget, bundleIdToMerge, assetReference);
1342
+ }
1343
+ }
1344
+ }
1345
+
1252
1346
  function getBigIntFromContentKey(contentKey) {
1253
1347
  let b = Buffer.alloc(64);
1254
1348
  b.write(contentKey);