@atlaspack/bundler-default 2.14.5-canary.36 → 2.14.5-canary.360

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +601 -0
  2. package/dist/DefaultBundler.js +84 -0
  3. package/dist/MonolithicBundler.js +68 -0
  4. package/dist/bundleMerge.js +137 -0
  5. package/dist/bundlerConfig.js +223 -0
  6. package/dist/decorateLegacyGraph.js +189 -0
  7. package/dist/idealGraph.js +1471 -0
  8. package/dist/memoize.js +31 -0
  9. package/dist/stats.js +69 -0
  10. package/lib/DefaultBundler.js +6 -1
  11. package/lib/MonolithicBundler.js +11 -3
  12. package/lib/bundleMerge.js +106 -37
  13. package/lib/bundlerConfig.js +52 -6
  14. package/lib/decorateLegacyGraph.js +24 -3
  15. package/lib/idealGraph.js +410 -55
  16. package/lib/memoize.js +39 -0
  17. package/lib/stats.js +85 -0
  18. package/lib/types/DefaultBundler.d.ts +18 -0
  19. package/lib/types/MonolithicBundler.d.ts +2 -0
  20. package/lib/types/bundleMerge.d.ts +9 -0
  21. package/lib/types/bundlerConfig.d.ts +36 -0
  22. package/lib/types/decorateLegacyGraph.d.ts +3 -0
  23. package/lib/types/idealGraph.d.ts +40 -0
  24. package/lib/types/memoize.d.ts +2 -0
  25. package/lib/types/stats.d.ts +16 -0
  26. package/package.json +20 -12
  27. package/src/{DefaultBundler.js → DefaultBundler.ts} +21 -6
  28. package/src/{MonolithicBundler.js → MonolithicBundler.ts} +17 -5
  29. package/src/bundleMerge.ts +250 -0
  30. package/src/{bundlerConfig.js → bundlerConfig.ts} +106 -45
  31. package/src/{decorateLegacyGraph.js → decorateLegacyGraph.ts} +26 -7
  32. package/src/{idealGraph.js → idealGraph.ts} +729 -137
  33. package/src/memoize.ts +32 -0
  34. package/src/stats.ts +97 -0
  35. package/tsconfig.json +30 -0
  36. package/tsconfig.tsbuildinfo +1 -0
  37. package/src/bundleMerge.js +0 -103
