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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('#types').Finding} Finding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Past this many, a finding's `where` list ends with "and N more".
|
|
7
|
+
*/
|
|
8
|
+
const LISTED = 8;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when a project uses features that ember.nvp cannot migrate.
|
|
12
|
+
*
|
|
13
|
+
* Nothing is written before this is thrown.
|
|
14
|
+
*/
|
|
15
|
+
export class MigrationError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} from the old blueprint
|
|
18
|
+
* @param {Finding[]} findings
|
|
19
|
+
*/
|
|
20
|
+
constructor(from, findings) {
|
|
21
|
+
super(
|
|
22
|
+
`ember.nvp cannot migrate this project from ${from}.\n` +
|
|
23
|
+
`Fix these items, then run ember.nvp again:\n\n` +
|
|
24
|
+
formatFindings(findings),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
this.name = "MigrationError";
|
|
28
|
+
this.from = from;
|
|
29
|
+
this.findings = findings;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A numbered list:
|
|
35
|
+
*
|
|
36
|
+
* 1. Templates in .hbs files
|
|
37
|
+
* - src/components/foo.hbs
|
|
38
|
+
* Move each template into its component as <template>.
|
|
39
|
+
*
|
|
40
|
+
* @param {Finding[]} findings
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function formatFindings(findings) {
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
let lines = [];
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < findings.length; i++) {
|
|
48
|
+
let finding = /** @type {Finding} */ (findings[i]);
|
|
49
|
+
let where = finding.where ?? [];
|
|
50
|
+
|
|
51
|
+
lines.push(`${i + 1}. ${finding.title}`);
|
|
52
|
+
|
|
53
|
+
for (let j = 0; j < where.length && j < LISTED; j++) {
|
|
54
|
+
lines.push(` - ${where[j]}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (where.length > LISTED) {
|
|
58
|
+
lines.push(` - and ${where.length - LISTED} more`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let line of finding.action.split("\n")) {
|
|
62
|
+
lines.push(` ${line}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
lines.push("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return lines.join("\n").trimEnd();
|
|
69
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import addonBlueprint from "./at-ember/addon-blueprint/index.js";
|
|
2
|
+
import appBlueprint from "./at-ember/app-blueprint/index.js";
|
|
3
|
+
import classicAddon from "./at-ember-tooling/classic-build-addon-blueprint/index.js";
|
|
4
|
+
import classicApp from "./at-ember-tooling/classic-build-app-blueprint/index.js";
|
|
5
|
+
import { MigrationError } from "./findings.js";
|
|
6
|
+
|
|
7
|
+
export { MigrationError, formatFindings } from "./findings.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The older blueprints that ember.nvp migrates from.
|
|
11
|
+
*
|
|
12
|
+
* Detection asks them in this order.
|
|
13
|
+
* The markers of each one are its own, so the order only decides ties:
|
|
14
|
+
* - a v2 addon builds with rollup and @embroider/addon-dev
|
|
15
|
+
* - a classic addon has an index.js for ember-cli, and a dummy app in tests/
|
|
16
|
+
* - a Vite app still has an ember-cli-build for @embroider/compat
|
|
17
|
+
* - a classic app has an ember-cli-build without Vite
|
|
18
|
+
*
|
|
19
|
+
* @type {import('#types').Migration[]}
|
|
20
|
+
*/
|
|
21
|
+
export const migrations = [addonBlueprint, classicAddon, appBlueprint, classicApp];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} directory an existing project
|
|
25
|
+
* @returns {import('#types').Migration | undefined}
|
|
26
|
+
*/
|
|
27
|
+
export function detectMigration(directory) {
|
|
28
|
+
return migrations.find((migration) => migration.detect(directory));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks the project, and changes nothing.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('#utils/project.js').Project} project
|
|
35
|
+
* @returns {Promise<{ migration: import('#types').Migration, report: import('#types').MigrationReport } | undefined>}
|
|
36
|
+
* undefined when the project is not from an older blueprint
|
|
37
|
+
*/
|
|
38
|
+
export async function checkMigration(project) {
|
|
39
|
+
let migration = detectMigration(project.directory);
|
|
40
|
+
|
|
41
|
+
if (!migration) return;
|
|
42
|
+
|
|
43
|
+
let report = await migration.check(project);
|
|
44
|
+
|
|
45
|
+
if (migration.type !== project.type) {
|
|
46
|
+
report.unsupported.unshift({
|
|
47
|
+
title: `A ${migration.type} from ${migration.label}`,
|
|
48
|
+
where: [`selected type: ${project.type}`],
|
|
49
|
+
action: `Select "${migration.type}" as the project type.`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { migration, report };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Moves a project from an older blueprint to the ember.nvp base,
|
|
58
|
+
* so that the base and the layers can run over it.
|
|
59
|
+
*
|
|
60
|
+
* Throws a MigrationError, before any change,
|
|
61
|
+
* when the project uses features that ember.nvp does not support.
|
|
62
|
+
*
|
|
63
|
+
* @param {import('#utils/project.js').Project} project
|
|
64
|
+
* @returns {Promise<{ migration: import('#types').Migration, report: import('#types').MigrationReport } | undefined>}
|
|
65
|
+
* undefined when the project is not from an older blueprint
|
|
66
|
+
*/
|
|
67
|
+
export async function migrate(project) {
|
|
68
|
+
let result = await checkMigration(project);
|
|
69
|
+
|
|
70
|
+
if (!result) return;
|
|
71
|
+
|
|
72
|
+
let { migration, report } = result;
|
|
73
|
+
|
|
74
|
+
if (report.unsupported.length > 0) {
|
|
75
|
+
throw new MigrationError(migration.label, report.unsupported);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await migration.run(project, report);
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { parse } from "ember-estree";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An ESTree node.
|
|
5
|
+
*
|
|
6
|
+
* ember-estree types every property as `unknown`,
|
|
7
|
+
* and these helpers read many shapes of config files.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {{ type: string, start?: number, end?: number, [key: string]: any }} Node
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} source
|
|
14
|
+
* @param {string} filePath decides the parser: `.ts` parses TypeScript
|
|
15
|
+
* @returns {Node | undefined} the Program, or undefined when the file does not parse
|
|
16
|
+
*/
|
|
17
|
+
export function parseProgram(source, filePath) {
|
|
18
|
+
try {
|
|
19
|
+
let file = /** @type {Node} */ (parse(source, { filePath }));
|
|
20
|
+
|
|
21
|
+
return /** @type {Node} */ (file.program ?? file);
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calls `visit` for every node below `node`, parents first.
|
|
29
|
+
*
|
|
30
|
+
* @param {unknown} node
|
|
31
|
+
* @param {(node: Node, parent: Node | undefined) => void} visit
|
|
32
|
+
* @param {Node} [parent]
|
|
33
|
+
*/
|
|
34
|
+
export function walk(node, visit, parent) {
|
|
35
|
+
if (!node || typeof node !== "object") return;
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(node)) {
|
|
38
|
+
for (let child of node) walk(child, visit, parent);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let astNode = /** @type {Node} */ (node);
|
|
43
|
+
|
|
44
|
+
if (typeof astNode.type !== "string") return;
|
|
45
|
+
|
|
46
|
+
visit(astNode, parent);
|
|
47
|
+
|
|
48
|
+
for (let [key, value] of Object.entries(astNode)) {
|
|
49
|
+
if (key === "loc" || key === "range" || key === "parent") continue;
|
|
50
|
+
|
|
51
|
+
walk(value, visit, astNode);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {unknown} node
|
|
57
|
+
* @returns {string | undefined} the value of a string literal, or of a template literal without expressions
|
|
58
|
+
*/
|
|
59
|
+
export function stringValue(node) {
|
|
60
|
+
let astNode = /** @type {Node | undefined} */ (node);
|
|
61
|
+
|
|
62
|
+
if (!astNode) return;
|
|
63
|
+
|
|
64
|
+
if (astNode.type === "Literal" && typeof astNode.value === "string") {
|
|
65
|
+
return astNode.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (astNode.type === "StringLiteral") {
|
|
69
|
+
return /** @type {string} */ (astNode.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (astNode.type === "TemplateLiteral" && astNode.expressions?.length === 0) {
|
|
73
|
+
return astNode.quasis?.[0]?.value?.cooked;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} node
|
|
79
|
+
* @returns {string[] | undefined} the values of an array of string literals
|
|
80
|
+
*/
|
|
81
|
+
export function stringArray(node) {
|
|
82
|
+
let astNode = /** @type {Node | undefined} */ (node);
|
|
83
|
+
|
|
84
|
+
if (astNode?.type !== "ArrayExpression") return;
|
|
85
|
+
|
|
86
|
+
/** @type {string[]} */
|
|
87
|
+
let result = [];
|
|
88
|
+
|
|
89
|
+
for (let element of astNode.elements ?? []) {
|
|
90
|
+
let value = stringValue(element);
|
|
91
|
+
|
|
92
|
+
if (value === undefined) return;
|
|
93
|
+
|
|
94
|
+
result.push(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {Node | undefined} node an ObjectExpression
|
|
102
|
+
* @param {string} name
|
|
103
|
+
* @returns {Node | undefined} the value of the property named `name`
|
|
104
|
+
*/
|
|
105
|
+
export function propertyValue(node, name) {
|
|
106
|
+
if (node?.type !== "ObjectExpression") return;
|
|
107
|
+
|
|
108
|
+
for (let property of node.properties ?? []) {
|
|
109
|
+
if (property.type !== "Property" && property.type !== "ObjectProperty") continue;
|
|
110
|
+
|
|
111
|
+
let key = property.key;
|
|
112
|
+
let keyName = key?.type === "Identifier" ? key.name : stringValue(key);
|
|
113
|
+
|
|
114
|
+
if (keyName === name) return property.value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {Node | undefined} node an ObjectExpression
|
|
120
|
+
* @returns {string[]} the names of its properties, with `...` for spreads and computed keys
|
|
121
|
+
*/
|
|
122
|
+
export function propertyNames(node) {
|
|
123
|
+
if (node?.type !== "ObjectExpression") return [];
|
|
124
|
+
|
|
125
|
+
/** @type {string[]} */
|
|
126
|
+
let names = [];
|
|
127
|
+
|
|
128
|
+
for (let property of node.properties ?? []) {
|
|
129
|
+
let key = property.key;
|
|
130
|
+
let keyName = key?.type === "Identifier" && !property.computed ? key.name : stringValue(key);
|
|
131
|
+
|
|
132
|
+
names.push(keyName ?? "...");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return names;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Maps each imported (or required) local name to the module it comes from.
|
|
140
|
+
*
|
|
141
|
+
* import { babel } from '@rollup/plugin-babel'; babel → @rollup/plugin-babel
|
|
142
|
+
* const EmberApp = require('ember-cli/lib/broccoli/ember-app');
|
|
143
|
+
*
|
|
144
|
+
* @param {Node} program
|
|
145
|
+
* @returns {Map<string, string>}
|
|
146
|
+
*/
|
|
147
|
+
export function importedNames(program) {
|
|
148
|
+
/** @type {Map<string, string>} */
|
|
149
|
+
let names = new Map();
|
|
150
|
+
|
|
151
|
+
walk(program, (node) => {
|
|
152
|
+
if (node.type === "ImportDeclaration") {
|
|
153
|
+
let source = stringValue(node.source);
|
|
154
|
+
|
|
155
|
+
if (!source) return;
|
|
156
|
+
|
|
157
|
+
for (let specifier of node.specifiers ?? []) {
|
|
158
|
+
names.set(specifier.local.name, source);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (node.type === "VariableDeclarator" && node.init) {
|
|
165
|
+
let source = requiredModule(node.init);
|
|
166
|
+
|
|
167
|
+
if (!source) return;
|
|
168
|
+
|
|
169
|
+
if (node.id?.type === "Identifier") {
|
|
170
|
+
names.set(node.id.name, source);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (node.id?.type === "ObjectPattern") {
|
|
174
|
+
for (let property of node.id.properties ?? []) {
|
|
175
|
+
if (property.value?.type === "Identifier") {
|
|
176
|
+
names.set(property.value.name, source);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return names;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {Node} node
|
|
188
|
+
* @returns {string | undefined} `x` for `require('x')`, `require('x').y`, and `await import('x')`
|
|
189
|
+
*/
|
|
190
|
+
function requiredModule(node) {
|
|
191
|
+
let current = node;
|
|
192
|
+
|
|
193
|
+
while (current?.type === "MemberExpression" || current?.type === "AwaitExpression") {
|
|
194
|
+
current = current.type === "AwaitExpression" ? current.argument : current.object;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (current?.type === "ImportExpression") {
|
|
198
|
+
return stringValue(current.source);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
current?.type === "CallExpression" &&
|
|
203
|
+
current.callee?.type === "Identifier" &&
|
|
204
|
+
current.callee.name === "require"
|
|
205
|
+
) {
|
|
206
|
+
return stringValue(current.arguments?.[0]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @param {string} source
|
|
212
|
+
* @param {Node} node
|
|
213
|
+
* @returns {string} the node's source text
|
|
214
|
+
*/
|
|
215
|
+
export function sourceOf(source, node) {
|
|
216
|
+
return source.slice(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
|
|
217
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { remove } from "./files.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tooling that the old blueprints generate, by the layer that replaces it.
|
|
8
|
+
*
|
|
9
|
+
* When the user selects the layer, the migration removes the old tooling,
|
|
10
|
+
* so that the layer can write its own.
|
|
11
|
+
* Otherwise the old tooling stays as it is.
|
|
12
|
+
*
|
|
13
|
+
* @type {Record<string, { files: string[], scripts: string[], devDependencies: string[] }>}
|
|
14
|
+
*/
|
|
15
|
+
const REPLACED = {
|
|
16
|
+
"eslint-bundled": {
|
|
17
|
+
files: [
|
|
18
|
+
"eslint.config.mjs",
|
|
19
|
+
"eslint.config.cjs",
|
|
20
|
+
"eslint.config.js",
|
|
21
|
+
".eslintrc.js",
|
|
22
|
+
".eslintrc.cjs",
|
|
23
|
+
".eslintrc.json",
|
|
24
|
+
".eslintrc",
|
|
25
|
+
".eslintignore",
|
|
26
|
+
],
|
|
27
|
+
scripts: ["lint:js", "lint:js:fix"],
|
|
28
|
+
devDependencies: [
|
|
29
|
+
"@babel/eslint-parser",
|
|
30
|
+
"@eslint/js",
|
|
31
|
+
"@typescript-eslint/eslint-plugin",
|
|
32
|
+
"@typescript-eslint/parser",
|
|
33
|
+
"eslint-config-prettier",
|
|
34
|
+
"eslint-plugin-decorator-position",
|
|
35
|
+
"eslint-plugin-ember",
|
|
36
|
+
"eslint-plugin-import",
|
|
37
|
+
"eslint-plugin-n",
|
|
38
|
+
"eslint-plugin-node",
|
|
39
|
+
"eslint-plugin-prettier",
|
|
40
|
+
"eslint-plugin-qunit",
|
|
41
|
+
"eslint-plugin-warp-drive",
|
|
42
|
+
"globals",
|
|
43
|
+
"typescript-eslint",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
prettier: {
|
|
47
|
+
files: [
|
|
48
|
+
".prettierrc.mjs",
|
|
49
|
+
".prettierrc.cjs",
|
|
50
|
+
".prettierrc.js",
|
|
51
|
+
".prettierrc.json",
|
|
52
|
+
".prettierrc",
|
|
53
|
+
],
|
|
54
|
+
scripts: ["lint:format"],
|
|
55
|
+
devDependencies: [],
|
|
56
|
+
},
|
|
57
|
+
"github-actions": {
|
|
58
|
+
files: [".github/workflows/ci.yml"],
|
|
59
|
+
scripts: [],
|
|
60
|
+
devDependencies: [],
|
|
61
|
+
},
|
|
62
|
+
// Glint's own CLI and environments, which the typescript layer's setup does not use
|
|
63
|
+
typescript: {
|
|
64
|
+
files: [],
|
|
65
|
+
scripts: [],
|
|
66
|
+
devDependencies: [
|
|
67
|
+
"@glint/core",
|
|
68
|
+
"@glint/environment-ember-loose",
|
|
69
|
+
"@glint/environment-ember-template-imports",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {import('#utils/project.js').Project} project
|
|
76
|
+
* @param {import('#types').MigrationReport} report
|
|
77
|
+
*/
|
|
78
|
+
export async function removeToolingOfWantedLayers(project, report) {
|
|
79
|
+
for (let [layer, replaced] of Object.entries(REPLACED)) {
|
|
80
|
+
if (!project.wantsLayer(layer)) continue;
|
|
81
|
+
|
|
82
|
+
await remove(project, replaced.files);
|
|
83
|
+
await packageJson.removeDevDependencies(replaced.devDependencies, project.directory);
|
|
84
|
+
await packageJson.modify((json) => {
|
|
85
|
+
for (let script of replaced.scripts) {
|
|
86
|
+
delete json.scripts?.[script];
|
|
87
|
+
}
|
|
88
|
+
}, project.directory);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!project.wantsLayer("typescript")) return;
|
|
92
|
+
|
|
93
|
+
let manifest = await packageJson.read(project.directory);
|
|
94
|
+
/** @type {string[]} */
|
|
95
|
+
let glint = [];
|
|
96
|
+
|
|
97
|
+
for (let [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
98
|
+
// the typescript layer writes its own lint:types
|
|
99
|
+
if (name !== "lint:types" && /(^|\s|&&)glint\b/.test(String(command)))
|
|
100
|
+
glint.push(`${name}: ${command}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (glint.length > 0) {
|
|
104
|
+
report.todo.push({
|
|
105
|
+
title: "Scripts that run the glint CLI",
|
|
106
|
+
where: glint,
|
|
107
|
+
action:
|
|
108
|
+
"Run the checker of the typescript layer instead, the way lint:types does.\n" +
|
|
109
|
+
"The migration removed @glint/core, which has the glint CLI.",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The testem config that the qunit layer writes, by project kind
|
|
116
|
+
*/
|
|
117
|
+
const QUNIT_TESTEM = {
|
|
118
|
+
app: join(import.meta.dirname, "../layers/qunit/files/testem.cjs"),
|
|
119
|
+
library: join(import.meta.dirname, "../layers/qunit/library-files/config/test/testem.cjs"),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Browser flags are what projects change in a testem config,
|
|
124
|
+
* for example to let Chrome render WebGL.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} root
|
|
127
|
+
* @param {string} file the old testem config, which the qunit layer's config replaces
|
|
128
|
+
* @param {"app" | "library"} kind
|
|
129
|
+
* @returns {import('#types').Finding | undefined} undefined when both configs pass the same flags
|
|
130
|
+
*/
|
|
131
|
+
export function changedTestemFlags(root, file, kind) {
|
|
132
|
+
if (!existsSync(join(root, file))) return;
|
|
133
|
+
|
|
134
|
+
let old = browserFlags(readFileSync(join(root, file), "utf-8"));
|
|
135
|
+
let current = browserFlags(readFileSync(QUNIT_TESTEM[kind], "utf-8"));
|
|
136
|
+
let target = kind === "library" ? "config/test/testem.cjs" : "testem.cjs";
|
|
137
|
+
|
|
138
|
+
/** @type {string[]} */
|
|
139
|
+
let where = [];
|
|
140
|
+
|
|
141
|
+
for (let flag of old) {
|
|
142
|
+
if (!current.has(flag)) where.push(`${flag}: only in ${file}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (let flag of current) {
|
|
146
|
+
if (!old.has(flag)) where.push(`${flag}: only in ${target}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (where.length === 0) return;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
title: "Browser flags that differ in the new testem config",
|
|
153
|
+
where,
|
|
154
|
+
action: `Change the flags in ${target} to the ones that your tests need.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} source
|
|
160
|
+
* @returns {Set<string>} such as `--headless`, with `--headless=new` read as `--headless`
|
|
161
|
+
*/
|
|
162
|
+
function browserFlags(source) {
|
|
163
|
+
let flags = new Set();
|
|
164
|
+
|
|
165
|
+
for (let [, flag] of source.matchAll(/["'`](--[\w-]+(?:=[^"'`]*)?)["'`]/g)) {
|
|
166
|
+
flags.add(flag === "--headless=new" ? "--headless" : flag);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return flags;
|
|
170
|
+
}
|