@atlaspack/bundler-default 2.14.5-canary.14 → 2.14.5-canary.140

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.
@@ -0,0 +1,245 @@
1
+ import invariant from 'assert';
2
+ import nullthrows from 'nullthrows';
3
+ import {ContentGraph} from '@atlaspack/graph';
4
+ import type {NodeId} from '@atlaspack/graph';
5
+ import {setUnion, setIntersectStatic} from '@atlaspack/utils';
6
+ import type {Bundle, IdealBundleGraph} from './idealGraph';
7
+ import {memoize, clearCaches} from './memoize';
8
+
9
+ function getBundlesForBundleGroup(
10
+ bundleGraph: IdealBundleGraph,
11
+ bundleGroupId: NodeId,
12
+ ): number {
13
+ let count = 0;
14
+ bundleGraph.traverse((nodeId) => {
15
+ // @ts-expect-error TS2339
16
+ if (bundleGraph.getNode(nodeId)?.bundleBehavior !== 'inline') {
17
+ count++;
18
+ }
19
+ }, bundleGroupId);
20
+ return count;
21
+ }
22
+
23
+ let getBundleOverlap = (
24
+ sourceBundlesA: Set<NodeId>,
25
+ sourceBundlesB: Set<NodeId>,
26
+ ): number => {
27
+ let allSourceBundles = setUnion(sourceBundlesA, sourceBundlesB);
28
+ let sharedSourceBundles = setIntersectStatic(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
+ getBundleOverlap(
42
+ bundleA.bundle.sourceBundles,
43
+ bundleB.bundle.sourceBundles,
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: Array<Array<NodeId>> = [];
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
+ contentKey: string;
151
+ };
152
+ function getPossibleMergeCandidates(
153
+ bundleGraph: IdealBundleGraph,
154
+ bundles: Array<NodeId>,
155
+ ): Array<[MergeCandidate, MergeCandidate]> {
156
+ let mergeCandidates = bundles.map((bundleId) => {
157
+ let bundle = bundleGraph.getNode(bundleId);
158
+ invariant(bundle && bundle !== 'root', 'Bundle should exist');
159
+
160
+ return {
161
+ id: bundleId,
162
+ bundle,
163
+ contentKey: bundleId.toString(),
164
+ };
165
+ });
166
+
167
+ const uniquePairs: Array<[MergeCandidate, MergeCandidate]> = [];
168
+
169
+ for (let i = 0; i < mergeCandidates.length; i++) {
170
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
171
+ let a = mergeCandidates[i];
172
+ let b = mergeCandidates[j];
173
+
174
+ // @ts-expect-error TS18048
175
+ if (a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)) {
176
+ uniquePairs.push([a, b]);
177
+ }
178
+ }
179
+ }
180
+ return uniquePairs;
181
+ }
182
+
183
+ export type MergeGroup = {
184
+ overlapThreshold?: number;
185
+ maxBundleSize?: number;
186
+ sourceBundles?: Array<NodeId>;
187
+ minBundlesInGroup?: number;
188
+ };
189
+ type EdgeType = number;
190
+
191
+ export function findMergeCandidates(
192
+ bundleGraph: IdealBundleGraph,
193
+ bundles: Array<NodeId>,
194
+ config: Array<MergeGroup>,
195
+ ): Array<Array<NodeId>> {
196
+ let graph = new ContentGraph<NodeId, EdgeType>();
197
+ let candidates = new Map<NodeId, EdgeType>();
198
+
199
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(
200
+ bundleGraph,
201
+ bundles,
202
+ );
203
+
204
+ // Build graph of clustered merge candidates
205
+ for (let i = 0; i < config.length; i++) {
206
+ // Ensure edge type coresponds to config index
207
+ let edgeType = i + 1;
208
+
209
+ for (let group of allPossibleMergeCandidates) {
210
+ let candidateA = group[0];
211
+ let candidateB = group[1];
212
+
213
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
214
+ continue;
215
+ }
216
+
217
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(
218
+ candidateA.contentKey,
219
+ candidateA.id,
220
+ );
221
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
222
+ candidateB.contentKey,
223
+ candidateB.id,
224
+ );
225
+
226
+ // Add edge in both directions
227
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
228
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
229
+
230
+ candidates.set(bundleNode, edgeType);
231
+ candidates.set(otherBundleNode, edgeType);
232
+ }
233
+
234
+ // Remove bundles that have been allocated to a higher priority merge
235
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(
236
+ (group) =>
237
+ !graph.hasContentKey(group[0].contentKey) &&
238
+ !graph.hasContentKey(group[1].contentKey),
239
+ );
240
+ }
241
+
242
+ clearCaches();
243
+
244
+ return getMergeClusters(graph, candidates);
245
+ }
@@ -1,5 +1,3 @@
1
- // @flow strict-local
2
-
3
1
  import {encodeJSONKeyComponent} from '@atlaspack/diagnostic';