package/lib/memoize.js ADDED
@@ -0,0 +1,39 @@
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
+ let caches = [];
17
+ function clearCaches() {
18
+ for (let cache of caches) {
19
+ cache.clear();
20
+ }
21
+ }
22
+ function memoize(fn) {
23
+ let cache = new (_manyKeysMap().default)();
24
+ caches.push(cache);
25
+ return function (...args) {
26
+ // Navigate through the cache hierarchy
27
+ let cached = cache.get(args);
28
+ if (cached !== undefined) {
29
+ // If the result is cached, return it
30
+ return cached;
31
+ }
32
+
33
+ // Calculate the result and cache it
34
+ // @ts-expect-error TS2683
35
+ const result = fn.apply(this, args);
36
+ cache.set(args, result);
37
+ return result;
38
+ };
39
+ }
package/lib/stats.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.Stats = void 0;
7
+ function _path() {
8
+ const data = require("path");
9
+ _path = function () {
10
+ return data;
11
+ };
12
+ return data;
13
+ }
14
+ function _utils() {
15
+ const data = require("@atlaspack/utils");
16
+ _utils = function () {
17
+ return data;
18
+ };
19
+ return data;
20
+ }
21
+ class Stats {
22
+ merges = new (_utils().DefaultMap)(() => []);
23
+ constructor(projectRoot) {
24
+ this.projectRoot = projectRoot;
25
+ }
26
+ trackMerge(bundleToKeep, bundleToRemove, reason) {
27
+ if (!_utils().debugTools['bundle-stats']) {
28
+ return;
29
+ }
30
+ this.merges.get(bundleToKeep).push(...this.merges.get(bundleToRemove), {
31
+ id: bundleToRemove,
32
+ reason
33
+ });
34
+ this.merges.delete(bundleToRemove);
35
+ }
36
+ getBundleLabel(bundle) {
37
+ if (bundle.manualSharedBundle) {
38
+ return bundle.manualSharedBundle;
39
+ }
40
+ if (bundle.mainEntryAsset) {
41
+ let relativePath = (0, _path().relative)(this.projectRoot, bundle.mainEntryAsset.filePath);
42
+ if (relativePath.length > 100) {
43
+ relativePath = relativePath.slice(0, 50) + '...' + relativePath.slice(-50);
44
+ }
45
+ return relativePath;
46
+ }
47
+ return `shared`;
48
+ }
49
+ report(getBundle) {
50
+ if (!_utils().debugTools['bundle-stats']) {
51
+ return;
52
+ }
53
+ let mergeResults = [];
54
+ let totals = {
55
+ label: 'Totals',
56
+ merges: 0
57
+ };
58
+ for (let [bundleId, mergedBundles] of this.merges) {
59
+ let bundle = getBundle(bundleId);
60
+ if (!bundle) {
61
+ continue;
62
+ }
63
+ let result = {
64
+ label: this.getBundleLabel(bundle),
65
+ size: bundle.size,
66
+ merges: mergedBundles.length
67
+ };
68
+ for (let merged of mergedBundles) {
69
+ result[merged.reason] = (result[merged.reason] || 0) + 1;
70
+ totals[merged.reason] = (totals[merged.reason] || 0) + 1;
71
+ }
72
+ totals.merges += mergedBundles.length;
73
+ mergeResults.push(result);
74
+ }
75
+ mergeResults.sort((a, b) => {
76
+ // Sort by bundle size descending
77
+ return b.size - a.size;
78
+ });
79
+ mergeResults.push(totals);
80
+
81
+ // eslint-disable-next-line no-console
82
+ console.table(mergeResults);
83
+ }
84
+ }
85
+ exports.Stats = Stats;
@@ -0,0 +1,18 @@
1
+ import { Bundler } from '@atlaspack/plugin';
2
+ /**
3
+ *
4
+ * The Bundler works by creating an IdealGraph, which contains a BundleGraph that models bundles
5
+ * connected to other bundles by what references them, and thus models BundleGroups.
6
+ *
7
+ * First, we enter `bundle({bundleGraph, config})`. Here, "bundleGraph" is actually just the
8
+ * assetGraph turned into a type `MutableBundleGraph`, which will then be mutated in decorate,
9
+ * and turned into what we expect the bundleGraph to be as per the old (default) bundler structure
10
+ * & what the rest of Atlaspack expects a BundleGraph to be.
11
+ *
12
+ * `bundle({bundleGraph, config})` First gets a Mapping of target to entries, In most cases there is
13
+ * only one target, and one or more entries. (Targets are pertinent in monorepos or projects where you
14
+ * will have two or more distDirs, or output folders.) Then calls create IdealGraph and Decorate per target.
15
+ *
16
+ */
17
+ declare const _default: Bundler<unknown>;
18
+ export default _default;
@@ -0,0 +1,2 @@
1
+ import type { Asset, Dependency, MutableBundleGraph } from '@atlaspack/types-internal';
2
+ export declare function addJSMonolithBundle(bundleGraph: MutableBundleGraph, entryAsset: Asset, entryDep: Dependency): void;
@@ -0,0 +1,9 @@
1
+ import type { NodeId } from '@atlaspack/graph';
2
+ import type { IdealBundleGraph } from './idealGraph';
3
+ export type MergeGroup = {
4
+ overlapThreshold?: number;
5
+ maxBundleSize?: number;
6
+ sourceBundles?: Array<NodeId>;
7
+ minBundlesInGroup?: number;
8
+ };
9
+ export declare function findMergeCandidates(bundleGraph: IdealBundleGraph, bundles: Array<NodeId>, config: Array<MergeGroup>): Array<Array<NodeId>>;
@@ -0,0 +1,36 @@
1
+ import type { Config, PluginOptions, PluginLogger } from '@atlaspack/types-internal';
2
+ type Glob = string;
3
+ type ManualSharedBundles = Array<{
4
+ name: string;
5
+ assets: Array<Glob>;
6
+ types?: Array<string>;
7
+ root?: string;
8
+ split?: number;
9
+ }>;
10
+ export type SharedBundleMergeCandidates = Array<{
11
+ overlapThreshold?: number;
12
+ maxBundleSize?: number;
13
+ sourceBundles?: Array<string>;
14
+ minBundlesInGroup?: number;
15
+ }>;
16
+ export interface AsyncBundleMerge {
17
+ /** Consider all async bundles smaller than this for merging */
18
+ bundleSize: number;
19
+ /** The max bytes allowed to be potentially overfetched due to a merge */
20
+ maxOverfetchSize: number;
21
+ /** Bundles to ignore from merging */
22
+ ignore?: Array<Glob>;
23
+ }
24
+ export type ResolvedBundlerConfig = {
25
+ minBundles: number;
26
+ minBundleSize: number;
27
+ maxParallelRequests: number;
28
+ projectRoot: string;
29
+ disableSharedBundles: boolean;
30
+ manualSharedBundles: ManualSharedBundles;
31
+ loadConditionalBundlesInParallel?: boolean;
32
+ sharedBundleMerge?: SharedBundleMergeCandidates;
33
+ asyncBundleMerge?: AsyncBundleMerge;
34
+ };
35
+ export declare function loadBundlerConfig(config: Config, options: PluginOptions, logger: PluginLogger): Promise<ResolvedBundlerConfig>;
36
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { MutableBundleGraph } from '@atlaspack/types-internal';
2
+ import type { IdealGraph } from './idealGraph';
3
+ export declare function decorateLegacyGraph(idealGraph: IdealGraph, bundleGraph: MutableBundleGraph): void;
@@ -0,0 +1,40 @@
1
+ import { BitSet, ContentGraph, Graph, NodeId } from '@atlaspack/graph';
2
+ import type { Asset, BundleBehavior, Dependency, Environment, MutableBundleGraph, Target, PluginLogger } from '@atlaspack/types';
3
+ import { DefaultMap } from '@atlaspack/utils';
4
+ import type { ResolvedBundlerConfig } from './bundlerConfig';
5
+ export type Bundle = {
6
+ uniqueKey: string | null | undefined;
7
+ assets: Set<Asset>;
8
+ internalizedAssets?: BitSet;
9
+ bundleBehavior?: BundleBehavior | null | undefined;
10
+ needsStableName: boolean;
11
+ mainEntryAsset: Asset | null | undefined;
12
+ bundleRoots: Set<Asset>;
13
+ size: number;
14
+ sourceBundles: Set<NodeId>;
15
+ target: Target;
16
+ env: Environment;
17
+ type: string;
18
+ manualSharedBundle: string | null | undefined;
19
+ };
20
+ export type DependencyBundleGraph = ContentGraph<{
21
+ value: Bundle;
22
+ type: 'bundle';
23
+ } | {
24
+ value: Dependency;
25
+ type: 'dependency';
26
+ }, number>;
27
+ export declare const idealBundleGraphEdges: Readonly<{
28
+ default: 1;
29
+ conditional: 2;
30
+ }>;
31
+ export type IdealBundleGraph = Graph<Bundle | 'root', (typeof idealBundleGraphEdges)[keyof typeof idealBundleGraphEdges]>;
32
+ export type IdealGraph = {
33
+ assetReference: DefaultMap<Asset, Array<[Dependency, Bundle]>>;
34
+ assets: Array<Asset>;
35
+ bundleGraph: IdealBundleGraph;
36
+ bundleGroupBundleIds: Set<NodeId>;
37
+ dependencyBundleGraph: DependencyBundleGraph;
38
+ manualAssetToBundle: Map<Asset, NodeId>;
39
+ };
40
+ export declare function createIdealGraph(assetGraph: MutableBundleGraph, config: ResolvedBundlerConfig, entries: Map<Asset, Dependency>, logger: PluginLogger): IdealGraph;
@@ -0,0 +1,2 @@
1
+ export declare function clearCaches(): void;
2
+ export declare function memoize<Args extends Array<unknown>, Return>(fn: (...args: Args) => Return): (...args: Args) => Return;
@@ -0,0 +1,16 @@
1
+ import { NodeId } from '@atlaspack/graph';
2
+ import { DefaultMap } from '@atlaspack/utils';
3
+ import { Bundle } from './idealGraph';
4
+ interface MergedBundle {
5
+ id: NodeId;
6
+ reason: string;
7
+ }
8
+ export declare class Stats {
9
+ projectRoot: string;
10
+ merges: DefaultMap<NodeId, MergedBundle[]>;
11
+ constructor(projectRoot: string);
12
+ trackMerge(bundleToKeep: NodeId, bundleToRemove: NodeId, reason: string): void;
13
+ getBundleLabel(bundle: Bundle): string;
14
+ report(getBundle: (bundleId: NodeId) => Bundle | null | undefined): void;
15
+ }
16
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/bundler-default",
3
- "version": "2.14.5-canary.36+2d10c6656",
3
+ "version": "2.14.5-canary.360+fc3adc098",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -10,19 +10,27 @@
10
10
  "type": "git",
