-
Notifications
You must be signed in to change notification settings - Fork 137
/
index.ts
211 lines (191 loc) · 6.37 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { resolve, relative, isAbsolute } from 'path';
import { readFileSync } from 'fs';
import {
AddonMeta,
AddonInstance,
isDeepAddonInstance,
PackageInfo,
} from '@embroider/shared-internals';
import buildFunnel from 'broccoli-funnel';
import type { Node } from 'broccoli-node-api';
import { satisfies } from 'semver';
export interface ShimOptions {
disabled?: (options: any) => boolean;
}
function addonMeta(pkgJSON: PackageInfo): AddonMeta {
let meta = pkgJSON['ember-addon'];
if (meta?.version !== 2 || meta?.type !== 'addon') {
throw new Error(`did not find valid v2 addon metadata in ${pkgJSON.name}`);
}
return meta as AddonMeta;
}
export function addonV1Shim(directory: string, options: ShimOptions = {}) {
let pkg: PackageInfo = JSON.parse(
readFileSync(resolve(directory, './package.json'), 'utf8')
);
let meta = addonMeta(pkg);
let disabled = false;
const rootTrees = new WeakMap<AddonInstance, Node>();
function rootTree(addonInstance: AddonInstance): Node {
let tree = rootTrees.get(addonInstance);
if (!tree) {
tree = addonInstance.treeGenerator(directory);
rootTrees.set(addonInstance, tree);
}
return tree;
}
let autoImportInstance: EAI2Instance | undefined;
return {
name: pkg.name,
included(this: AddonInstance, ...args: unknown[]) {
let parentOptions;
let parentName: string;
if (isDeepAddonInstance(this)) {
parentOptions = this.parent.options;
parentName = this.parent.name;
} else {
parentOptions = this.app.options;
parentName = this.parent.name();
}
// if we're being used by a v1 package, that package needs ember-auto-import 2
if ((this.parent.pkg['ember-addon']?.version ?? 1) < 2) {
let autoImport = locateAutoImport(this.parent.addons);
if (!autoImport.present) {
throw new Error(
`${parentName} needs to depend on ember-auto-import in order to use ${this.name}`
);
}
if (!autoImport.satisfiesV2) {
throw new Error(
`${parentName} has ember-auto-import ${autoImport.version} which is not new enough to use ${this.name}. It needs to upgrade to >=2.0`
);
}
autoImportInstance = autoImport.instance;
autoImportInstance.registerV2Addon(this.name, directory);
} else {
// if we're being used by a v2 addon, it also has this shim and will
// forward our registration onward to ember-auto-import
(this.parent as EAI2Instance).registerV2Addon(this.name, directory);
}
if (options.disabled) {
disabled = options.disabled(parentOptions);
}
// this is at the end so we can find our own autoImportInstance before any
// deeper v2 addons ask us to forward registrations upward to it
this._super.included.apply(this, args);
},
treeForApp(this: AddonInstance) {
if (disabled) {
return undefined;
}
let maybeAppJS = meta['app-js'];
if (maybeAppJS) {
const appJS = maybeAppJS;
return buildFunnel(rootTree(this), {
files: Object.values(appJS),
getDestinationPath(relativePath: string): string {
for (let [exteriorName, interiorName] of Object.entries(appJS)) {
if (relativePath === interiorName) {
return exteriorName;
}
}
throw new Error(
`bug in addonV1Shim, no match for ${relativePath} in ${JSON.stringify(
appJS
)}`
);
},
});
}
},
treeForAddon() {
// this never goes through broccoli -- it's always pulled into the app via
// ember-auto-import, as needed. This means it always benefits from
// tree-shaking.
return undefined;
},
treeForPublic(this: AddonInstance) {
if (disabled) {
return undefined;
}
let maybeAssets = meta['public-assets'];
if (maybeAssets) {
const assets = maybeAssets;
return buildFunnel(rootTree(this), {
files: Object.keys(assets),
getDestinationPath(relativePath: string): string {
for (let [interiorName, exteriorName] of Object.entries(assets)) {
if (relativePath === interiorName) {
return exteriorName;
}
}
throw new Error(
`bug in addonV1Shim, no match for ${relativePath} in ${JSON.stringify(
assets
)}`
);
},
});
}
},
cacheKeyForTree(this: AddonInstance, treeType: string): string {
return `embroider-addon-shim/${treeType}/${directory}`;
},
isDevelopingAddon(this: AddonInstance) {
// if the app is inside our own directory, we must be under development.
// This setting controls whether ember-cli will watch for changes in the
// broccoli trees we expose, but it doesn't have any control over our
// files that get auto-imported into the app. For that, you should use
// ember-auto-import's watchDependencies option (and this should become
// part of the blueprint for test apps).
let appInstance = this._findHost();
return isInside(directory, appInstance.project.root);
},
registerV2Addon(name: string, root: string): void {
autoImportInstance!.registerV2Addon(name, root);
},
};
}
function isInside(parentDir: string, otherDir: string): boolean {
let rel = relative(parentDir, otherDir);
return Boolean(rel) && !rel.startsWith('..') && !isAbsolute(rel);
}
type EAI2Instance = AddonInstance & {
registerV2Addon(name: string, root: string): void;
};
function locateAutoImport(addons: AddonInstance[]):
| { present: false }
| {
present: true;
version: string;
satisfiesV2: false;
}
| {
present: true;
version: string;
satisfiesV2: true;
instance: EAI2Instance;
} {
let instance = addons.find((a) => a.name === 'ember-auto-import');
if (!instance) {
return { present: false };
}
let version = instance.pkg.version;
let satisfiesV2 = satisfies(version, '>=2.0.0-alpha.0', {
includePrerelease: true,
});
if (satisfiesV2) {
return {
present: true,
version,
satisfiesV2,
instance: instance as EAI2Instance,
};
} else {
return {
present: true,
version,
satisfiesV2,
};
}
}