ember.nvp 1.8.0 → 1.10.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 +15 -35
- package/CHANGELOG.md +33 -0
- package/README.md +73 -4
- package/package.json +2 -1
- package/packages/rolldown/package.json +6 -2
- package/packages/rolldown/src/dts.test.ts +105 -1
- package/packages/rolldown/src/isolated-declarations.ts +67 -61
- package/packages/rolldown/src/transform.test.ts +212 -15
- package/packages/rolldown/src/transform.ts +106 -26
- package/src/cli/args.js +170 -48
- package/src/cli/generator.js +2 -1
- package/src/cli/help.js +82 -0
- package/src/cli/index.js +15 -0
- package/src/cli/questions/layer-options.js +130 -0
- package/src/cli/questions/validate-option.js +106 -0
- package/src/layers/eslint-bundled/app.test.ts +182 -0
- package/src/layers/eslint-bundled/index.js +172 -0
- package/src/layers/expect-type/files/type-tests/index.test.ts +4 -0
- package/src/layers/expect-type/files/type-tests/tsconfig.json +8 -0
- package/src/layers/expect-type/index.js +114 -0
- package/src/layers/github-actions/app.test.ts +1 -1
- package/src/layers/typescript/app.test.ts +6 -0
- package/src/layers/typescript/index.js +48 -148
- package/src/layers/typescript/typescript-6.js +102 -0
- package/src/layers/typescript/typescript-7.js +177 -0
- package/src/utils/manifest.js +13 -0
- package/src/utils/npm.js +27 -0
- package/src/utils/project.js +23 -0
- package/src/utils/types.ts +52 -1
- package/src/utils/typescript.js +147 -0
- package/test/ember.nvp/base-library.test.ts +11 -0
- package/test/ember.nvp/cli.test.ts +142 -1
- package/test/ember.nvp/layer-expect-type.test.ts +113 -0
- package/test/ember.nvp/layer-options.test.ts +248 -0
- package/test/ember.nvp/layer-readme.test.ts +4 -2
- package/test/ember.nvp/layer-typescript.test.ts +302 -0
- package/test/ember.nvp/layers.test.ts +2 -2
- package/test/ember.nvp/permutations/shared.ts +6 -6
- package/test/ember.nvp/validate-option.test.ts +152 -0
- package/test/test-helpers/permutate.test.ts +2 -7
- package/test/test-helpers.ts +12 -2
- package/vitest.config.js +7 -1
- package/src/layers/eslint-bundled-ember/index.js +0 -91
- package/src/layers/eslint-bundled-nvp/app.test.ts +0 -91
- package/src/layers/eslint-bundled-nvp/index.js +0 -91
- /package/src/layers/{eslint-bundled-ember/files → eslint-bundled/files/ember}/eslint.config.js +0 -0
- /package/src/layers/{eslint-bundled-nvp/files → eslint-bundled/files/nvp}/eslint.config.js +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { applyFolderTo } from "#utils/fs.js";
|
|
4
|
+
import { getLatest } from "#utils/npm.js";
|
|
5
|
+
import { usesTypeScript7 } from "#utils/typescript.js";
|
|
6
|
+
|
|
7
|
+
const deps = {
|
|
8
|
+
"expect-type": "^1.3.0",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The library tsconfig only includes `src` (it drives declaration output),
|
|
13
|
+
* so type tests get their own project.
|
|
14
|
+
*/
|
|
15
|
+
const scripts = {
|
|
16
|
+
"lint:type-tests": "ember-tsc --noEmit --project type-tests",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const typescript7Scripts = {
|
|
20
|
+
"lint:type-tests": "tsc --noEmit --runExternalCode --project type-tests",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @type {import('#types').Layer}
|
|
25
|
+
*/
|
|
26
|
+
export default {
|
|
27
|
+
label: "expect-type",
|
|
28
|
+
hint: "type tests for the published API",
|
|
29
|
+
|
|
30
|
+
async run(project) {
|
|
31
|
+
// Apps aren't published, so they have no public API to pin
|
|
32
|
+
if (!project.isLibrary) return;
|
|
33
|
+
|
|
34
|
+
if (!(await project.hasOrWantsLayer("typescript"))) return;
|
|
35
|
+
|
|
36
|
+
await applyFolderTo(join(import.meta.dirname, "files"), project);
|
|
37
|
+
await packageJson.addDevDependencies(await getLatest(deps), project.directory);
|
|
38
|
+
await packageJson.addScripts(
|
|
39
|
+
usesTypeScript7(project, await packageJson.read(project.directory))
|
|
40
|
+
? typescript7Scripts
|
|
41
|
+
: scripts,
|
|
42
|
+
project.directory,
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @overload
|
|
48
|
+
* @param {import('#utils/project.js').Project} project
|
|
49
|
+
* @param {true} explain
|
|
50
|
+
* @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* @overload
|
|
54
|
+
* @param {import('#utils/project.js').Project} project
|
|
55
|
+
* @param {boolean | undefined} [explain]
|
|
56
|
+
* @returns {Promise<boolean>}
|
|
57
|
+
*/
|
|
58
|
+
async isSetup(project, explain) {
|
|
59
|
+
/** @type {string[]} */
|
|
60
|
+
const reasons = [];
|
|
61
|
+
|
|
62
|
+
// Nothing to set up outside TypeScript libraries,
|
|
63
|
+
// so nothing can be missing
|
|
64
|
+
if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) {
|
|
65
|
+
return explain ? { isSetup: true, reasons } : true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!project.hasFile("type-tests/tsconfig.json")) {
|
|
69
|
+
if (!explain) return false;
|
|
70
|
+
|
|
71
|
+
reasons.push("type-tests/tsconfig.json is missing");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let manifest = await packageJson.read(project.directory);
|
|
75
|
+
|
|
76
|
+
for (let script of Object.keys(scripts)) {
|
|
77
|
+
if (!manifest.scripts?.[script]) {
|
|
78
|
+
if (!explain) return false;
|
|
79
|
+
|
|
80
|
+
reasons.push(`package.json is missing the "${script}" script`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let dep of Object.keys(deps)) {
|
|
85
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
86
|
+
if (!explain) return false;
|
|
87
|
+
|
|
88
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (explain) {
|
|
93
|
+
return {
|
|
94
|
+
isSetup: reasons.length === 0,
|
|
95
|
+
reasons,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return reasons.length === 0;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {import('#utils/project.js').Project} project
|
|
104
|
+
*/
|
|
105
|
+
async readme(project) {
|
|
106
|
+
if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) return;
|
|
107
|
+
|
|
108
|
+
return `### Type tests
|
|
109
|
+
|
|
110
|
+
Type tests in \`type-tests/\` use [expect-type](https://github.com/mmkal/expect-type) to pin the types of the public API.
|
|
111
|
+
|
|
112
|
+
- \`${project.runPrefix} lint:type-tests\` - Typecheck the type tests`;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -32,7 +32,7 @@ for (let packageManager of ["pnpm", "npm"] as const) {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it("after emitting with an eslint layer", async () => {
|
|
35
|
-
await reapply(project, ["eslint-bundled
|
|
35
|
+
await reapply(project, ["eslint-bundled", "github-actions"]);
|
|
36
36
|
|
|
37
37
|
let result = await githubActionsLayer.isSetup(project);
|
|
38
38
|
|
|
@@ -8,6 +8,9 @@ const expect = hardExpect.soft;
|
|
|
8
8
|
|
|
9
9
|
let githubActionsLayer = layers.find((layer) => layer.name === "github-actions")!;
|
|
10
10
|
|
|
11
|
+
// version 7 is covered in test/ember.nvp/layer-typescript.test.ts
|
|
12
|
+
const options = { typescript: { version: "6" } };
|
|
13
|
+
|
|
11
14
|
describe("starting with javascript", () => {
|
|
12
15
|
let project: Project;
|
|
13
16
|
|
|
@@ -15,6 +18,7 @@ describe("starting with javascript", () => {
|
|
|
15
18
|
project = await generate({
|
|
16
19
|
type: "app",
|
|
17
20
|
packageManager: "pnpm",
|
|
21
|
+
options,
|
|
18
22
|
layers: ["git"],
|
|
19
23
|
});
|
|
20
24
|
});
|
|
@@ -41,6 +45,7 @@ describe("starting with typescript", () => {
|
|
|
41
45
|
project = await generate({
|
|
42
46
|
type: "app",
|
|
43
47
|
packageManager: "pnpm",
|
|
48
|
+
options,
|
|
44
49
|
layers: ["typescript", "git"],
|
|
45
50
|
});
|
|
46
51
|
});
|
|
@@ -67,6 +72,7 @@ describe("package.json scripts", () => {
|
|
|
67
72
|
project = await generate({
|
|
68
73
|
type: "app",
|
|
69
74
|
packageManager: "pnpm",
|
|
75
|
+
options,
|
|
70
76
|
layers: ["typescript", "git"],
|
|
71
77
|
});
|
|
72
78
|
|
|
@@ -1,43 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { cp } from "node:fs/promises";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { hasConfiguredTSBabel, prependPlugin } from "#utils/babel.js";
|
|
6
|
-
import { getLatest } from "#utils/npm.js";
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { readManifest } from "#utils/manifest.js";
|
|
7
3
|
import { isLibraryType } from "#utils/project-type.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const extensionBase = join(bases, "minimal-extension/files");
|
|
12
|
-
const libraryBase = join(bases, "minimal-library/files");
|
|
13
|
-
const customElementBase = join(bases, "minimal-custom-element/files");
|
|
14
|
-
|
|
15
|
-
const sharedDeps = {
|
|
16
|
-
"@glint/ember-tsc": "^1.0.8",
|
|
17
|
-
"@glint/template": "^1.7.3",
|
|
18
|
-
"@glint/tsserver-plugin": "^2.0.8",
|
|
19
|
-
typescript: "^6.0.3",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const appDeps = {
|
|
23
|
-
// Apps strip types via their own babel.config.js.
|
|
24
|
-
// Libraries have no babel config: ember() handles type stripping.
|
|
25
|
-
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
26
|
-
"@ember/app-tsconfig": "^2.0.0",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const libraryDeps = {
|
|
30
|
-
"@ember/library-tsconfig": "^2.0.0",
|
|
31
|
-
};
|
|
4
|
+
import { usesTypeScript7 } from "#utils/typescript.js";
|
|
5
|
+
import * as typescript6 from "./typescript-6.js";
|
|
6
|
+
import * as typescript7 from "./typescript-7.js";
|
|
32
7
|
|
|
33
8
|
/**
|
|
9
|
+
* A project that has TypeScript 7 keeps it, even when version 6 is selected.
|
|
10
|
+
* Nothing moves a project from 7 back to 6 yet.
|
|
11
|
+
*
|
|
34
12
|
* @param {import('#utils/project.js').Project} project
|
|
35
13
|
*/
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
14
|
+
async function setupFor(project) {
|
|
15
|
+
let manifest = await packageJson.read(project.directory);
|
|
16
|
+
|
|
17
|
+
return usesTypeScript7(project, manifest) ? typescript7 : typescript6;
|
|
41
18
|
}
|
|
42
19
|
|
|
43
20
|
/**
|
|
@@ -56,14 +33,38 @@ export default {
|
|
|
56
33
|
return isLibraryType(projectType);
|
|
57
34
|
},
|
|
58
35
|
|
|
36
|
+
options: {
|
|
37
|
+
version: {
|
|
38
|
+
type: "select",
|
|
39
|
+
prompt: "Which TypeScript version?",
|
|
40
|
+
default: "7",
|
|
41
|
+
options: [
|
|
42
|
+
{
|
|
43
|
+
value: "7",
|
|
44
|
+
label: "TypeScript 7.1+",
|
|
45
|
+
hint: "native tsc, checks .gts and .gjs with ember-content-mapper",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
value: "6",
|
|
49
|
+
label: "TypeScript 6",
|
|
50
|
+
hint: "checks with Glint's ember-tsc",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
async detect(project) {
|
|
54
|
+
let deps = (await readManifest(project)).devDependencies ?? {};
|
|
55
|
+
|
|
56
|
+
if (deps["@typescript/native"]) return "7";
|
|
57
|
+
if (deps.typescript) return "6";
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
59
64
|
async run(project) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*/
|
|
64
|
-
await addTSConfig(project);
|
|
65
|
-
await updatePackageJson(project);
|
|
66
|
-
await updateBabelConfig(project);
|
|
65
|
+
let setup = await setupFor(project);
|
|
66
|
+
|
|
67
|
+
await setup.run(project);
|
|
67
68
|
},
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -79,37 +80,9 @@ export default {
|
|
|
79
80
|
* @returns {Promise<boolean>}
|
|
80
81
|
*/
|
|
81
82
|
async isSetup(project, explain) {
|
|
82
|
-
const reasons = [];
|
|
83
|
-
|
|
84
|
-
if (!project.hasFile("tsconfig.json")) {
|
|
85
|
-
if (!explain) return false;
|
|
86
|
-
|
|
87
|
-
reasons.push("tsconfig.json is missing");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Only projects with their own babel config need the TS plugin in it.
|
|
91
|
-
// Without one (libraries), ember() strips types.
|
|
92
|
-
if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
|
|
93
|
-
if (!explain) return false;
|
|
94
|
-
|
|
95
|
-
reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
83
|
let manifest = await packageJson.read(project.directory);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!explain) return false;
|
|
102
|
-
|
|
103
|
-
reasons.push(`package.json is missing the "lint:types" script`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (let dep of Object.keys(depsFor(project))) {
|
|
107
|
-
if (!manifest.devDependencies?.[dep]) {
|
|
108
|
-
if (!explain) return false;
|
|
109
|
-
|
|
110
|
-
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
84
|
+
let setup = usesTypeScript7(project, manifest) ? typescript7 : typescript6;
|
|
85
|
+
let reasons = await setup.reasons(project, manifest);
|
|
113
86
|
|
|
114
87
|
if (explain) {
|
|
115
88
|
return {
|
|
@@ -124,82 +97,9 @@ export default {
|
|
|
124
97
|
/**
|
|
125
98
|
* @param {import('#utils/project.js').Project} project
|
|
126
99
|
*/
|
|
127
|
-
readme(project) {
|
|
128
|
-
|
|
100
|
+
async readme(project) {
|
|
101
|
+
let setup = await setupFor(project);
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
- \`${project.runPrefix} lint:types\` - Typecheck code with Glint/TypeScript`;
|
|
103
|
+
return setup.readme(project);
|
|
133
104
|
},
|
|
134
105
|
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* @param {import('#utils/project.js').Project} project
|
|
138
|
-
*/
|
|
139
|
-
async function updatePackageJson(project) {
|
|
140
|
-
await packageJson.modify(async (json) => {
|
|
141
|
-
json.scripts ||= {};
|
|
142
|
-
json.devDependencies ||= {};
|
|
143
|
-
|
|
144
|
-
Object.assign(json.scripts, {
|
|
145
|
-
"lint:types": "ember-tsc --noEmit",
|
|
146
|
-
});
|
|
147
|
-
Object.assign(json.devDependencies, await getLatest(depsFor(project)));
|
|
148
|
-
}, project.directory);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Copies the base's tsconfig, unless the project already has one.
|
|
153
|
-
*
|
|
154
|
-
* @param {import('#utils/project.js').Project} project
|
|
155
|
-
*/
|
|
156
|
-
async function addTSConfig(project) {
|
|
157
|
-
if (existsSync(project.path("tsconfig.json"))) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (project.type === "app") {
|
|
162
|
-
await cp(join(appBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (project.type === "extension") {
|
|
167
|
-
await cp(join(extensionBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (project.type === "library") {
|
|
172
|
-
await cp(join(libraryBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (project.type === "custom-element") {
|
|
177
|
-
await cp(join(customElementBase, "tsconfig.json"), project.path("tsconfig.json"));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* @param {import('#utils/project.js').Project} project
|
|
183
|
-
*/
|
|
184
|
-
async function updateBabelConfig(project) {
|
|
185
|
-
if (!project.hasFile("babel.config.js")) {
|
|
186
|
-
// Nothing to patch (libraries): ember() strips types.
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (await hasConfiguredTSBabel(project)) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
await prependPlugin(
|
|
195
|
-
project,
|
|
196
|
-
`[
|
|
197
|
-
"@babel/plugin-transform-typescript",
|
|
198
|
-
{
|
|
199
|
-
allExtensions: true,
|
|
200
|
-
onlyRemoveTypeImports: true,
|
|
201
|
-
allowDeclareFields: true,
|
|
202
|
-
},
|
|
203
|
-
]`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
2
|
+
import { hasConfiguredTSBabel } from "#utils/babel.js";
|
|
3
|
+
import { getLatest } from "#utils/npm.js";
|
|
4
|
+
import { addTSConfig, updateBabelConfig } from "#utils/typescript.js";
|
|
5
|
+
|
|
6
|
+
const sharedDeps = {
|
|
7
|
+
"@glint/ember-tsc": "^1.0.8",
|
|
8
|
+
"@glint/template": "^1.7.3",
|
|
9
|
+
"@glint/tsserver-plugin": "^2.0.8",
|
|
10
|
+
typescript: "^6.0.3",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const appDeps = {
|
|
14
|
+
// Apps strip types via their own babel.config.js.
|
|
15
|
+
// Libraries have no babel config: ember() handles type stripping.
|
|
16
|
+
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
17
|
+
"@ember/app-tsconfig": "^2.0.0",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const libraryDeps = {
|
|
21
|
+
"@ember/library-tsconfig": "^2.0.0",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {import('#utils/project.js').Project} project
|
|
26
|
+
*/
|
|
27
|
+
function depsFor(project) {
|
|
28
|
+
return {
|
|
29
|
+
...sharedDeps,
|
|
30
|
+
...(project.isLibrary ? libraryDeps : appDeps),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import('#utils/project.js').Project} project
|
|
36
|
+
*/
|
|
37
|
+
export async function run(project) {
|
|
38
|
+
/**
|
|
39
|
+
* TODO:
|
|
40
|
+
* - if jsconfig exists, switch to tsconfig
|
|
41
|
+
*/
|
|
42
|
+
await addTSConfig(project);
|
|
43
|
+
await updatePackageJson(project);
|
|
44
|
+
await updateBabelConfig(project);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('#utils/project.js').Project} project
|
|
49
|
+
* @param {Record<string, any>} manifest
|
|
50
|
+
* @returns {Promise<string[]>} why TypeScript 6 is not set up
|
|
51
|
+
*/
|
|
52
|
+
export async function reasons(project, manifest) {
|
|
53
|
+
const reasons = [];
|
|
54
|
+
|
|
55
|
+
if (!project.hasFile("tsconfig.json")) {
|
|
56
|
+
reasons.push("tsconfig.json is missing");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Only projects with their own babel config need the TS plugin in it.
|
|
60
|
+
// Without one (libraries), ember() strips types.
|
|
61
|
+
if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
|
|
62
|
+
reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!manifest.scripts?.["lint:types"]) {
|
|
66
|
+
reasons.push(`package.json is missing the "lint:types" script`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let dep of Object.keys(depsFor(project))) {
|
|
70
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
71
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return reasons;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {import('#utils/project.js').Project} project
|
|
80
|
+
*/
|
|
81
|
+
export function readme(project) {
|
|
82
|
+
return `### TypeScript
|
|
83
|
+
|
|
84
|
+
This project uses TypeScript and Glint for static type checking.
|
|
85
|
+
|
|
86
|
+
- \`${project.runPrefix} lint:types\` - Typecheck code with Glint/TypeScript`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {import('#utils/project.js').Project} project
|
|
91
|
+
*/
|
|
92
|
+
async function updatePackageJson(project) {
|
|
93
|
+
await packageJson.modify(async (json) => {
|
|
94
|
+
json.scripts ||= {};
|
|
95
|
+
json.devDependencies ||= {};
|
|
96
|
+
|
|
97
|
+
Object.assign(json.scripts, {
|
|
98
|
+
"lint:types": "ember-tsc --noEmit",
|
|
99
|
+
});
|
|
100
|
+
Object.assign(json.devDependencies, await getLatest(depsFor(project)));
|
|
101
|
+
}, project.directory);
|
|
102
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { packageJson, tsconfig } from "ember-apply";
|
|
2
|
+
import { hasConfiguredTSBabel } from "#utils/babel.js";
|
|
3
|
+
import { getLatest } from "#utils/npm.js";
|
|
4
|
+
import {
|
|
5
|
+
TYPESCRIPT_7,
|
|
6
|
+
addTSConfig,
|
|
7
|
+
hasTypeScript6,
|
|
8
|
+
syncTypeScript6,
|
|
9
|
+
updateBabelConfig,
|
|
10
|
+
} from "#utils/typescript.js";
|
|
11
|
+
|
|
12
|
+
const MAPPER = "ember-content-mapper";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TypeScript 7 does not load tsserver plugins.
|
|
16
|
+
* The content mapper replaces this one.
|
|
17
|
+
*/
|
|
18
|
+
const TSSERVER_PLUGIN = "@glint/tsserver-plugin";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `@typescript/native` owns the `tsc` bin.
|
|
22
|
+
* `typescript` is left to eslint, which needs TypeScript 6, see `syncTypeScript6`.
|
|
23
|
+
*
|
|
24
|
+
* The mapper transforms with `@glint/ember-tsc` and references its types.
|
|
25
|
+
* `@glint/template` types the signatures written by hand.
|
|
26
|
+
*/
|
|
27
|
+
const sharedDeps = {
|
|
28
|
+
"@glint/ember-tsc": "^1.11.0",
|
|
29
|
+
"@glint/template": "^1.7.3",
|
|
30
|
+
"@typescript/native": TYPESCRIPT_7,
|
|
31
|
+
[MAPPER]: "^0.3.1",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const appDeps = {
|
|
35
|
+
// Apps strip types via their own babel.config.js.
|
|
36
|
+
// Libraries have no babel config: ember() handles type stripping.
|
|
37
|
+
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
38
|
+
"@ember/app-tsconfig": "^2.0.0",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const libraryDeps = {
|
|
42
|
+
"@ember/library-tsconfig": "^2.0.0",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const scripts = {
|
|
46
|
+
"lint:types": "tsc --noEmit --runExternalCode",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import('#utils/project.js').Project} project
|
|
51
|
+
*/
|
|
52
|
+
function depsFor(project) {
|
|
53
|
+
return {
|
|
54
|
+
...sharedDeps,
|
|
55
|
+
...(project.isLibrary ? libraryDeps : appDeps),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {Record<string, any>} config
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function hasContentMapper(config) {
|
|
64
|
+
return (
|
|
65
|
+
Array.isArray(config.contentMappers) &&
|
|
66
|
+
config.contentMappers.some((/** @type {any} */ mapper) => mapper?.package === MAPPER)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {import('#utils/project.js').Project} project
|
|
72
|
+
*/
|
|
73
|
+
export async function run(project) {
|
|
74
|
+
await addTSConfig(project);
|
|
75
|
+
await updateTSConfig(project);
|
|
76
|
+
await updatePackageJson(project);
|
|
77
|
+
await syncTypeScript6(project);
|
|
78
|
+
await updateBabelConfig(project);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {import('#utils/project.js').Project} project
|
|
83
|
+
* @param {Record<string, any>} manifest
|
|
84
|
+
* @returns {Promise<string[]>} why TypeScript 7 is not set up
|
|
85
|
+
*/
|
|
86
|
+
export async function reasons(project, manifest) {
|
|
87
|
+
const reasons = [];
|
|
88
|
+
|
|
89
|
+
if (!project.hasFile("tsconfig.json")) {
|
|
90
|
+
reasons.push("tsconfig.json is missing");
|
|
91
|
+
} else if (!hasContentMapper(await tsconfig.read(project.directory))) {
|
|
92
|
+
reasons.push(`tsconfig.json is missing the ${MAPPER} entry in contentMappers`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Only projects with their own babel config need the TS plugin in it.
|
|
96
|
+
// Without one (libraries), ember() strips types.
|
|
97
|
+
if (project.hasFile("babel.config.js") && !(await hasConfiguredTSBabel(project))) {
|
|
98
|
+
reasons.push(`babel.config is missing @babel/plugin-transform-typescript`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!manifest.scripts?.["lint:types"]?.includes("--runExternalCode")) {
|
|
102
|
+
reasons.push(`package.json's "lint:types" script is missing --runExternalCode`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (let dep of Object.keys(depsFor(project))) {
|
|
106
|
+
if (!manifest.devDependencies?.[dep]) {
|
|
107
|
+
reasons.push(`package.json is missing ${dep} in devDependencies`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (manifest.devDependencies?.typescript && !hasTypeScript6(manifest)) {
|
|
112
|
+
reasons.push(`package.json's typescript must be @typescript/typescript6, or tsc is ambiguous`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return reasons;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {import('#utils/project.js').Project} project
|
|
120
|
+
*/
|
|
121
|
+
export function readme(project) {
|
|
122
|
+
return `### TypeScript
|
|
123
|
+
|
|
124
|
+
This project uses TypeScript 7 and [ember-content-mapper](https://github.com/NullVoxPopuli/ember-content-mapper) for static type checking.
|
|
125
|
+
|
|
126
|
+
- \`${project.runPrefix} lint:types\` - Typecheck code with TypeScript
|
|
127
|
+
- Imports of \`.gts\` and \`.gjs\` modules must include the extension
|
|
128
|
+
- Editor setup: see [ember-content-mapper's editors section](https://github.com/NullVoxPopuli/ember-content-mapper#editors)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {import('#utils/project.js').Project} project
|
|
133
|
+
*/
|
|
134
|
+
async function updateTSConfig(project) {
|
|
135
|
+
let config = await tsconfig.read(project.directory);
|
|
136
|
+
|
|
137
|
+
if (hasContentMapper(config) && !hasTSServerPlugin(config)) return;
|
|
138
|
+
|
|
139
|
+
await tsconfig.modify((json) => {
|
|
140
|
+
if (!hasContentMapper(json)) {
|
|
141
|
+
json.contentMappers ||= [];
|
|
142
|
+
json.contentMappers.push({ package: MAPPER, extensions: [".gts", ".gjs"] });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (hasTSServerPlugin(json)) {
|
|
146
|
+
json.compilerOptions.plugins = json.compilerOptions.plugins.filter(
|
|
147
|
+
(/** @type {any} */ plugin) => plugin?.name !== TSSERVER_PLUGIN,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (json.compilerOptions.plugins.length === 0) {
|
|
151
|
+
delete json.compilerOptions.plugins;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}, project.directory);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {Record<string, any>} config
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function hasTSServerPlugin(config) {
|
|
162
|
+
let plugins = config.compilerOptions?.plugins;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
Array.isArray(plugins) &&
|
|
166
|
+
plugins.some((/** @type {any} */ plugin) => plugin?.name === TSSERVER_PLUGIN)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {import('#utils/project.js').Project} project
|
|
172
|
+
*/
|
|
173
|
+
async function updatePackageJson(project) {
|
|
174
|
+
await packageJson.addScripts(scripts, project.directory);
|
|
175
|
+
await packageJson.addDevDependencies(await getLatest(depsFor(project)), project.directory);
|
|
176
|
+
await packageJson.removeDevDependencies([TSSERVER_PLUGIN], project.directory);
|
|
177
|
+
}
|
package/src/utils/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { packageJson } from "ember-apply";
|
|
1
2
|
import { hasAllKeys } from "./object.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,3 +8,15 @@ import { hasAllKeys } from "./object.js";
|
|
|
7
8
|
export function hasDevDeps(manifest, listOrObject) {
|
|
8
9
|
return hasAllKeys(manifest.devDependencies ?? {}, listOrObject);
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Like `packageJson.read`, but an empty object when there is no package.json yet
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./project.js').Project} project
|
|
16
|
+
* @returns {Promise<Record<string, any>>}
|
|
17
|
+
*/
|
|
18
|
+
export async function readManifest(project) {
|
|
19
|
+
if (!project.hasFile("package.json")) return {};
|
|
20
|
+
|
|
21
|
+
return packageJson.read(project.directory);
|
|
22
|
+
}
|