11
11
  "url": "https://github.com/atlassian-labs/atlaspack.git"
12
12
  },
13
- "main": "lib/DefaultBundler.js",
14
- "source": "src/DefaultBundler.js",
13
+ "main": "./lib/DefaultBundler.js",
14
+ "source": "./src/DefaultBundler.ts",
15
+ "types": "./lib/types/DefaultBundler.d.ts",
15
16
  "engines": {
16
17
  "node": ">= 16.0.0"
17
18
  },
18
19
  "dependencies": {
19
- "@atlaspack/diagnostic": "2.14.1-canary.104+2d10c6656",
20
- "@atlaspack/feature-flags": "2.14.1-canary.104+2d10c6656",
21
- "@atlaspack/graph": "3.4.1-canary.104+2d10c6656",
22
- "@atlaspack/plugin": "2.14.5-canary.36+2d10c6656",
23
- "@atlaspack/rust": "3.2.1-canary.36+2d10c6656",
24
- "@atlaspack/utils": "2.14.5-canary.36+2d10c6656",
25
- "nullthrows": "^1.1.1"
20
+ "@atlaspack/diagnostic": "2.14.1-canary.428+fc3adc098",
21
+ "@atlaspack/feature-flags": "2.14.1-canary.428+fc3adc098",
22
+ "@atlaspack/graph": "3.4.1-canary.428+fc3adc098",
23
+ "@atlaspack/plugin": "2.14.5-canary.360+fc3adc098",
24
+ "@atlaspack/rust": "3.2.1-canary.360+fc3adc098",
25
+ "@atlaspack/types-internal": "2.14.1-canary.428+fc3adc098",
26
+ "@atlaspack/utils": "2.14.5-canary.360+fc3adc098",
27
+ "@types/sorted-array-functions": "^1.0.0",
28
+ "many-keys-map": "^1.0.3",
29
+ "nullthrows": "^1.1.1",
30
+ "sorted-array-functions": "^1.0.0"
26
31
  },
