ember.nvp 1.13.0 → 1.15.0
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/.release-plan.json +3 -3
- package/CHANGELOG.md +22 -0
- package/README.md +28 -0
- package/package.json +2 -1
- package/src/cli/generator.js +10 -0
- package/src/cli/index.js +19 -2
- package/src/cli/questions/layers.js +3 -2
- package/src/cli/questions/migration.js +69 -0
- package/src/index.js +1 -0
- package/src/layers/qunit/index.js +55 -1
- package/src/layers/qunit/library-ts-files/config/tsconfig.publish.json +8 -0
- package/src/migration-from/addons.js +154 -0
- package/src/migration-from/app.js +1117 -0
- package/src/migration-from/at-ember/addon-blueprint/index.js +515 -0
- package/src/migration-from/at-ember/addon-blueprint/rollup.js +188 -0
- package/src/migration-from/at-ember/app-blueprint/index.js +60 -0
- package/src/migration-from/at-ember-tooling/classic-build-addon-blueprint/index.js +601 -0
- package/src/migration-from/at-ember-tooling/classic-build-app-blueprint/index.js +118 -0
- package/src/migration-from/babel.js +207 -0
- package/src/migration-from/ember-cli-build.js +238 -0
- package/src/migration-from/files.js +120 -0
- package/src/migration-from/findings.js +69 -0
- package/src/migration-from/index.js +81 -0
- package/src/migration-from/javascript.js +217 -0
- package/src/migration-from/layers.js +170 -0
- package/src/migration-from/library.js +311 -0
- package/src/utils/types.ts +69 -0
- package/test/ember.nvp/layer-qunit.test.ts +53 -6
- package/test/ember.nvp/migration-from-fixtures.ts +506 -0
- package/test/ember.nvp/migration-from.test.ts +475 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readJSON } from "#utils/fs.js";
|
|
4
|
+
import { checkApp, migrateApp } from "../../app.js";
|
|
5
|
+
import { EMBER_CLI_BUILD } from "../../ember-cli-build.js";
|
|
6
|
+
import { firstExisting } from "../../files.js";
|
|
7
|
+
|
|
8
|
+
const VITE_CONFIGS = ["vite.config.mjs", "vite.config.js", "vite.config.ts", "vite.config.mts"];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An app that builds with Vite and `@embroider/vite`.
|
|
12
|
+
*
|
|
13
|
+
* This covers `@ember/app-blueprint`, and its predecessor `@embroider/app-blueprint`.
|
|
14
|
+
* Both keep an ember-cli-build for `@embroider/compat`,
|
|
15
|
+
* which lets v1 addons, `.hbs` templates, and the classic resolver keep working.
|
|
16
|
+
*
|
|
17
|
+
* ember.nvp apps build without compat.
|
|
18
|
+
* So the migration stops at each compat feature that the app still uses,
|
|
19
|
+
* and lists them for the user.
|
|
20
|
+
*
|
|
21
|
+
* @type {import('#types').Migration}
|
|
22
|
+
*/
|
|
23
|
+
export default {
|
|
24
|
+
label: "@ember/app-blueprint",
|
|
25
|
+
type: "app",
|
|
26
|
+
|
|
27
|
+
detect(directory) {
|
|
28
|
+
let manifest = readJSON(join(directory, "package.json"));
|
|
29
|
+
|
|
30
|
+
// v2 addons build their tests and demo apps with @embroider/vite too
|
|
31
|
+
if (
|
|
32
|
+
manifest?.keywords?.includes("ember-addon") ||
|
|
33
|
+
manifest?.["ember-addon"]?.type === "addon"
|
|
34
|
+
) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!existsSync(join(directory, "app"))) return false;
|
|
39
|
+
|
|
40
|
+
let build = firstExisting(directory, EMBER_CLI_BUILD);
|
|
41
|
+
|
|
42
|
+
if (build && readFileSync(join(directory, build), "utf-8").includes("@embroider/vite")) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let vite = firstExisting(directory, VITE_CONFIGS);
|
|
47
|
+
|
|
48
|
+
return Boolean(
|
|
49
|
+
vite && readFileSync(join(directory, vite), "utf-8").includes("@embroider/vite"),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async check(project) {
|
|
54
|
+
return checkApp(project);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async run(project, report) {
|
|
58
|
+
await migrateApp(project, report, { indexHtml: "index.html" });
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { readJSON } from "#utils/fs.js";
|
|
6
|
+
import { getLatest } from "#utils/npm.js";
|
|
7
|
+
import { rewriteImportsToMatchFiles } from "#utils/rewrite-imports.js";
|
|
8
|
+
import { installedDependencies, v1AddonsIn } from "../../addons.js";
|
|
9
|
+
import { TOOLCHAIN } from "../../app.js";
|
|
10
|
+
import {
|
|
11
|
+
filesWithExtension,
|
|
12
|
+
firstExisting,
|
|
13
|
+
listFiles,
|
|
14
|
+
move,
|
|
15
|
+
remove,
|
|
16
|
+
testsWithHbs,
|
|
17
|
+
} from "../../files.js";
|
|
18
|
+
import { parseProgram, propertyNames, propertyValue, stringValue, walk } from "../../javascript.js";
|
|
19
|
+
import { changedTestemFlags, removeToolingOfWantedLayers } from "../../layers.js";
|
|
20
|
+
import {
|
|
21
|
+
buildWithTsdown,
|
|
22
|
+
entriesFor,
|
|
23
|
+
renameCommonJSFiles,
|
|
24
|
+
useEmberSourceForTests,
|
|
25
|
+
usesCss,
|
|
26
|
+
} from "../../library.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {import('#types').MigrationReport} MigrationReport
|
|
30
|
+
* @typedef {import('#utils/project.js').Project} Project
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Keys of the addon object in index.js that a v2 addon can do without
|
|
35
|
+
*/
|
|
36
|
+
const HARMLESS_ADDON_KEYS = new Set(["name", "moduleName", "isDevelopingAddon", "options"]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `options` in index.js that only configure ember-cli-babel
|
|
40
|
+
*/
|
|
41
|
+
const HARMLESS_ADDON_OPTIONS = new Set(["ember-cli-babel"]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A v1 addon: ember-cli loads its index.js, and merges addon/ and app/ into the app.
|
|
45
|
+
*
|
|
46
|
+
* This covers `@ember-tooling/classic-build-addon-blueprint`,
|
|
47
|
+
* and the `addon` blueprint that ember-cli had before it.
|
|
48
|
+
*
|
|
49
|
+
* The migration makes a v2 addon that builds with tsdown:
|
|
50
|
+
* - addon/ moves to src/, and addon-test-support/ to src/test-support/
|
|
51
|
+
* - app/ re-exports come back from `appReexports(...)` in tsdown.config.js
|
|
52
|
+
* - addon-main.cjs lets apps with ember-cli keep using the addon
|
|
53
|
+
*
|
|
54
|
+
* @type {import('#types').Migration}
|
|
55
|
+
*/
|
|
56
|
+
export default {
|
|
57
|
+
label: "@ember-tooling/classic-build-addon-blueprint",
|
|
58
|
+
type: "library",
|
|
59
|
+
|
|
60
|
+
detect(directory) {
|
|
61
|
+
let manifest = readJSON(join(directory, "package.json"));
|
|
62
|
+
|
|
63
|
+
if (!manifest?.keywords?.includes("ember-addon")) return false;
|
|
64
|
+
if (manifest["ember-addon"]?.version === 2) return false;
|
|
65
|
+
|
|
66
|
+
return existsSync(join(directory, manifest["ember-addon"]?.main ?? "index.js"));
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async check(project) {
|
|
70
|
+
/** @type {MigrationReport} */
|
|
71
|
+
let report = { unsupported: [], todo: [], layers: [] };
|
|
72
|
+
let root = project.directory;
|
|
73
|
+
let manifest = await packageJson.read(root);
|
|
74
|
+
let main = manifest["ember-addon"]?.main ?? "index.js";
|
|
75
|
+
|
|
76
|
+
let hooks = addonHooks(root, main);
|
|
77
|
+
|
|
78
|
+
if (hooks.length > 0) {
|
|
79
|
+
report.unsupported.push({
|
|
80
|
+
title: "ember-cli hooks in the addon's main file",
|
|
81
|
+
where: hooks,
|
|
82
|
+
action:
|
|
83
|
+
"Move what each hook does into the addon's modules, or into setup steps for apps.\n" +
|
|
84
|
+
"A v2 addon has no build hooks.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let templates = filesWithExtension(root, ["addon", "app"], [".hbs"]);
|
|
89
|
+
|
|
90
|
+
if (templates.length > 0) {
|
|
91
|
+
report.unsupported.push({
|
|
92
|
+
title: "Templates in .hbs files",
|
|
93
|
+
where: templates,
|
|
94
|
+
action:
|
|
95
|
+
"Move each template into its component as <template>, in a .gjs or .gts file.\n" +
|
|
96
|
+
"The build compiles <template> only.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let styles = listFiles(root, "addon/styles").concat(listFiles(root, "app/styles"));
|
|
101
|
+
|
|
102
|
+
if (styles.length > 0) {
|
|
103
|
+
report.unsupported.push({
|
|
104
|
+
title: "Styles that ember-cli merges into the app",
|
|
105
|
+
where: styles,
|
|
106
|
+
action:
|
|
107
|
+
"Import each stylesheet from the modules that need it, such as `import './button.css';`.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let assets = listFiles(root, "public")
|
|
112
|
+
.concat(listFiles(root, "vendor"))
|
|
113
|
+
.filter((file) => !file.endsWith(".gitkeep"));
|
|
114
|
+
|
|
115
|
+
if (assets.length > 0) {
|
|
116
|
+
report.unsupported.push({
|
|
117
|
+
title: "Files for the app's public or vendor tree",
|
|
118
|
+
where: assets,
|
|
119
|
+
action:
|
|
120
|
+
"Import each asset from the module that uses it, or publish the assets in another package.\n" +
|
|
121
|
+
"The build does not copy files into apps.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (existsSync(join(root, "config/environment.js")) && !isEmptyAddonConfig(root)) {
|
|
126
|
+
report.unsupported.push({
|
|
127
|
+
title: "Addon config from config/environment.js",
|
|
128
|
+
where: ["config/environment.js"],
|
|
129
|
+
action:
|
|
130
|
+
"Let apps pass the config to the addon's modules, or use @embroider/macros getOwnConfig.",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let reexports = readAppFolder(root, manifest.name);
|
|
135
|
+
|
|
136
|
+
if (reexports.custom.length > 0) {
|
|
137
|
+
report.unsupported.push({
|
|
138
|
+
title: "Modules in app/ that are not re-exports",
|
|
139
|
+
where: reexports.custom,
|
|
140
|
+
action:
|
|
141
|
+
"Move the code into addon/, and re-export it from app/ with the same path:\n" +
|
|
142
|
+
` export { default } from '${manifest.name}/components/example';`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (reexports.renamed.length > 0) {
|
|
147
|
+
report.todo.push({
|
|
148
|
+
title: "app/ re-exports under another name",
|
|
149
|
+
where: reexports.renamed,
|
|
150
|
+
action:
|
|
151
|
+
"Add a `mapFilename` option to `appReexports(...)` in tsdown.config.js for each one.",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @type {Record<string, string>} */
|
|
156
|
+
let dependencies = {};
|
|
157
|
+
|
|
158
|
+
for (let [name, range] of Object.entries(manifest.dependencies ?? {})) {
|
|
159
|
+
if (!TOOLCHAIN.has(name)) dependencies[name] = String(range);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let v1 = v1AddonsIn(installedDependencies(project.desires.path, dependencies));
|
|
163
|
+
|
|
164
|
+
if (v1.length > 0) {
|
|
165
|
+
report.unsupported.push({
|
|
166
|
+
title: "v1 addons in dependencies",
|
|
167
|
+
where: v1,
|
|
168
|
+
action:
|
|
169
|
+
"Upgrade each addon to a v2 version, or remove it.\nApps without ember-cli cannot load v1 addons.",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (listFiles(root, "tests/dummy").length > 0 && !isDefaultDummyApp(root)) {
|
|
174
|
+
report.todo.push({
|
|
175
|
+
title: "A dummy app",
|
|
176
|
+
where: ["tests/dummy/"],
|
|
177
|
+
action:
|
|
178
|
+
"Move the demo into its own app, and move acceptance tests with it. Then delete tests/dummy/.\n" +
|
|
179
|
+
"The test build loads every module in tests/, so it fails until tests/dummy/ is gone.",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let hbsTests = testsWithHbs(root, ["tests"]);
|
|
184
|
+
|
|
185
|
+
if (hbsTests.length > 0) {
|
|
186
|
+
report.todo.push({
|
|
187
|
+
title: "Tests that render hbs templates",
|
|
188
|
+
where: hbsTests,
|
|
189
|
+
action:
|
|
190
|
+
"Convert each test to .gjs or .gts, and render with <template>.\n" +
|
|
191
|
+
"The hbs template literal needs ember-cli.",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let testem = changedTestemFlags(root, "testem.js", "library");
|
|
196
|
+
|
|
197
|
+
if (testem) report.todo.push(testem);
|
|
198
|
+
|
|
199
|
+
if (listFiles(root, "blueprints").length > 0) {
|
|
200
|
+
report.todo.push({
|
|
201
|
+
title: "Blueprints for ember install",
|
|
202
|
+
where: ["blueprints/"],
|
|
203
|
+
action: "Check that `ember install` still runs them, or document the setup steps for apps.",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let emberTry = firstExisting(root, ["config/ember-try.js", "tests/dummy/config/ember-try.js"]);
|
|
208
|
+
|
|
209
|
+
if (emberTry) {
|
|
210
|
+
report.todo.push({
|
|
211
|
+
title: "ember-try scenarios",
|
|
212
|
+
where: [emberTry],
|
|
213
|
+
action:
|
|
214
|
+
"Move the scenarios that you still need to .try.mjs for @embroider/try.\n" +
|
|
215
|
+
"The new test setup needs ember-source 7.2 or later.",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
existsSync(join(root, "tsconfig.json")) ||
|
|
221
|
+
filesWithExtension(root, ["addon"], [".ts", ".gts"]).length > 0
|
|
222
|
+
) {
|
|
223
|
+
report.layers.push("typescript");
|
|
224
|
+
report.todo.push({
|
|
225
|
+
title: "Declarations from isolatedDeclarations",
|
|
226
|
+
where: ["tsconfig.json"],
|
|
227
|
+
action:
|
|
228
|
+
"Run the build, and add the explicit types that TypeScript asks for.\n" +
|
|
229
|
+
"tsdown makes .d.ts files with isolatedDeclarations, which needs a type on each export.",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (listFiles(root, "tests").some((file) => /-test\.(js|ts|gjs|gts)$/.test(file))) {
|
|
234
|
+
report.layers.push("qunit");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return report;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async run(project, report) {
|
|
241
|
+
let root = project.directory;
|
|
242
|
+
let manifest = await packageJson.read(root);
|
|
243
|
+
let name = manifest.name;
|
|
244
|
+
let main = manifest["ember-addon"]?.main ?? "index.js";
|
|
245
|
+
let typescript = await project.hasOrWantsLayer("typescript");
|
|
246
|
+
let reexports = readAppFolder(root, name);
|
|
247
|
+
let testSupport = listFiles(root, "addon-test-support").length > 0;
|
|
248
|
+
|
|
249
|
+
await removeToolingOfWantedLayers(project, report);
|
|
250
|
+
|
|
251
|
+
for (let file of listFiles(root, "addon")) {
|
|
252
|
+
if (file.endsWith(".gitkeep")) continue;
|
|
253
|
+
|
|
254
|
+
await move(project, file, file.replace(/^addon\//, "src/"));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let file of listFiles(root, "addon-test-support")) {
|
|
258
|
+
await move(project, file, file.replace(/^addon-test-support\//, "src/test-support/"));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!project.hasFile("src/index.js") && !project.hasFile("src/index.ts")) {
|
|
262
|
+
await mkdir(project.path("src"), { recursive: true });
|
|
263
|
+
await writeFile(project.path(typescript ? "src/index.ts" : "src/index.js"), "export {};\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (isDefaultDummyApp(root)) {
|
|
267
|
+
await remove(project, ["tests/dummy"]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (isEmptyAddonConfig(root)) {
|
|
271
|
+
await remove(project, ["config/environment.js"]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await remove(project, [
|
|
275
|
+
"addon",
|
|
276
|
+
"addon-test-support",
|
|
277
|
+
"app",
|
|
278
|
+
"public/.gitkeep",
|
|
279
|
+
"vendor/.gitkeep",
|
|
280
|
+
main,
|
|
281
|
+
"ember-cli-build.js",
|
|
282
|
+
"testem.js",
|
|
283
|
+
"tests/index.html",
|
|
284
|
+
"tests/test-helper.js",
|
|
285
|
+
"tests/test-helper.ts",
|
|
286
|
+
".npmignore",
|
|
287
|
+
".ember-cli",
|
|
288
|
+
".watchmanconfig",
|
|
289
|
+
"tsconfig.declarations.json",
|
|
290
|
+
// the library base writes its own
|
|
291
|
+
"tsconfig.json",
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
await writeFile(
|
|
295
|
+
project.path("addon-main.cjs"),
|
|
296
|
+
`"use strict";\n\nconst { addonV1Shim } = require("@embroider/addon-shim");\n\nmodule.exports = addonV1Shim(__dirname);\n`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await packageJson.removeDependencies(Array.from(TOOLCHAIN), root);
|
|
300
|
+
await packageJson.removeDevDependencies(Array.from(TOOLCHAIN), root);
|
|
301
|
+
await packageJson.addDependencies(
|
|
302
|
+
await getLatest({ "@embroider/addon-shim": "^1.10.3", "decorator-transforms": "^2.4.0" }),
|
|
303
|
+
root,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
await packageJson.modify((json) => {
|
|
307
|
+
json["ember-addon"] = { version: 2, type: "addon", main: "addon-main.cjs" };
|
|
308
|
+
json.files ??= ["addon-main.cjs", "dist", "src"];
|
|
309
|
+
json.exports ??= exportsFor(typescript, testSupport);
|
|
310
|
+
json.imports = { ...json.imports, "#src/*": "./src/*" };
|
|
311
|
+
|
|
312
|
+
for (let [script, command] of Object.entries(json.scripts ?? {})) {
|
|
313
|
+
if (/\bember (test|serve|build|try:\S+|s|t|b)\b/.test(String(command))) {
|
|
314
|
+
delete json.scripts[script];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}, root);
|
|
318
|
+
|
|
319
|
+
await rewriteSelfImports(project, name);
|
|
320
|
+
await renameCommonJSFiles(project);
|
|
321
|
+
|
|
322
|
+
await buildWithTsdown(project, {
|
|
323
|
+
entry: entriesFor("src", ["**/*.js"]),
|
|
324
|
+
appReexports: reexports.globs.length > 0 ? reexports.globs : undefined,
|
|
325
|
+
css: usesCss(root, "src"),
|
|
326
|
+
typescript,
|
|
327
|
+
outDir: undefined,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await useEmberSourceForTests(project);
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
listFiles(root, "tests").some((file) => /-test\.(js|ts|gjs|gts)$/.test(file)) &&
|
|
334
|
+
!project.wantsLayer("qunit")
|
|
335
|
+
) {
|
|
336
|
+
report.todo.push({
|
|
337
|
+
title: "Tests without a test setup",
|
|
338
|
+
where: ["tests/"],
|
|
339
|
+
action: "Run ember.nvp again with the qunit layer, or delete the tests.",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* @param {string} root
|
|
347
|
+
* @param {string} main
|
|
348
|
+
* @returns {string[]} hooks and options of the addon object that a v2 addon cannot have
|
|
349
|
+
*/
|
|
350
|
+
function addonHooks(root, main) {
|
|
351
|
+
let path = join(root, main);
|
|
352
|
+
|
|
353
|
+
if (!existsSync(path)) return [];
|
|
354
|
+
|
|
355
|
+
let source = readFileSync(path, "utf-8");
|
|
356
|
+
let program = parseProgram(source, main);
|
|
357
|
+
|
|
358
|
+
if (!program) return [`${main}: ember.nvp cannot read this file`];
|
|
359
|
+
|
|
360
|
+
/** @type {import('../../javascript.js').Node | undefined} */
|
|
361
|
+
let addon;
|
|
362
|
+
|
|
363
|
+
walk(program, (node) => {
|
|
364
|
+
if (node.type !== "AssignmentExpression") return;
|
|
365
|
+
|
|
366
|
+
let left = node.left;
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
left?.type === "MemberExpression" &&
|
|
370
|
+
left.object?.name === "module" &&
|
|
371
|
+
left.property?.name === "exports"
|
|
372
|
+
) {
|
|
373
|
+
addon = node.right;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (addon?.type !== "ObjectExpression") {
|
|
378
|
+
return [`${main}: module.exports is not an object`];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** @type {string[]} */
|
|
382
|
+
let found = [];
|
|
383
|
+
|
|
384
|
+
for (let key of propertyNames(addon)) {
|
|
385
|
+
if (!HARMLESS_ADDON_KEYS.has(key)) found.push(`${main}: ${key}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (let option of propertyNames(propertyValue(addon, "options"))) {
|
|
389
|
+
if (!HARMLESS_ADDON_OPTIONS.has(option)) found.push(`${main}: options.${option}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return found;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The blueprint's config/environment.js gives the addon no config:
|
|
397
|
+
*
|
|
398
|
+
* module.exports = function () { return {}; };
|
|
399
|
+
*
|
|
400
|
+
* @param {string} root
|
|
401
|
+
*/
|
|
402
|
+
function isEmptyAddonConfig(root) {
|
|
403
|
+
let path = join(root, "config/environment.js");
|
|
404
|
+
|
|
405
|
+
if (!existsSync(path)) return false;
|
|
406
|
+
|
|
407
|
+
let source = readFileSync(path, "utf-8")
|
|
408
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
409
|
+
.replace(/\/\/.*$/gm, "");
|
|
410
|
+
|
|
411
|
+
return /^\s*(["']use strict["'];?)?\s*module\.exports\s*=\s*function\s*\([^)]*\)\s*\{\s*return\s*\{\s*\};?\s*\};?\s*$/.test(
|
|
412
|
+
source,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Files of the blueprint's dummy app, relative to tests/dummy/app/
|
|
418
|
+
*/
|
|
419
|
+
const DUMMY_APP_FILES = new Set([
|
|
420
|
+
"app.js",
|
|
421
|
+
"app.ts",
|
|
422
|
+
"config/environment.d.ts",
|
|
423
|
+
"deprecation-workflow.js",
|
|
424
|
+
"deprecation-workflow.ts",
|
|
425
|
+
"index.html",
|
|
426
|
+
"router.js",
|
|
427
|
+
"router.ts",
|
|
428
|
+
"styles/app.css",
|
|
429
|
+
"templates/application.gjs",
|
|
430
|
+
"templates/application.gts",
|
|
431
|
+
"templates/application.hbs",
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* What the blueprint's application template has
|
|
436
|
+
*/
|
|
437
|
+
const DUMMY_TEMPLATE =
|
|
438
|
+
/import\s+\w+\s+from\s+["']ember-page-title\/helpers\/page-title["'];?|<\/?template>|\{\{\s*page-?[tT]itle\s+["'][^"']*["']\s*\}\}|<h2 id="title">Welcome to Ember<\/h2>|\{\{outlet\}\}/g;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Whether tests/dummy/ is still the blueprint's: no routes, no components, no demo of its own.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} root
|
|
444
|
+
*/
|
|
445
|
+
function isDefaultDummyApp(root) {
|
|
446
|
+
let files = listFiles(root, "tests/dummy/app").filter((file) => !file.endsWith(".gitkeep"));
|
|
447
|
+
|
|
448
|
+
if (files.length === 0) return false;
|
|
449
|
+
|
|
450
|
+
for (let file of files) {
|
|
451
|
+
if (!DUMMY_APP_FILES.has(file.replace(/^tests\/dummy\/app\//, ""))) return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let router = firstExisting(root, ["tests/dummy/app/router.js", "tests/dummy/app/router.ts"]);
|
|
455
|
+
|
|
456
|
+
if (router && /this\.route\(/.test(readFileSync(join(root, router), "utf-8"))) return false;
|
|
457
|
+
|
|
458
|
+
let template = firstExisting(root, [
|
|
459
|
+
"tests/dummy/app/templates/application.gjs",
|
|
460
|
+
"tests/dummy/app/templates/application.gts",
|
|
461
|
+
"tests/dummy/app/templates/application.hbs",
|
|
462
|
+
]);
|
|
463
|
+
|
|
464
|
+
if (!template) return true;
|
|
465
|
+
|
|
466
|
+
return readFileSync(join(root, template), "utf-8").replace(DUMMY_TEMPLATE, "").trim() === "";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @typedef {object} AppFolder
|
|
471
|
+
* @property {string[]} globs `appReexports` globs for the built files that app/ re-exports
|
|
472
|
+
* @property {string[]} renamed re-exports whose path differs from the module they re-export
|
|
473
|
+
* @property {string[]} custom modules in app/ with code of their own
|
|
474
|
+
*/
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @param {string} root
|
|
478
|
+
* @param {string} name the addon's package name
|
|
479
|
+
* @returns {AppFolder}
|
|
480
|
+
*/
|
|
481
|
+
function readAppFolder(root, name) {
|
|
482
|
+
/** @type {AppFolder} */
|
|
483
|
+
let result = { globs: [], renamed: [], custom: [] };
|
|
484
|
+
|
|
485
|
+
for (let file of listFiles(root, "app")) {
|
|
486
|
+
if (file.endsWith(".gitkeep") || file.endsWith(".hbs")) continue;
|
|
487
|
+
|
|
488
|
+
let path = file.replace(/^app\//, "").replace(/\.\w+$/, "");
|
|
489
|
+
let target = reexportedFrom(readFileSync(join(root, file), "utf-8"), file);
|
|
490
|
+
|
|
491
|
+
if (!target) {
|
|
492
|
+
result.custom.push(file);
|
|
493
|
+
} else if (target !== `${name}/${path}` && `${target}/index` !== `${name}/${path}`) {
|
|
494
|
+
result.renamed.push(`${file}: ${target}`);
|
|
495
|
+
} else {
|
|
496
|
+
result.globs.push(`${path}.js`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The module that an app/ file re-exports, in either form:
|
|
505
|
+
*
|
|
506
|
+
* export { default } from 'my-addon/components/foo';
|
|
507
|
+
*
|
|
508
|
+
* import Foo from 'my-addon/components/foo';
|
|
509
|
+
* export default Foo;
|
|
510
|
+
*
|
|
511
|
+
* @param {string} source
|
|
512
|
+
* @param {string} file
|
|
513
|
+
* @returns {string | undefined} undefined when the file has code of its own
|
|
514
|
+
*/
|
|
515
|
+
function reexportedFrom(source, file) {
|
|
516
|
+
let program = parseProgram(source, file);
|
|
517
|
+
let body = program?.body ?? [];
|
|
518
|
+
|
|
519
|
+
if (body.length === 1 && body[0].type === "ExportNamedDeclaration" && body[0].source) {
|
|
520
|
+
return stringValue(body[0].source);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let [first, second] = body;
|
|
524
|
+
|
|
525
|
+
if (
|
|
526
|
+
body.length === 2 &&
|
|
527
|
+
first?.type === "ImportDeclaration" &&
|
|
528
|
+
first.specifiers?.length === 1 &&
|
|
529
|
+
first.specifiers[0].type === "ImportDefaultSpecifier" &&
|
|
530
|
+
second?.type === "ExportDefaultDeclaration" &&
|
|
531
|
+
second.declaration?.name === first.specifiers[0].local.name
|
|
532
|
+
) {
|
|
533
|
+
return stringValue(first.source);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* @param {boolean} typescript
|
|
539
|
+
* @param {boolean} testSupport
|
|
540
|
+
* @returns {Record<string, unknown>}
|
|
541
|
+
*/
|
|
542
|
+
function exportsFor(typescript, testSupport) {
|
|
543
|
+
/**
|
|
544
|
+
* @param {string} path
|
|
545
|
+
*/
|
|
546
|
+
let entry = (path) =>
|
|
547
|
+
typescript
|
|
548
|
+
? { types: `./dist/${path}.d.ts`, default: `./dist/${path}.js` }
|
|
549
|
+
: `./dist/${path}.js`;
|
|
550
|
+
|
|
551
|
+
/** @type {Record<string, unknown>} */
|
|
552
|
+
let exports = { ".": entry("index") };
|
|
553
|
+
|
|
554
|
+
if (testSupport) {
|
|
555
|
+
exports["./test-support"] = entry("test-support/index");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
exports["./addon-main.js"] = "./addon-main.cjs";
|
|
559
|
+
exports["./*"] = entry("*");
|
|
560
|
+
|
|
561
|
+
return exports;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* A v1 addon imports its own modules by the addon's name.
|
|
566
|
+
* A v2 addon imports them by path:
|
|
567
|
+
* - relative paths in src/
|
|
568
|
+
* - `#src/*` in tests, so that tests run against the source
|
|
569
|
+
*
|
|
570
|
+
* @param {Project} project
|
|
571
|
+
* @param {string} name
|
|
572
|
+
*/
|
|
573
|
+
async function rewriteSelfImports(project, name) {
|
|
574
|
+
let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
575
|
+
let selfImport = new RegExp(`((?:from|import)\\s*\\(?\\s*)(["'])${escaped}(/[^"']*)?\\2`, "g");
|
|
576
|
+
|
|
577
|
+
for (let file of filesWithExtension(
|
|
578
|
+
project.directory,
|
|
579
|
+
["src", "tests"],
|
|
580
|
+
[".js", ".ts", ".gjs", ".gts"],
|
|
581
|
+
)) {
|
|
582
|
+
if (file.startsWith("tests/dummy/")) continue;
|
|
583
|
+
|
|
584
|
+
let path = project.path(file);
|
|
585
|
+
let source = readFileSync(path, "utf-8");
|
|
586
|
+
|
|
587
|
+
let result = source.replace(selfImport, (_match, keyword, quote, subpath = "") => {
|
|
588
|
+
let target = subpath === "" ? "index" : subpath.slice(1);
|
|
589
|
+
|
|
590
|
+
if (file.startsWith("tests/")) return `${keyword}${quote}#src/${target}${quote}`;
|
|
591
|
+
|
|
592
|
+
let specifier = relative(dirname(file), `src/${target}`).split("\\").join("/");
|
|
593
|
+
|
|
594
|
+
return `${keyword}${quote}${specifier.startsWith(".") ? specifier : `./${specifier}`}${quote}`;
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (result === source) continue;
|
|
598
|
+
|
|
599
|
+
await writeFile(path, rewriteImportsToMatchFiles(result, path));
|
|
600
|
+
}
|
|
601
|
+
}
|