4
2
  import type {
5
3
  Config,
@@ -7,55 +5,63 @@ import type {
7
5
  BuildMode,
8
6
  PluginLogger,
9
7
  } from '@atlaspack/types';
10
- import {type SchemaEntity, validateSchema} from '@atlaspack/utils';
8
+ import {getFeatureFlag} from '@atlaspack/feature-flags';
9
+ import {SchemaEntity, validateSchema} from '@atlaspack/utils';
11
10
  import invariant from 'assert';
12
11
 
13
12
  type Glob = string;
14
13
 
15
- type ManualSharedBundles = Array<{|
16
- name: string,
17
- assets: Array<Glob>,
18
- types?: Array<string>,
19
- root?: string,
20
- split?: number,
21
- |}>;
22
-
23
- type BaseBundlerConfig = {|
24
- http?: number,
25
- minBundles?: number,
26
- minBundleSize?: number,
27
- maxParallelRequests?: number,
28
- disableSharedBundles?: boolean,
29
- manualSharedBundles?: ManualSharedBundles,
30
- loadConditionalBundlesInParallel?: boolean,
31
- sharedBundleMergeThreshold?: number,
32
- |};
33
-
34
- type BundlerConfig = {|
35
- [mode: BuildMode]: BaseBundlerConfig,
36
- |} & BaseBundlerConfig;
37
-
38
- export type ResolvedBundlerConfig = {|
39
- minBundles: number,
40
- minBundleSize: number,
41
- maxParallelRequests: number,
42
- projectRoot: string,
43
- disableSharedBundles: boolean,
44
- manualSharedBundles: ManualSharedBundles,
45
- loadConditionalBundlesInParallel?: boolean,
46
- sharedBundleMergeThreshold: number,
47
- |};
14
+ type ManualSharedBundles = Array<{
15
+ name: string;
16
+ assets: Array<Glob>;
17
+ types?: Array<string>;
18
+ root?: string;
19
+ split?: number;
20
+ }>;
21
+
22
+ export type MergeCandidates = Array<{
23
+ overlapThreshold?: number;
24
+ maxBundleSize?: number;
25
+ sourceBundles?: Array<string>;
26
+ minBundlesInGroup?: number;
27
+ }>;
28
+
29
+ type BaseBundlerConfig = {
30
+ http?: number;
31
+ minBundles?: number;
32
+ minBundleSize?: number;
33
+ maxParallelRequests?: number;
34
+ disableSharedBundles?: boolean;
35
+ manualSharedBundles?: ManualSharedBundles;
36
+ loadConditionalBundlesInParallel?: boolean;
37
+ sharedBundleMerge?: MergeCandidates;
38
+ };
39
+
40
+ type BundlerConfig = Partial<Record<BuildMode, BaseBundlerConfig>> &
41
+ BaseBundlerConfig;
42
+
43
+ export type ResolvedBundlerConfig = {
44
+ minBundles: number;
45
+ minBundleSize: number;
46
+ maxParallelRequests: number;
47
+ projectRoot: string;
48
+ disableSharedBundles: boolean;
49
+ manualSharedBundles: ManualSharedBundles;
50
+ loadConditionalBundlesInParallel?: boolean;
51
+ sharedBundleMerge?: MergeCandidates;
52
+ };
48
53
 
49
54
  function resolveModeConfig(
50
55
  config: BundlerConfig,
51
56
  mode: BuildMode,
52
57
  ): BaseBundlerConfig {
53
- let generalConfig = {};
54
- let modeConfig = {};
58
+ let generalConfig: Record<string, any> = {};
59
+ let modeConfig: Record<string, any> = {};
55
60
 
56
61
  for (const key of Object.keys(config)) {
57
62
  if (key === 'development' || key === 'production') {
58
63
  if (key === mode) {
64
+ // @ts-expect-error TS2322
59
65
  modeConfig = config[key];
60
66
  }
61
67
  } else {
@@ -63,7 +69,6 @@ function resolveModeConfig(
63
69
  }
64
70
  }
65
71
 
66
- // $FlowFixMe Not sure how to convince flow here...
67
72
  return {
68
73
  ...generalConfig,
69
74
  ...modeConfig,
@@ -78,7 +83,7 @@ const HTTP_OPTIONS = {
78
83
  minBundleSize: 30000,
79
84
  maxParallelRequests: 6,
80
85
  disableSharedBundles: false,
81
- sharedBundleMergeThreshold: 1,
86
+ sharedBundleMerge: [],
82
87
  },
83
88
  '2': {
84
89
  minBundles: 1,
@@ -86,9 +91,9 @@ const HTTP_OPTIONS = {
86
91
  minBundleSize: 20000,
87
92
  maxParallelRequests: 25,
88
93
  disableSharedBundles: false,
89
- sharedBundleMergeThreshold: 1,
94
+ sharedBundleMerge: [],
90
95
  },
91
- };
96
+ } as const;
92
97
 
93
98
  const CONFIG_SCHEMA: SchemaEntity = {
94
99
  type: 'object',
@@ -128,6 +133,30 @@ const CONFIG_SCHEMA: SchemaEntity = {
128
133
  additionalProperties: false,
129
134
  },
130
135
  },
136
+ sharedBundleMerge: {
137
+ type: 'array',
138
+ items: {
139
+ type: 'object',
140
+ properties: {
141
+ overlapThreshold: {
142
+ type: 'number',
143
+ },
144
+ maxBundleSize: {
145
+ type: 'number',
146
+ },
147
+ sourceBundles: {
148
+ type: 'array',
149
+ items: {
150
+ type: 'string',
151
+ },
152
+ },
153
+ minBundlesInGroup: {
154
+ type: 'number',
155
+ },
156
+ },
157
+ additionalProperties: false,
158
+ },
159
+ },
131
160
  minBundles: {
132
161
  type: 'number',
133
162
  },
@@ -155,15 +184,24 @@ export async function loadBundlerConfig(
155
184
  options: PluginOptions,
156
185
  logger: PluginLogger,
157
186
  ): Promise<ResolvedBundlerConfig> {
158
- let conf = await config.getConfig<BundlerConfig>([], {
159
- packageKey: '@atlaspack/bundler-default',
160
- });
187
+ let conf;
188
+
189
+ if (getFeatureFlag('resolveBundlerConfigFromCwd')) {
190
+ conf = await config.getConfigFrom(`${process.cwd()}/index`, [], {
191
+ packageKey: '@atlaspack/bundler-default',
192
+ });
193
+ } else {
194
+ conf = await config.getConfig<BundlerConfig>([], {
195
+ packageKey: '@atlaspack/bundler-default',
196
+ });
197
+ }
161
198
 
162
199
  if (!conf) {
163
200
  const modDefault = {
164
201
  ...HTTP_OPTIONS['2'],
165
202
  projectRoot: options.projectRoot,
166
- };
203
+ } as const;
204
+ // @ts-expect-error TS2322
167
205
  return modDefault;
168
206
  }
169
207
 
@@ -226,14 +264,14 @@ export async function loadBundlerConfig(
226
264
  );
227
265
 
228
266
  let http = modeConfig.http ?? 2;
267
+ // @ts-expect-error TS7053
229
268
  let defaults = HTTP_OPTIONS[http];
230
269
 
231
270
  return {
232
271
  minBundles: modeConfig.minBundles ?? defaults.minBundles,
233
272
  minBundleSize: modeConfig.minBundleSize ?? defaults.minBundleSize,
234
- sharedBundleMergeThreshold:
235
- modeConfig.sharedBundleMergeThreshold ??
236
- defaults.sharedBundleMergeThreshold,
273
+ sharedBundleMerge:
274
+ modeConfig.sharedBundleMerge ?? defaults.sharedBundleMerge,
237
275
  maxParallelRequests:
238
276
  modeConfig.maxParallelRequests ?? defaults.maxParallelRequests,
239
277
  projectRoot: options.projectRoot,
@@ -1,6 +1,4 @@
1
- // @flow strict-local
2
-
3
- import {ALL_EDGE_TYPES, type NodeId} from '@atlaspack/graph';
1
+ import {ALL_EDGE_TYPES, NodeId} from '@atlaspack/graph';
4
2
  import type {
5
3
  Bundle as LegacyBundle,
6
4
  BundleGroup,
@@ -25,11 +23,13 @@ export function decorateLegacyGraph(
25
23
  bundleGroupBundleIds,
26
24
  manualAssetToBundle,
27
25
  } = idealGraph;
26
+ // This line can be deleted once supportWebpackChunkName feature flag is removed.
28
27
  let entryBundleToBundleGroup: Map<NodeId, BundleGroup> = new Map();
29
28
  // Step Create Bundles: Create bundle groups, bundles, and shared bundles and add assets to them
30
29
  for (let [bundleNodeId, idealBundle] of idealBundleGraph.nodes.entries()) {
31
30
  if (!idealBundle || idealBundle === 'root') continue;
32
31
  let entryAsset = idealBundle.mainEntryAsset;
32
+ // This line can be deleted once supportWebpackChunkName feature flag is removed.
33
33
  let bundleGroup;
34
34
  let bundle;
35
35
 
@@ -52,18 +52,26 @@ export function decorateLegacyGraph(
52
52
  entryAsset != null,
53
53
  'Processing a bundleGroup with no entry asset',
54
54
  );
55
+
56
+ let bundleGroups = new Map();
55
57
  for (let dependency of dependencies) {
56
58
  bundleGroup = bundleGraph.createBundleGroup(
57
59
  dependency,
58
60
  idealBundle.target,
59
61
  );
62
+ bundleGroups.set(bundleGroup.entryAssetId, bundleGroup);
63
+ }
64
+ if (getFeatureFlag('supportWebpackChunkName')) {
65
+ invariant(bundleGroups.size > 0, 'No bundle groups created');
66
+ } else {
67
+ invariant(bundleGroup);
68
+ entryBundleToBundleGroup.set(bundleNodeId, bundleGroup);
60
69
  }
61
- invariant(bundleGroup);
62
- entryBundleToBundleGroup.set(bundleNodeId, bundleGroup);
63
70
 
64
71
  bundle = nullthrows(
65
72
  bundleGraph.createBundle({
66
73
  entryAsset: nullthrows(entryAsset),
74
+ bundleRoots: Array.from(idealBundle.bundleRoots),
67
75
  needsStableName: idealBundle.needsStableName,
68
76
  bundleBehavior: idealBundle.bundleBehavior,
69
77
  target: idealBundle.target,
@@ -71,7 +79,14 @@ export function decorateLegacyGraph(
71
79
  }),
72
80
  );
73
81
 
74
- bundleGraph.addBundleToBundleGroup(bundle, bundleGroup);
82
+ if (getFeatureFlag('supportWebpackChunkName')) {
83
+ for (let bundleGroup of bundleGroups.values()) {
84
+ bundleGraph.addBundleToBundleGroup(bundle, bundleGroup);
85
+ }
86
+ } else {
87
+ invariant(bundleGroup);
88
+ bundleGraph.addBundleToBundleGroup(bundle, bundleGroup);
89
+ }
75
90
  } else if (
76
91
  idealBundle.sourceBundles.size > 0 &&
77
92
  !idealBundle.mainEntryAsset
@@ -109,6 +124,7 @@ export function decorateLegacyGraph(
109
124
  bundle = nullthrows(
110
125
  bundleGraph.createBundle({
111
126
  entryAsset,
127
+ bundleRoots: Array.from(idealBundle.bundleRoots),
112
128
  needsStableName: idealBundle.needsStableName,
113
129
  bundleBehavior: idealBundle.bundleBehavior,
114
130
  target: idealBundle.target,
@@ -195,11 +211,13 @@ export function decorateLegacyGraph(
195
211
  }
196
212
  }
197
213
 
214
+ // @ts-expect-error TS2488
198
215
  for (let {type, from, to} of idealBundleGraph.getAllEdges()) {
199
216
  let sourceBundle = nullthrows(idealBundleGraph.getNode(from));
200
217
  if (sourceBundle === 'root') {
201
218
  continue;
202
219
  }
220
+ // @ts-expect-error TS2367
203
221
  invariant(sourceBundle !== 'root');
204
222
 
205
223
  let legacySourceBundle = nullthrows(
@@ -210,6 +228,7 @@ export function decorateLegacyGraph(
210
228
  if (targetBundle === 'root') {
211
229
  continue;
212
230
  }
231
+ // @ts-expect-error TS2367
213
232
  invariant(targetBundle !== 'root');
214
233
  let legacyTargetBundle = nullthrows(
215
234
  idealBundleToLegacyBundle.get(targetBundle),