ember.nvp 1.14.0 → 1.16.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 +37 -5
- package/CHANGELOG.md +31 -0
- package/README.md +28 -0
- package/package.json +2 -1
- package/packages/rolldown/package.json +2 -2
- package/packages/rolldown/src/transform.test.ts +3 -3
- package/packages/vite/package.json +4 -4
- package/packages/vite/src/ember.test.ts +38 -0
- package/packages/vite/src/ember.ts +38 -5
- package/src/bases/minimal-app/files/package.json +2 -2
- package/src/bases/minimal-extension/files/package.json +2 -2
- 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 +1 -1
- 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/migration-from-fixtures.ts +506 -0
- package/test/ember.nvp/migration-from.test.ts +475 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { packageJson, tsconfig } from "ember-apply";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { readJSON } from "#utils/fs.js";
|
|
6
|
+
import { installedDependencies, v1AddonsIn } from "../../addons.js";
|
|
7
|
+
import { beyondEmberRolldown, readBabelConfig } from "../../babel.js";
|
|
8
|
+
import { filesWithExtension, firstExisting, listFiles, remove } from "../../files.js";
|
|
9
|
+
import { changedTestemFlags, removeToolingOfWantedLayers } from "../../layers.js";
|
|
10
|
+
import {
|
|
11
|
+
buildWithTsdown,
|
|
12
|
+
entriesFor,
|
|
13
|
+
renameCommonJSFiles,
|
|
14
|
+
useEmberSourceForTests,
|
|
15
|
+
usesCss,
|
|
16
|
+
} from "../../library.js";
|
|
17
|
+
import { readRollupConfig } from "./rollup.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import('#types').Finding} Finding
|
|
21
|
+
* @typedef {import('#types').MigrationReport} MigrationReport
|
|
22
|
+
* @typedef {import('#utils/project.js').Project} Project
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const ROLLUP_CONFIGS = ["rollup.config.mjs", "rollup.config.js", "rollup.config.cjs"];
|
|
26
|
+
|
|
27
|
+
const BABEL_CONFIGS = [
|
|
28
|
+
"babel.publish.config.cjs",
|
|
29
|
+
"babel.publish.config.mjs",
|
|
30
|
+
"babel.publish.config.js",
|
|
31
|
+
"babel.publish.config.json",
|
|
32
|
+
"babel.config.json",
|
|
33
|
+
"babel.config.cjs",
|
|
34
|
+
"babel.config.mjs",
|
|
35
|
+
"babel.config.js",
|
|
36
|
+
".babelrc",
|
|
37
|
+
".babelrc.json",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The test setup that the blueprint generates.
|
|
42
|
+
* The qunit layer has its own.
|
|
43
|
+
*/
|
|
44
|
+
const TEST_SETUP = [
|
|
45
|
+
"testem.cjs",
|
|
46
|
+
"testem.js",
|
|
47
|
+
"tests/index.html",
|
|
48
|
+
"tests/test-helper.ts",
|
|
49
|
+
"tests/test-helper.js",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Only the build, tests, and demo app use these.
|
|
54
|
+
* Each one either goes away with the migration, or comes back through a layer.
|
|
55
|
+
*/
|
|
56
|
+
const REPLACED_DEV_DEPENDENCIES = [
|
|
57
|
+
"@embroider/addon-dev",
|
|
58
|
+
"@embroider/compat",
|
|
59
|
+
"@embroider/core",
|
|
60
|
+
"@embroider/test-setup",
|
|
61
|
+
"@embroider/vite",
|
|
62
|
+
"@rollup/plugin-babel",
|
|
63
|
+
"ember-strict-application-resolver",
|
|
64
|
+
"rollup",
|
|
65
|
+
"rollup-plugin-delete",
|
|
66
|
+
"rollup-plugin-glimmer-template-tag",
|
|
67
|
+
"rollup-plugin-ts",
|
|
68
|
+
"testem",
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What the blueprint's demo app has: a router without routes,
|
|
73
|
+
* and an application template with a title and a greeting.
|
|
74
|
+
*/
|
|
75
|
+
const DEMO_APP_FILES = new Set([
|
|
76
|
+
"demo-app/app.gjs",
|
|
77
|
+
"demo-app/app.gts",
|
|
78
|
+
"demo-app/styles.css",
|
|
79
|
+
"demo-app/templates/application.gjs",
|
|
80
|
+
"demo-app/templates/application.gts",
|
|
81
|
+
]);
|
|
82
|
+
const DEMO_TEMPLATE =
|
|
83
|
+
/import\s*\{\s*pageTitle\s*\}\s*from\s*["']ember-page-title["'];?|const greeting = ["']hello["'];?|<\/?template>|\{\{pageTitle "Demo App"\}\}|<h1>Welcome to ember!<\/h1>|\{\{greeting\}\}, world!/g;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compiler options that the blueprint's tsconfig files set.
|
|
87
|
+
* The library base's tsconfig.json covers what a library needs.
|
|
88
|
+
*/
|
|
89
|
+
const BLUEPRINT_COMPILER_OPTIONS = new Set([
|
|
90
|
+
"allowImportingTsExtensions",
|
|
91
|
+
"allowJs",
|
|
92
|
+
"declaration",
|
|
93
|
+
"declarationDir",
|
|
94
|
+
"emitDeclarationOnly",
|
|
95
|
+
"lib",
|
|
96
|
+
"noEmit",
|
|
97
|
+
"noEmitOnError",
|
|
98
|
+
"rootDir",
|
|
99
|
+
"skipLibCheck",
|
|
100
|
+
"types",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A v2 addon that builds with rollup and `@embroider/addon-dev`.
|
|
105
|
+
*
|
|
106
|
+
* This covers `@ember/addon-blueprint`, and the addon package of its predecessor,
|
|
107
|
+
* `@embroider/addon-blueprint` (a monorepo with a separate test-app).
|
|
108
|
+
*
|
|
109
|
+
* The migration moves the build to tsdown with `@nullvoxpopuli/ember-rolldown`.
|
|
110
|
+
* The addon keeps its `ember-addon` metadata and `addon-main.cjs`,
|
|
111
|
+
* so that apps with ember-cli can still use it.
|
|
112
|
+
*
|
|
113
|
+
* @type {import('#types').Migration}
|
|
114
|
+
*/
|
|
115
|
+
export default {
|
|
116
|
+
label: "@ember/addon-blueprint",
|
|
117
|
+
type: "library",
|
|
118
|
+
|
|
119
|
+
detect(directory) {
|
|
120
|
+
let manifest = readJSON(join(directory, "package.json"));
|
|
121
|
+
|
|
122
|
+
if (manifest?.["ember-addon"]?.version !== 2) return false;
|
|
123
|
+
|
|
124
|
+
let config = firstExisting(directory, ROLLUP_CONFIGS);
|
|
125
|
+
|
|
126
|
+
return Boolean(
|
|
127
|
+
config && readFileSync(join(directory, config), "utf-8").includes("@embroider/addon-dev"),
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async check(project) {
|
|
132
|
+
/** @type {MigrationReport} */
|
|
133
|
+
let report = { unsupported: [], todo: [], layers: [] };
|
|
134
|
+
let root = project.directory;
|
|
135
|
+
let manifest = await packageJson.read(root);
|
|
136
|
+
let rollupFile = /** @type {string} */ (firstExisting(root, ROLLUP_CONFIGS));
|
|
137
|
+
let rollup = readRollupConfig(root, rollupFile);
|
|
138
|
+
|
|
139
|
+
if (!rollup) {
|
|
140
|
+
report.unsupported.push({
|
|
141
|
+
title: "A rollup config that ember.nvp cannot read",
|
|
142
|
+
where: [rollupFile],
|
|
143
|
+
action:
|
|
144
|
+
"Use `new Addon(...)` from @embroider/addon-dev/rollup, the way the blueprint does.",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return report;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let templates = filesWithExtension(root, [rollup.srcDir], [".hbs"]);
|
|
151
|
+
|
|
152
|
+
if (templates.length > 0) {
|
|
153
|
+
report.unsupported.push({
|
|
154
|
+
title: "Templates in .hbs files",
|
|
155
|
+
where: templates,
|
|
156
|
+
action:
|
|
157
|
+
"Move each template into its component as <template>, in a .gjs or .gts file.\n" +
|
|
158
|
+
"The build compiles <template> only.",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (rollup.publicAssets) {
|
|
163
|
+
report.unsupported.push({
|
|
164
|
+
title: "Public assets",
|
|
165
|
+
where: [rollup.publicAssets],
|
|
166
|
+
action:
|
|
167
|
+
"Import each asset from the module that uses it, or publish the assets in another package.\n" +
|
|
168
|
+
"The build does not copy public assets into apps.",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let v1 = v1AddonsIn(installedDependencies(project.desires.path, manifest.dependencies ?? {}));
|
|
173
|
+
|
|
174
|
+
if (v1.length > 0) {
|
|
175
|
+
report.unsupported.push({
|
|
176
|
+
title: "v1 addons in dependencies",
|
|
177
|
+
where: v1,
|
|
178
|
+
action:
|
|
179
|
+
"Upgrade each addon to a v2 version, or remove it.\nApps without ember-cli cannot load v1 addons.",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (rollup.unreadEntrypoints) {
|
|
184
|
+
report.todo.push({
|
|
185
|
+
title: "publicEntrypoints that are not a list of strings",
|
|
186
|
+
where: [rollupFile],
|
|
187
|
+
action: "Check `entry` in tsdown.config.js. The migration made every module an entry.",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (rollup.appReexportsOptions) {
|
|
192
|
+
report.todo.push({
|
|
193
|
+
title: "appReexports options",
|
|
194
|
+
where: [rollup.appReexportsOptions],
|
|
195
|
+
action: "Add these options to `appReexports(...)` in tsdown.config.js.",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (rollup.otherPlugins.length > 0) {
|
|
200
|
+
report.todo.push({
|
|
201
|
+
title: "Rollup plugins that the new build does not have",
|
|
202
|
+
where: rollup.otherPlugins,
|
|
203
|
+
action:
|
|
204
|
+
"Add the plugins that you still need to `plugins` in tsdown.config.js.\n" +
|
|
205
|
+
"tsdown runs most rollup plugins.",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let babel = publishBabelConfig(root, rollup.babelConfig);
|
|
210
|
+
let extraBabel = babel ? beyondEmberRolldown(babel.config) : [];
|
|
211
|
+
|
|
212
|
+
if (babel && extraBabel.length > 0) {
|
|
213
|
+
report.todo.push({
|
|
214
|
+
title: `A babel config with more than the build's defaults`,
|
|
215
|
+
where: [`${babel.file}: ${extraBabel.join(", ")}`],
|
|
216
|
+
action:
|
|
217
|
+
"The build uses this config in place of its defaults.\n" +
|
|
218
|
+
"Remove what the build does not need, then run the build.",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let typescript = usesTypeScript(root, rollup.srcDir);
|
|
223
|
+
|
|
224
|
+
if (typescript) {
|
|
225
|
+
report.layers.push("typescript");
|
|
226
|
+
report.todo.push({
|
|
227
|
+
title: "Declarations from isolatedDeclarations",
|
|
228
|
+
where: ["tsconfig.json"],
|
|
229
|
+
action:
|
|
230
|
+
"Run the build, and add the explicit types that TypeScript asks for.\n" +
|
|
231
|
+
"tsdown makes .d.ts files with isolatedDeclarations, which needs a type on each export.",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let options = await customCompilerOptions(project);
|
|
235
|
+
|
|
236
|
+
if (options.length > 0) {
|
|
237
|
+
report.todo.push({
|
|
238
|
+
title: "Compiler options that the new tsconfig.json does not have",
|
|
239
|
+
where: options,
|
|
240
|
+
action: "Add the options that you still need to tsconfig.json.",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (hasTests(root)) {
|
|
246
|
+
report.layers.push("qunit");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (listFiles(root, "demo-app").length > 0 && !isDefaultDemoApp(root)) {
|
|
250
|
+
report.todo.push({
|
|
251
|
+
title: "A demo app",
|
|
252
|
+
where: ["index.html", "demo-app/"],
|
|
253
|
+
action:
|
|
254
|
+
"Move the demo into its own app, or delete it.\n" +
|
|
255
|
+
"The test build also builds index.html, so the demo must keep building until it moves.",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let eslintConfig = firstExisting(root, [
|
|
260
|
+
"eslint.config.mjs",
|
|
261
|
+
"eslint.config.js",
|
|
262
|
+
"eslint.config.cjs",
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
if (
|
|
266
|
+
eslintConfig &&
|
|
267
|
+
readFileSync(join(root, eslintConfig), "utf-8").includes("@babel/eslint-parser")
|
|
268
|
+
) {
|
|
269
|
+
report.todo.push({
|
|
270
|
+
title: "An ESLint config that parses with babel",
|
|
271
|
+
where: [eslintConfig],
|
|
272
|
+
action:
|
|
273
|
+
"Select the eslint-bundled layer, or configure @babel/eslint-parser yourself.\n" +
|
|
274
|
+
"The babel config that it reads goes away with the migration.",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let testem = changedTestemFlags(root, "testem.cjs", "library");
|
|
279
|
+
|
|
280
|
+
if (testem) report.todo.push(testem);
|
|
281
|
+
|
|
282
|
+
if (existsSync(join(root, ".try.mjs"))) {
|
|
283
|
+
report.todo.push({
|
|
284
|
+
title: "ember-try scenarios",
|
|
285
|
+
where: [".try.mjs"],
|
|
286
|
+
action:
|
|
287
|
+
"Remove the scenarios that set ENABLE_COMPAT_BUILD or use ember-source below 7.2.\n" +
|
|
288
|
+
"The new test setup has no compat mode, and it needs the strict resolver of ember-source 7.2.",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return report;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
async run(project, report) {
|
|
296
|
+
let root = project.directory;
|
|
297
|
+
let rollupFile = /** @type {string} */ (firstExisting(root, ROLLUP_CONFIGS));
|
|
298
|
+
let rollup = /** @type {import('./rollup.js').RollupSetup} */ (
|
|
299
|
+
readRollupConfig(root, rollupFile)
|
|
300
|
+
);
|
|
301
|
+
let typescript = await project.hasOrWantsLayer("typescript");
|
|
302
|
+
let babel = publishBabelConfig(root, rollup.babelConfig);
|
|
303
|
+
|
|
304
|
+
await removeToolingOfWantedLayers(project, report);
|
|
305
|
+
|
|
306
|
+
await remove(project, [rollupFile, "config/ember-cli-update.json", "tsconfig.publish.json"]);
|
|
307
|
+
// the blueprint's config for editors and tests, not for the build
|
|
308
|
+
await remove(project, otherBabelConfigs(root, babel?.file));
|
|
309
|
+
|
|
310
|
+
if (babel && beyondEmberRolldown(babel.config).length === 0) {
|
|
311
|
+
await remove(project, [babel.file]);
|
|
312
|
+
} else if (babel) {
|
|
313
|
+
await withoutColocationPlugin(project, babel.file);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// the old test setup builds with @embroider/vite, which leaves with the migration
|
|
317
|
+
await remove(project, TEST_SETUP);
|
|
318
|
+
await removeBlueprintViteConfig(project);
|
|
319
|
+
|
|
320
|
+
if (hasTests(root) && !project.wantsLayer("qunit")) {
|
|
321
|
+
report.todo.push({
|
|
322
|
+
title: "Tests without a test setup",
|
|
323
|
+
where: ["tests/"],
|
|
324
|
+
action: "Run ember.nvp again with the qunit layer, or delete the tests.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// a demo app of your own is in the report, and keeps the resolver that it imports
|
|
329
|
+
let demo = listFiles(root, "demo-app").length > 0;
|
|
330
|
+
let defaultDemo = demo && isDefaultDemoApp(root);
|
|
331
|
+
|
|
332
|
+
if (defaultDemo) {
|
|
333
|
+
await remove(project, ["index.html", "demo-app"]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (onlyComments(root, "unpublished-development-types/index.d.ts")) {
|
|
337
|
+
await remove(project, ["unpublished-development-types"]);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// the library base writes its own
|
|
341
|
+
await remove(project, ["tsconfig.json"]);
|
|
342
|
+
|
|
343
|
+
await packageJson.removeDevDependencies(
|
|
344
|
+
demo && !defaultDemo
|
|
345
|
+
? REPLACED_DEV_DEPENDENCIES.filter((name) => name !== "ember-strict-application-resolver")
|
|
346
|
+
: REPLACED_DEV_DEPENDENCIES,
|
|
347
|
+
root,
|
|
348
|
+
);
|
|
349
|
+
await packageJson.modify((json) => {
|
|
350
|
+
// the old test setup's script, which the qunit layer replaces
|
|
351
|
+
if (json.scripts?.test?.includes("testem")) delete json.scripts.test;
|
|
352
|
+
}, root);
|
|
353
|
+
|
|
354
|
+
await renameCommonJSFiles(project);
|
|
355
|
+
|
|
356
|
+
await buildWithTsdown(project, {
|
|
357
|
+
entry: entriesFor(rollup.srcDir, rollup.publicEntrypoints),
|
|
358
|
+
appReexports: rollup.appReexports,
|
|
359
|
+
css: usesCss(root, rollup.srcDir),
|
|
360
|
+
typescript,
|
|
361
|
+
outDir: rollup.destDir === "dist" ? undefined : rollup.destDir,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await useEmberSourceForTests(project);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The babel config that the rollup build reads.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} root
|
|
372
|
+
* @param {string | undefined} named the file that rollup.config names
|
|
373
|
+
* @returns {{ file: string, config: import('../../babel.js').BabelConfig } | undefined}
|
|
374
|
+
*/
|
|
375
|
+
function publishBabelConfig(root, named) {
|
|
376
|
+
let file = named && existsSync(join(root, named)) ? named : firstExisting(root, BABEL_CONFIGS);
|
|
377
|
+
|
|
378
|
+
if (!file) return;
|
|
379
|
+
|
|
380
|
+
let config = readBabelConfig(root, file) ?? {
|
|
381
|
+
plugins: [],
|
|
382
|
+
presets: [],
|
|
383
|
+
hasCode: true,
|
|
384
|
+
templateTransforms: [],
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return { file, config };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {string} root
|
|
392
|
+
* @param {string | undefined} publishConfig
|
|
393
|
+
* @returns {string[]}
|
|
394
|
+
*/
|
|
395
|
+
function otherBabelConfigs(root, publishConfig) {
|
|
396
|
+
return BABEL_CONFIGS.filter((file) => file !== publishConfig && existsSync(join(root, file)));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The colocation plugin joins .hbs files to their components.
|
|
401
|
+
* The migration requires that no .hbs files remain,
|
|
402
|
+
* and the plugin leaves with @embroider/addon-dev.
|
|
403
|
+
*
|
|
404
|
+
* @param {Project} project
|
|
405
|
+
* @param {string} file
|
|
406
|
+
*/
|
|
407
|
+
async function withoutColocationPlugin(project, file) {
|
|
408
|
+
let path = project.path(file);
|
|
409
|
+
let source = readFileSync(path, "utf-8");
|
|
410
|
+
let result = source.replace(
|
|
411
|
+
/\s*["']@embroider\/addon-dev\/template-colocation-plugin["'],?/g,
|
|
412
|
+
"",
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (result !== source) {
|
|
416
|
+
await writeFile(path, result);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The blueprint's vite.config.mjs builds the tests and the demo app.
|
|
422
|
+
* The qunit layer writes its own.
|
|
423
|
+
*
|
|
424
|
+
* @param {Project} project
|
|
425
|
+
*/
|
|
426
|
+
async function removeBlueprintViteConfig(project) {
|
|
427
|
+
for (let file of ["vite.config.mjs", "vite.config.js", "vite.config.ts"]) {
|
|
428
|
+
let source = await project.read(file);
|
|
429
|
+
|
|
430
|
+
if (source?.includes("@embroider/vite")) {
|
|
431
|
+
await remove(project, [file]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @param {string} root
|
|
438
|
+
* @param {string} srcDir
|
|
439
|
+
*/
|
|
440
|
+
function usesTypeScript(root, srcDir) {
|
|
441
|
+
return (
|
|
442
|
+
existsSync(join(root, "tsconfig.json")) ||
|
|
443
|
+
filesWithExtension(root, [srcDir], [".ts", ".gts"]).some((file) => !file.endsWith(".d.ts"))
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @param {string} root
|
|
449
|
+
*/
|
|
450
|
+
function hasTests(root) {
|
|
451
|
+
return listFiles(root, "tests").some((file) => /-test\.(js|ts|gjs|gts)$/.test(file));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Whether index.html and demo-app/ are still the blueprint's demo app
|
|
456
|
+
*
|
|
457
|
+
* @param {string} root
|
|
458
|
+
*/
|
|
459
|
+
function isDefaultDemoApp(root) {
|
|
460
|
+
for (let file of listFiles(root, "demo-app")) {
|
|
461
|
+
if (!DEMO_APP_FILES.has(file)) return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let app = firstExisting(root, ["demo-app/app.gts", "demo-app/app.gjs"]);
|
|
465
|
+
|
|
466
|
+
if (app && /this\.route\(/.test(readFileSync(join(root, app), "utf-8"))) return false;
|
|
467
|
+
|
|
468
|
+
let template = firstExisting(root, [
|
|
469
|
+
"demo-app/templates/application.gts",
|
|
470
|
+
"demo-app/templates/application.gjs",
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
if (!template) return true;
|
|
474
|
+
|
|
475
|
+
return readFileSync(join(root, template), "utf-8").replace(DEMO_TEMPLATE, "").trim() === "";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @param {string} root
|
|
480
|
+
* @param {string} file
|
|
481
|
+
* @returns {boolean} whether the file has nothing but comments, like the blueprint's type registry
|
|
482
|
+
*/
|
|
483
|
+
function onlyComments(root, file) {
|
|
484
|
+
let path = join(root, file);
|
|
485
|
+
|
|
486
|
+
if (!existsSync(path)) return false;
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
readFileSync(path, "utf-8")
|
|
490
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
491
|
+
.replace(/\/\/.*$/gm, "")
|
|
492
|
+
.trim() === ""
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @param {Project} project
|
|
498
|
+
* @returns {Promise<string[]>} compiler options beyond the blueprint's, from tsconfig.json and tsconfig.publish.json
|
|
499
|
+
*/
|
|
500
|
+
async function customCompilerOptions(project) {
|
|
501
|
+
/** @type {string[]} */
|
|
502
|
+
let custom = [];
|
|
503
|
+
|
|
504
|
+
for (let file of ["tsconfig.json", "tsconfig.publish.json"]) {
|
|
505
|
+
if (!project.hasFile(file)) continue;
|
|
506
|
+
|
|
507
|
+
let config = await tsconfig.read(project.path(file)).catch(() => undefined);
|
|
508
|
+
|
|
509
|
+
for (let option of Object.keys(config?.compilerOptions ?? {})) {
|
|
510
|
+
if (!BLUEPRINT_COMPILER_OPTIONS.has(option)) custom.push(`${file}: ${option}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return custom;
|
|
515
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
importedNames,
|
|
5
|
+
parseProgram,
|
|
6
|
+
propertyValue,
|
|
7
|
+
sourceOf,
|
|
8
|
+
stringArray,
|
|
9
|
+
stringValue,
|
|
10
|
+
walk,
|
|
11
|
+
} from "../../javascript.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rollup plugins whose work the tsdown build does by itself
|
|
15
|
+
*/
|
|
16
|
+
const REPLACED_PLUGINS = new Set([
|
|
17
|
+
"@embroider/addon-dev/rollup",
|
|
18
|
+
"@rollup/plugin-babel",
|
|
19
|
+
"rollup-plugin-glimmer-template-tag",
|
|
20
|
+
"rollup-plugin-ts",
|
|
21
|
+
"@rollup/plugin-typescript",
|
|
22
|
+
"rollup-plugin-delete",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `@embroider/addon-dev` plugins whose work the tsdown build does by itself
|
|
27
|
+
*/
|
|
28
|
+
const REPLACED_ADDON_METHODS = new Set([
|
|
29
|
+
"dependencies",
|
|
30
|
+
"hbs",
|
|
31
|
+
"gjs",
|
|
32
|
+
"clean",
|
|
33
|
+
"declarations",
|
|
34
|
+
"keepAssets",
|
|
35
|
+
"output",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a rollup.config asks of `@embroider/addon-dev`
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} RollupSetup
|
|
42
|
+
* @property {string} srcDir
|
|
43
|
+
* @property {string} destDir
|
|
44
|
+
* @property {string[]} publicEntrypoints globs for built files, relative to `destDir`
|
|
45
|
+
* @property {boolean} unreadEntrypoints whether `publicEntrypoints` was not a list of strings
|
|
46
|
+
* @property {string[] | undefined} appReexports globs for built files, relative to `destDir`
|
|
47
|
+
* @property {string | undefined} appReexportsOptions the source of the options argument
|
|
48
|
+
* @property {string | undefined} publicAssets the source of the `publicAssets` call
|
|
49
|
+
* @property {string | undefined} babelConfig the babel config file that the build reads, when the config names one
|
|
50
|
+
* @property {string[]} otherPlugins the source of each plugin that the tsdown build does not replace
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string} root
|
|
55
|
+
* @param {string} file relative to `root`
|
|
56
|
+
* @returns {RollupSetup | undefined} undefined when the config has no `new Addon(...)`
|
|
57
|
+
*/
|
|
58
|
+
export function readRollupConfig(root, file) {
|
|
59
|
+
let source = readFileSync(join(root, file), "utf-8");
|
|
60
|
+
let program = parseProgram(source, file);
|
|
61
|
+
|
|
62
|
+
if (!program) return;
|
|
63
|
+
|
|
64
|
+
let imports = importedNames(program);
|
|
65
|
+
|
|
66
|
+
/** @type {RollupSetup} */
|
|
67
|
+
let setup = {
|
|
68
|
+
srcDir: "src",
|
|
69
|
+
destDir: "dist",
|
|
70
|
+
publicEntrypoints: ["**/*.js"],
|
|
71
|
+
unreadEntrypoints: false,
|
|
72
|
+
appReexports: undefined,
|
|
73
|
+
appReexportsOptions: undefined,
|
|
74
|
+
publicAssets: undefined,
|
|
75
|
+
babelConfig: babelConfigNamedIn(source),
|
|
76
|
+
otherPlugins: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** @type {string | undefined} */
|
|
80
|
+
let addonName;
|
|
81
|
+
|
|
82
|
+
walk(program, (node, parent) => {
|
|
83
|
+
if (node.type !== "NewExpression" || node.callee?.type !== "Identifier") return;
|
|
84
|
+
if (imports.get(node.callee.name) !== "@embroider/addon-dev/rollup") return;
|
|
85
|
+
|
|
86
|
+
let options = node.arguments?.[0];
|
|
87
|
+
|
|
88
|
+
setup.srcDir = stringValue(propertyValue(options, "srcDir")) ?? setup.srcDir;
|
|
89
|
+
setup.destDir = stringValue(propertyValue(options, "destDir")) ?? setup.destDir;
|
|
90
|
+
|
|
91
|
+
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") {
|
|
92
|
+
addonName = parent.id.name;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!addonName) return;
|
|
97
|
+
|
|
98
|
+
let plugins = pluginsArray(program);
|
|
99
|
+
|
|
100
|
+
for (let plugin of plugins?.elements ?? []) {
|
|
101
|
+
if (!plugin) continue;
|
|
102
|
+
|
|
103
|
+
let method = addonMethod(plugin, addonName);
|
|
104
|
+
|
|
105
|
+
if (method === "publicEntrypoints") {
|
|
106
|
+
let globs = stringArray(plugin.arguments?.[0]);
|
|
107
|
+
|
|
108
|
+
setup.publicEntrypoints = globs ?? setup.publicEntrypoints;
|
|
109
|
+
setup.unreadEntrypoints = !globs;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (method === "appReexports") {
|
|
114
|
+
setup.appReexports = stringArray(plugin.arguments?.[0]) ?? ["**/*.js"];
|
|
115
|
+
|
|
116
|
+
let options = plugin.arguments?.[1];
|
|
117
|
+
|
|
118
|
+
setup.appReexportsOptions = options ? sourceOf(source, options) : undefined;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (method === "publicAssets") {
|
|
123
|
+
setup.publicAssets = sourceOf(source, plugin);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (method && REPLACED_ADDON_METHODS.has(method)) continue;
|
|
128
|
+
|
|
129
|
+
let callee = plugin.type === "CallExpression" ? plugin.callee : undefined;
|
|
130
|
+
let from = callee?.type === "Identifier" ? imports.get(callee.name) : undefined;
|
|
131
|
+
|
|
132
|
+
if (from && REPLACED_PLUGINS.has(from)) continue;
|
|
133
|
+
|
|
134
|
+
setup.otherPlugins.push(sourceOf(source, plugin).replace(/\s+/g, " "));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return setup;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {import('../../javascript.js').Node} program
|
|
142
|
+
* @returns {import('../../javascript.js').Node | undefined} the first `plugins: [...]` array
|
|
143
|
+
*/
|
|
144
|
+
function pluginsArray(program) {
|
|
145
|
+
/** @type {import('../../javascript.js').Node | undefined} */
|
|
146
|
+
let found;
|
|
147
|
+
|
|
148
|
+
walk(program, (node) => {
|
|
149
|
+
if (found || node.type !== "ObjectExpression") return;
|
|
150
|
+
|
|
151
|
+
let plugins = propertyValue(node, "plugins");
|
|
152
|
+
|
|
153
|
+
if (plugins?.type === "ArrayExpression") found = plugins;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return found;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {import('../../javascript.js').Node} node
|
|
161
|
+
* @param {string} addonName
|
|
162
|
+
* @returns {string | undefined} `hbs` for `addon.hbs()`
|
|
163
|
+
*/
|
|
164
|
+
function addonMethod(node, addonName) {
|
|
165
|
+
if (node.type !== "CallExpression") return;
|
|
166
|
+
|
|
167
|
+
let callee = node.callee;
|
|
168
|
+
|
|
169
|
+
if (callee?.type !== "MemberExpression") return;
|
|
170
|
+
if (callee.object?.type !== "Identifier" || callee.object.name !== addonName) return;
|
|
171
|
+
|
|
172
|
+
return callee.property?.name;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The blueprint names the config with a path:
|
|
177
|
+
*
|
|
178
|
+
* const babelConfig = resolve(rootDirectory, './babel.publish.config.cjs');
|
|
179
|
+
*
|
|
180
|
+
* @param {string} source
|
|
181
|
+
* @returns {string | undefined}
|
|
182
|
+
*/
|
|
183
|
+
function babelConfigNamedIn(source) {
|
|
184
|
+
let match =
|
|
185
|
+
/["'`](?:\.\/)?((?:config\/)?babel(?:\.publish)?\.config\.(?:c|m)?js(?:on)?)["'`]/.exec(source);
|
|
186
|
+
|
|
187
|
+
return match?.[1];
|
|
188
|
+
}
|