@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.
- package/CHANGELOG.md +601 -0
- package/dist/DefaultBundler.js +84 -0
- package/dist/MonolithicBundler.js +68 -0
- package/dist/bundleMerge.js +137 -0
- package/dist/bundlerConfig.js +223 -0
- package/dist/decorateLegacyGraph.js +189 -0
- package/dist/idealGraph.js +1471 -0
- package/dist/memoize.js +31 -0
- package/dist/stats.js +69 -0
- package/lib/DefaultBundler.js +6 -1
- package/lib/MonolithicBundler.js +11 -3
- package/lib/bundleMerge.js +106 -37
- package/lib/bundlerConfig.js +52 -6
- package/lib/decorateLegacyGraph.js +24 -3
- package/lib/idealGraph.js +410 -55
- package/lib/memoize.js +39 -0
- package/lib/stats.js +85 -0
- package/lib/types/DefaultBundler.d.ts +18 -0
- package/lib/types/MonolithicBundler.d.ts +2 -0
- package/lib/types/bundleMerge.d.ts +9 -0
- package/lib/types/bundlerConfig.d.ts +36 -0
- package/lib/types/decorateLegacyGraph.d.ts +3 -0
- package/lib/types/idealGraph.d.ts +40 -0
- package/lib/types/memoize.d.ts +2 -0
- package/lib/types/stats.d.ts +16 -0
- package/package.json +20 -12
- package/src/{DefaultBundler.js → DefaultBundler.ts} +21 -6
- package/src/{MonolithicBundler.js → MonolithicBundler.ts} +17 -5
- package/src/bundleMerge.ts +250 -0
- package/src/{bundlerConfig.js → bundlerConfig.ts} +106 -45
- package/src/{decorateLegacyGraph.js → decorateLegacyGraph.ts} +26 -7
- package/src/{idealGraph.js → idealGraph.ts} +729 -137
- package/src/memoize.ts +32 -0
- package/src/stats.ts +97 -0
- package/tsconfig.json +30 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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,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,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,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.
|
|
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.
|
|
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.
|
|
20
|
-
"@atlaspack/feature-flags": "2.14.1-canary.
|
|
21
|
-
"@atlaspack/graph": "3.4.1-canary.
|
|
22
|
-
"@atlaspack/plugin": "2.14.5-canary.
|
|
23
|
-
"@atlaspack/rust": "3.2.1-canary.
|
|
24
|
-
"@atlaspack/
|
|
25
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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
|
-
})
|
|
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(
|
|
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
|
-
|
|
2
|
-
|
|
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({
|
|
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 (
|
|
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
|
+
}
|