27
- "gitHead": "2d10c6656fc58743c5dbed1d2b6c5666887f9fe4"
28
- }
32
+ "scripts": {
33
+ "build:lib": "gulp build --gulpfile ../../../gulpfile.js --cwd ."
34
+ },
35
+ "gitHead": "fc3adc098f583e40d6d7687412cac6dde7cbb3f3"
36
+ }
@@ -1,5 +1,3 @@
1
- // @flow strict-local
2
-
3
1
  import {Bundler} from '@atlaspack/plugin';
4
2
  import type {Asset, Dependency, MutableBundleGraph} from '@atlaspack/types';
5
3
  import {DefaultMap} from '@atlaspack/utils';
@@ -25,14 +23,15 @@ import {addJSMonolithBundle} from './MonolithicBundler';
25
23
  * will have two or more distDirs, or output folders.) Then calls create IdealGraph and Decorate per target.
26
24
  *
27
25
  */
28
- export default (new Bundler({
26
+ export default new Bundler({
29
27
  loadConfig({config, options, logger}) {
30
28
  return loadBundlerConfig(config, options, logger);
31
29
  },
32
30
 
33
31
  bundle({bundleGraph, config, logger}) {
34
32
  let targetMap = getEntryByTarget(bundleGraph); // Organize entries by target output folder/ distDir
35
- let graphs = [];
33
+ // @ts-expect-error TS2304
34
+ let graphs: Array<IdealGraph> = [];
36
35
 
37
36
  for (let entries of targetMap.values()) {
38
37
  let singleFileEntries = new Map();
@@ -64,7 +63,7 @@ export default (new Bundler({
64
63
  }
65
64
  },
66
65
  optimize() {},
67
- }): Bundler);
66
+ }) as Bundler<unknown>;
68
67
 
69
68
  function getEntryByTarget(
70
69
  bundleGraph: MutableBundleGraph,
@@ -74,7 +73,23 @@ function getEntryByTarget(
74
73
  () => new Map(),
75
74
  );
76
75
  bundleGraph.traverse({
77
- enter(node, context, actions) {
76
+ enter(
77
+ // @ts-expect-error TS2304
78
+ node: BundleGraphTraversable,
79
+ context:
80
+ | {
81
+ readonly type: 'asset';
82
+ value: Asset;
83
+ }
84
+ | null
85
+ | undefined
86
+ | {
87
+ readonly type: 'dependency';
88
+ value: Dependency;
89
+ },
90
+ // @ts-expect-error TS2304
91
+ actions: TraversalActions,
92
+ ) {
78
93
  if (node.type !== 'asset') {
79
94
  return node;
80
95
  }
@@ -1,5 +1,10 @@
1
- // @flow strict-local
2
- import type {Asset, Dependency, MutableBundleGraph} from '@atlaspack/types';
1
+ import type {
2
+ Asset,
3
+ Dependency,
4
+ MutableBundleGraph,
5
+ } from '@atlaspack/types-internal';
6
+ import {getFeatureFlag} from '@atlaspack/feature-flags';
7
+
3
8
  import nullthrows from 'nullthrows';
4
9
 
5
10
  export function addJSMonolithBundle(
@@ -13,7 +18,11 @@ export function addJSMonolithBundle(
13
18
  );
14
19
 
15
20
  // Create a single bundle to hold all JS assets
16
- let bundle = bundleGraph.createBundle({entryAsset, target});
21
+ let bundle = bundleGraph.createBundle({
22
+ entryAsset,
23
+ target,
24
+ needsStableName: getFeatureFlag('singleFileOutputStableName'),
25
+ });
17
26
 
18
27
  bundleGraph.traverse(
19
28
  (node, _, actions) => {
@@ -39,9 +48,12 @@ export function addJSMonolithBundle(
39
48
  let assets = bundleGraph.getDependencyAssets(dependency);
40
49
 
41
50
  for (const asset of assets) {
42
- if (asset.bundleBehavior === 'isolated') {
51
+ if (
52
+ asset.bundleBehavior === 'isolated' ||
53
+ asset.bundleBehavior === 'inlineIsolated'
54
+ ) {
43
55
  throw new Error(
44
- 'Isolated assets are not supported for single file output builds',
56
+ `${asset.bundleBehavior === 'isolated' ? 'Isolated' : 'Inline isolated'} assets are not supported for single file output builds`,
45
57
  );
46
58
  }
47
59
 
@@ -0,0 +1,250 @@
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
+ const node = bundleGraph.getNode(nodeId);
16
+ if (
17
+ node &&
18
+ (node === 'root' ||
19
+ (node.bundleBehavior !== 'inline' &&
20
+ node.bundleBehavior !== 'inlineIsolated'))
21
+ ) {
22
+ count++;
23
+ }
24
+ }, bundleGroupId);
25
+ return count;
26
+ }
27
+
28
+ let getBundleOverlap = (
29
+ sourceBundlesA: Set<NodeId>,
30
+ sourceBundlesB: Set<NodeId>,
31
+ ): number => {
32
+ let allSourceBundles = setUnion(sourceBundlesA, sourceBundlesB);
33
+ let sharedSourceBundles = setIntersectStatic(sourceBundlesA, sourceBundlesB);
34
+
35
+ return sharedSourceBundles.size / allSourceBundles.size;
36
+ };
37
+
38
+ // Returns a decimal showing the proportion source bundles are common to
39
+ // both bundles versus the total number of source bundles.
40
+ function checkBundleThreshold(
41
+ bundleA: MergeCandidate,
42
+ bundleB: MergeCandidate,
43
+ threshold: number,
44
+ ): boolean {
45
+ return (
46
+ getBundleOverlap(
47
+ bundleA.bundle.sourceBundles,
48
+ bundleB.bundle.sourceBundles,
49
+ ) >= threshold
50
+ );
51
+ }
52
+
53
+ let checkSharedSourceBundles = memoize(
54
+ (bundle: Bundle, importantAncestorBundles: Array<NodeId>): boolean => {
55
+ return importantAncestorBundles.every((ancestorId) =>
56
+ bundle.sourceBundles.has(ancestorId),
57
+ );
58
+ },
59
+ );
60
+
61
+ let hasSuitableBundleGroup = memoize(
62
+ (
63
+ bundleGraph: IdealBundleGraph,
64
+ bundle: Bundle,
65
+ minBundlesInGroup: number,
66
+ ): boolean => {
67
+ for (let sourceBundle of bundle.sourceBundles) {
68
+ let bundlesInGroup = getBundlesForBundleGroup(bundleGraph, sourceBundle);
69
+
70
+ if (bundlesInGroup >= minBundlesInGroup) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ },
76
+ );
77
+
78
+ function validMerge(
79
+ bundleGraph: IdealBundleGraph,
80
+ config: MergeGroup,
81
+ bundleA: MergeCandidate,
82
+ bundleB: MergeCandidate,
83
+ ): boolean {
84
+ if (config.maxBundleSize != null) {
85
+ if (
86
+ bundleA.bundle.size > config.maxBundleSize ||
87
+ bundleB.bundle.size > config.maxBundleSize
88
+ ) {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ if (config.overlapThreshold != null) {
94
+ if (!checkBundleThreshold(bundleA, bundleB, config.overlapThreshold)) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ if (config.sourceBundles != null) {
100
+ if (
101
+ !checkSharedSourceBundles(bundleA.bundle, config.sourceBundles) ||
102
+ !checkSharedSourceBundles(bundleB.bundle, config.sourceBundles)
103
+ ) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ if (config.minBundlesInGroup != null) {
109
+ if (
110
+ !hasSuitableBundleGroup(
111
+ bundleGraph,
112
+ bundleA.bundle,
113
+ config.minBundlesInGroup,
114
+ ) ||
115
+ !hasSuitableBundleGroup(
116
+ bundleGraph,
117
+ bundleB.bundle,
118
+ config.minBundlesInGroup,
119
+ )
120
+ ) {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ function getMergeClusters(
129
+ graph: ContentGraph<NodeId, EdgeType>,
130
+ candidates: Map<NodeId, EdgeType>,
131
+ ): Array<Array<NodeId>> {
132
+ let clusters: Array<Array<NodeId>> = [];
133
+
134
+ for (let [candidate, edgeType] of candidates.entries()) {
135
+ let cluster: Array<NodeId> = [];
136
+
137
+ graph.traverse(
138
+ (nodeId) => {
139
+ cluster.push(nullthrows(graph.getNode(nodeId)));
140
+ // Remove node from candidates as it has already been processed
141
+ candidates.delete(nodeId);
142
+ },
143
+ candidate,
144
+ edgeType,
145
+ );
146
+ clusters.push(cluster);
147
+ }
148
+
149
+ return clusters;
150
+ }
151
+
152
+ type MergeCandidate = {
153
+ bundle: Bundle;
154
+ id: NodeId;
155
+ contentKey: string;
156
+ };
157
+ function getPossibleMergeCandidates(
158
+ bundleGraph: IdealBundleGraph,
159
+ bundles: Array<NodeId>,
160
+ ): Array<[MergeCandidate, MergeCandidate]> {
161
+ let mergeCandidates = bundles.map((bundleId) => {
162
+ let bundle = bundleGraph.getNode(bundleId);
163
+ invariant(bundle && bundle !== 'root', 'Bundle should exist');
164
+
165
+ return {
166
+ id: bundleId,
167
+ bundle,
168
+ contentKey: bundleId.toString(),
169
+ };
170
+ });
171
+
172
+ const uniquePairs: Array<[MergeCandidate, MergeCandidate]> = [];
173
+
174
+ for (let i = 0; i < mergeCandidates.length; i++) {
175
+ for (let j = i + 1; j < mergeCandidates.length; j++) {
176
+ let a = mergeCandidates[i];
177
+ let b = mergeCandidates[j];
178
+
179
+ // @ts-expect-error TS18048
180
+ if (a.bundle.internalizedAssets.equals(b.bundle.internalizedAssets)) {
181
+ uniquePairs.push([a, b]);
182
+ }
183
+ }
184
+ }
185
+ return uniquePairs;
186
+ }
187
+
188
+ export type MergeGroup = {
189
+ overlapThreshold?: number;
190
+ maxBundleSize?: number;
191
+ sourceBundles?: Array<NodeId>;
192
+ minBundlesInGroup?: number;
193
+ };
194
+ type EdgeType = number;
195
+
196
+ export function findMergeCandidates(
197
+ bundleGraph: IdealBundleGraph,
198
+ bundles: Array<NodeId>,
199
+ config: Array<MergeGroup>,
200
+ ): Array<Array<NodeId>> {
201
+ let graph = new ContentGraph<NodeId, EdgeType>();
202
+ let candidates = new Map<NodeId, EdgeType>();
203
+
204
+ let allPossibleMergeCandidates = getPossibleMergeCandidates(
205
+ bundleGraph,
206
+ bundles,
207
+ );
208
+
209
+ // Build graph of clustered merge candidates
210
+ for (let i = 0; i < config.length; i++) {
211
+ // Ensure edge type coresponds to config index
212
+ let edgeType = i + 1;
213
+
214
+ for (let group of allPossibleMergeCandidates) {
215
+ let candidateA = group[0];
216
+ let candidateB = group[1];
217
+
218
+ if (!validMerge(bundleGraph, config[i], candidateA, candidateB)) {
219
+ continue;
220
+ }
221
+
222
+ let bundleNode = graph.addNodeByContentKeyIfNeeded(
223
+ candidateA.contentKey,
224
+ candidateA.id,
225
+ );
226
+ let otherBundleNode = graph.addNodeByContentKeyIfNeeded(
227
+ candidateB.contentKey,
228
+ candidateB.id,
229
+ );
230
+
231
+ // Add edge in both directions
232
+ graph.addEdge(bundleNode, otherBundleNode, edgeType);
233
+ graph.addEdge(otherBundleNode, bundleNode, edgeType);
234
+
235
+ candidates.set(bundleNode, edgeType);
236
+ candidates.set(otherBundleNode, edgeType);
237
+ }
238
+
239
+ // Remove bundles that have been allocated to a higher priority merge
240
+ allPossibleMergeCandidates = allPossibleMergeCandidates.filter(
241
+ (group) =>
242
+ !graph.hasContentKey(group[0].contentKey) &&
243
+ !graph.hasContentKey(group[1].contentKey),
244
+ );
245
+ }
246
+
247
+ clearCaches();
248
+
249
+ return getMergeClusters(graph, candidates);
250
+ }