Skip to content

Commit b28699c

Browse files
authored
Merge pull request #325 from ef4/discovery-protocol
updating discovery protocol
2 parents bce9902 + ec85ecc commit b28699c

File tree

9 files changed

+1402
-161
lines changed

9 files changed

+1402
-161
lines changed

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@
4141
"name": "Build sample-merged",
4242
"program": "${workspaceFolder}/node_modules/.bin/ember",
4343
"cwd": "${workspaceFolder}/packages/sample-merged",
44-
"preLaunchTask": "tsc: build - tsconfig.json",
4544
"args": [
4645
"build"
47-
]
46+
],
47+
"outputCapture": "std"
4848
},
4949
{
5050
"type": "node",

packages/ember-auto-import/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"pkg-up": "^2.0.0",
5353
"resolve": "^1.7.1",
5454
"rimraf": "^2.6.2",
55+
"semver": "^7.3.4",
5556
"symlink-or-copy": "^1.2.0",
5657
"typescript-memoize": "^1.0.0-alpha.3",
5758
"walk-sync": "^0.3.3",
@@ -73,6 +74,7 @@
7374
"@types/node": "^10.7.1",
7475
"@types/pkg-up": "^2.0.0",
7576
"@types/resolve": "^0.0.8",
77+
"@types/semver": "^7.3.4",
7678
"@types/webpack": "^4.4.20",
7779
"babel-eslint": "^8.2.5",
7880
"broccoli": "^3.2.0",

packages/ember-auto-import/ts/auto-import.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,59 @@ import { buildDebugCallback } from 'broccoli-debug';
66
import BundleConfig from './bundle-config';
77
import Append from './broccoli-append';
88
import { Node } from 'broccoli-node-api';
9+
import { LeaderChooser } from './leader';
10+
import { AddonInstance, AppInstance, findTopmostAddon } from './ember-cli-models';
911

1012
const debugTree = buildDebugCallback('ember-auto-import');
11-
const protocol = '__ember_auto_import_protocol_v1__';
1213

13-
export default class AutoImport {
14-
private primaryPackage: any;
14+
// This interface must be stable across all versions of ember-auto-import that
15+
// speak the same leader-election protocol. So don't change this unless you know
16+
// what you're doing.
17+
export interface AutoImportSharedAPI {
18+
isPrimary(addonInstance: AddonInstance): boolean;
19+
analyze(tree: Node, addon: AddonInstance): Node;
20+
included(addonInstance: AddonInstance): void;
21+
updateFastBootManifest(manifest: { vendorFiles: string[] }): void;
22+
}
23+
24+
export default class AutoImport implements AutoImportSharedAPI {
25+
private primaryPackage: AddonInstance;
1526
private packages: Set<Package> = new Set();
1627
private env: 'development' | 'test' | 'production';
1728
private consoleWrite: (msg: string) => void;
1829
private analyzers: Map<Analyzer, Package> = new Map();
1930
private bundles: BundleConfig;
2031
private targets: unknown;
2132

22-
static lookup(appOrAddon: any): AutoImport {
23-
let g = global as any;
24-
if (!g[protocol]) {
25-
g[protocol] = new this(appOrAddon);
26-
}
27-
return g[protocol];
33+
static register(addon: AddonInstance) {
34+
LeaderChooser.for(addon).register(addon, () => new AutoImport(addon));
2835
}
2936

30-
constructor(appOrAddon: any) {
31-
function findHostContext(appOrAddon: any): any {
32-
return appOrAddon.parent.parent
33-
? findHostContext(appOrAddon.parent)
34-
: appOrAddon;
35-
}
37+
static lookup(addon: AddonInstance): AutoImportSharedAPI {
38+
return LeaderChooser.for(addon).leader;
39+
}
3640

37-
this.primaryPackage = appOrAddon;
38-
let hostContext = findHostContext(appOrAddon);
39-
this.packages.add(Package.lookup(hostContext));
40-
let host = hostContext.app;
41+
constructor(addonInstance: AddonInstance) {
42+
this.primaryPackage = addonInstance;
43+
let topmostAddon = findTopmostAddon(addonInstance);
44+
this.packages.add(Package.lookupParentOf(topmostAddon));
45+
let host = topmostAddon.app;
4146
this.env = host.env;
4247
this.targets = host.project.targets;
4348
this.bundles = new BundleConfig(host);
4449
if (!this.env) {
4550
throw new Error('Bug in ember-auto-import: did not discover environment');
4651
}
4752

48-
this.consoleWrite = (...args) => appOrAddon.project.ui.write(...args);
53+
this.consoleWrite = (...args) => addonInstance.project.ui.write(...args);
4954
}
5055

51-
isPrimary(appOrAddon: any) {
52-
return this.primaryPackage === appOrAddon;
56+
isPrimary(addon: AddonInstance) {
57+
return this.primaryPackage === addon;
5358
}
5459

55-
analyze(tree: Node, appOrAddon: any) {
56-
let pack = Package.lookup(appOrAddon);
60+
analyze(tree: Node, addon: AddonInstance) {
61+
let pack = Package.lookupParentOf(addon);
5762
this.packages.add(pack);
5863
let analyzer = new Analyzer(
5964
debugTree(tree, `preprocessor:input-${this.analyzers.size}`),
@@ -63,7 +68,7 @@ export default class AutoImport {
6368
return analyzer;
6469
}
6570

66-
makeBundler(allAppTree: Node) {
71+
private makeBundler(allAppTree: Node) {
6772
// The Splitter takes the set of imports from the Analyzer and
6873
// decides which ones to include in which bundles
6974
let splitter = new Splitter({
@@ -105,8 +110,8 @@ export default class AutoImport {
105110
});
106111
}
107112

108-
included(addonInstance: any) {
109-
let host = addonInstance._findHost();
113+
included(addonInstance: AddonInstance) {
114+
let host = findTopmostAddon(addonInstance).app;
110115
this.configureFingerprints(host);
111116

112117
// ember-cli as of 3.4-beta has introduced architectural changes that make
@@ -132,7 +137,7 @@ export default class AutoImport {
132137
// We need to disable fingerprinting of chunks, because (1) they already
133138
// have their own webpack-generated hashes and (2) the runtime loader code
134139
// can't easily be told about broccoli-asset-rev's hashes.
135-
private configureFingerprints(host: any) {
140+
private configureFingerprints(host: AppInstance) {
136141
let pattern = 'assets/chunk.*.js';
137142
if (!host.options.fingerprint) {
138143
host.options.fingerprint = {};

packages/ember-auto-import/ts/bundle-config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
*/
55

66
import { dirname } from 'path';
7+
import { AppInstance } from './ember-cli-models';
78
const testsPattern = new RegExp(`^/?[^/]+/(tests|test-support)/`);
89

910
export default class BundleConfig {
10-
constructor(private emberApp: any) {}
11+
constructor(private emberApp: AppInstance) {}
1112

1213
// This list of valid bundles, in priority order. The first one in the list that
1314
// needs a given import will end up with that import.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Node } from 'broccoli-node-api';
2+
export interface Project {
3+
targets: unknown;
4+
ui: {
5+
write(...args: any[]): void;
6+
};
7+
pkg: { name: string; version: string };
8+
root: string;
9+
addons: AddonInstance[];
10+
}
11+
12+
export interface AppInstance {
13+
env: "development" | "test" | "production";
14+
project: Project;
15+
options: any;
16+
addonPostprocessTree: (which: string, tree: Node) => Node;
17+
}
18+
19+
interface BaseAddonInstance {
20+
project: Project;
21+
pkg: { name: string; version: string };
22+
root: string;
23+
options: any;
24+
addons: AddonInstance[];
25+
name: string;
26+
}
27+
28+
export interface DeepAddonInstance extends BaseAddonInstance {
29+
// this is how it looks when an addon is beneath another addon
30+
parent: AddonInstance;
31+
}
32+
33+
export interface ShallowAddonInstance extends BaseAddonInstance {
34+
// this is how it looks when an addon is directly beneath the app
35+
parent: Project;
36+
app: AppInstance;
37+
}
38+
39+
export type AddonInstance = DeepAddonInstance | ShallowAddonInstance;
40+
41+
export function isDeepAddonInstance(
42+
addon: AddonInstance
43+
): addon is DeepAddonInstance {
44+
return addon.parent !== addon.project;
45+
}
46+
47+
export function findTopmostAddon(addon: AddonInstance): ShallowAddonInstance {
48+
if (isDeepAddonInstance(addon)) {
49+
return findTopmostAddon(addon.parent);
50+
} else {
51+
return addon;
52+
}
53+
}

packages/ember-auto-import/ts/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import AutoImport from './auto-import';
22
import { Node } from 'broccoli-node-api';
3+
// @ts-ignore
4+
import pkg from '../package';
35

46
module.exports = {
5-
name: 'ember-auto-import',
7+
name: pkg.name,
8+
9+
init(...args: any[]) {
10+
this._super.init.apply(this, args);
11+
AutoImport.register(this);
12+
},
613

714
setupPreprocessorRegistry(type: string, registry: any) {
815
// we register on our parent registry (so we will process code
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { gt } from "semver";
2+
import type AutoImport from "./auto-import";
3+
import { Project, AddonInstance } from "./ember-cli-models";
4+
import { Node } from "broccoli-node-api";
5+
6+
const protocolV1 = "__ember_auto_import_protocol_v1__";
7+
const protocolV2 = "__ember_auto_import_protocol_v2__";
8+
const g = (global as any) as {
9+
[protocolV1]: any;
10+
[protocolV2]: WeakMap<Project, LeaderChooser> | undefined;
11+
};
12+
13+
export class LeaderChooser {
14+
static for(addon: AddonInstance): LeaderChooser {
15+
let g = global as any;
16+
let map: WeakMap<Project, LeaderChooser> = g[protocolV2];
17+
if (!map) {
18+
map = g[protocolV2] = new WeakMap();
19+
}
20+
// this needs to be based on project and not app instance because at the
21+
// early stage where we're doing `register`, the app instance isn't
22+
// available on the addons yet
23+
let project = addon.project;
24+
let chooser = map.get(project);
25+
if (!chooser) {
26+
chooser = new this();
27+
map.set(project, chooser);
28+
}
29+
return chooser;
30+
}
31+
32+
private tentative: { create: () => AutoImport; version: string } | undefined;
33+
private locked: AutoImport | undefined;
34+
35+
register(addon: AddonInstance, create: () => AutoImport) {
36+
if (this.locked) {
37+
throw new Error(`bug: LeaderChooser already locked`);
38+
}
39+
let version = addon.pkg.version;
40+
if (!this.tentative || gt(version, this.tentative.version)) {
41+
this.tentative = { create, version };
42+
}
43+
}
44+
45+
get leader(): AutoImport {
46+
if (!this.locked) {
47+
if (!this.tentative) {
48+
throw new Error(`bug: no candidates added`);
49+
}
50+
this.locked = this.tentative.create();
51+
let v1 = g[protocolV1];
52+
if (v1?.isV1Placeholder) {
53+
v1.leader = this.locked;
54+
}
55+
}
56+
return this.locked;
57+
}
58+
}
59+
60+
class V1Placeholder {
61+
isV1Placeholder = true;
62+
leader: AutoImport | undefined;
63+
64+
// we never want v1-speaking copies of ember-auto-import to consider
65+
// themselves primary, so if they're asking here, the answer is no.
66+
isPrimary() {
67+
return false;
68+
}
69+
70+
// this is the only method that is called after isPrimary returns false. So we
71+
// need to implement this one and don't need to implement the other public API
72+
// of AutoImport.
73+
analyze(tree: Node, addon: AddonInstance) {
74+
if (!this.leader) {
75+
throw new Error(
76+
`bug: expected some protcol v2 copy of ember-auto-import to take charge before any v1 copy started trying to analyze trees`
77+
);
78+
}
79+
return this.leader.analyze(tree, addon);
80+
}
81+
}
82+
83+
// at module load time, preempt all earlier versions of ember-auto-import that
84+
// don't use our v2 leadership protocol. This ensures that the v2 protocol will
85+
// pick which version is in charge (and v1-speaking copies won't be eligible).
86+
(function v1ProtocolCompat() {
87+
let v1 = g[protocolV1];
88+
if (v1) {
89+
if (!v1.isV1Placeholder) {
90+
throw new Error(
91+
`bug: an old version of ember-auto-import has already taken over. This is unexpected.`
92+
);
93+
}
94+
} else {
95+
g[protocolV1] = new V1Placeholder;
96+
}
97+
})();

0 commit comments

Comments
 (0)