ember.nvp 1.9.0 → 1.11.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.
Files changed (43) hide show
  1. package/.release-plan.json +4 -4
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +65 -4
  4. package/package.json +2 -1
  5. package/packages/rolldown/package.json +6 -2
  6. package/packages/rolldown/src/dts.test.ts +105 -1
  7. package/packages/rolldown/src/isolated-declarations.ts +67 -61
  8. package/packages/rolldown/src/transform.test.ts +212 -15
  9. package/packages/rolldown/src/transform.ts +106 -26
  10. package/src/cli/args.js +105 -15
  11. package/src/cli/generator.js +2 -1
  12. package/src/cli/help.js +76 -16
  13. package/src/cli/index.js +15 -0
  14. package/src/cli/questions/layer-options.js +130 -0
  15. package/src/cli/questions/validate-option.js +106 -0
  16. package/src/layers/eslint-bundled/app.test.ts +182 -0
  17. package/src/layers/eslint-bundled/index.js +172 -0
  18. package/src/layers/expect-type/index.js +11 -1
  19. package/src/layers/github-actions/app.test.ts +1 -1
  20. package/src/layers/typescript/app.test.ts +6 -0
  21. package/src/layers/typescript/index.js +48 -148
  22. package/src/layers/typescript/typescript-6.js +102 -0
  23. package/src/layers/typescript/typescript-7.js +177 -0
  24. package/src/utils/manifest.js +13 -0
  25. package/src/utils/npm.js +27 -0
  26. package/src/utils/project.js +23 -0
  27. package/src/utils/types.ts +52 -1
  28. package/src/utils/typescript.js +147 -0
  29. package/test/ember.nvp/base-library.test.ts +11 -0
  30. package/test/ember.nvp/cli.test.ts +90 -3
  31. package/test/ember.nvp/layer-options.test.ts +249 -0
  32. package/test/ember.nvp/layer-readme.test.ts +4 -2
  33. package/test/ember.nvp/layer-typescript.test.ts +302 -0
  34. package/test/ember.nvp/layers.test.ts +1 -2
  35. package/test/ember.nvp/permutations/shared.ts +3 -4
  36. package/test/ember.nvp/validate-option.test.ts +152 -0
  37. package/test/test-helpers/permutate.test.ts +2 -7
  38. package/test/test-helpers.ts +8 -1
  39. package/src/layers/eslint-bundled-ember/index.js +0 -91
  40. package/src/layers/eslint-bundled-nvp/app.test.ts +0 -91
  41. package/src/layers/eslint-bundled-nvp/index.js +0 -91
  42. /package/src/layers/{eslint-bundled-ember/files → eslint-bundled/files/ember}/eslint.config.js +0 -0
  43. /package/src/layers/{eslint-bundled-nvp/files → eslint-bundled/files/nvp}/eslint.config.js +0 -0
@@ -0,0 +1,172 @@
1
+ import { getLatest } from "#utils/npm.js";
2
+ import { files, packageJson } from "ember-apply";
3
+ import { formatLabel } from "#utils/cli.js";
4
+ import { maybeLintWithConcurrently } from "#consolidators/linting.js";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { hasDevDeps, readManifest } from "#utils/manifest.js";
8
+ import { hasTypeScript6, syncTypeScript6, usesTypeScript7 } from "#utils/typescript.js";
9
+
10
+ /**
11
+ * The config package of each preset.
12
+ *
13
+ * Each preset also has a folder in `files/`.
14
+ *
15
+ * @type {Record<string, Record<string, string>>}
16
+ */
17
+ const PRESETS = {
18
+ ember: { "ember-eslint": "^1.0.0" },
19
+ nvp: { "@nullvoxpopuli/eslint-configs": "^7.0.0" },
20
+ };
21
+
22
+ /**
23
+ * @param {Record<string, any>} manifest
24
+ * @returns {string | undefined}
25
+ */
26
+ function presetIn(manifest) {
27
+ for (const [preset, deps] of Object.entries(PRESETS)) {
28
+ if (hasDevDeps(manifest, deps)) return preset;
29
+ }
30
+
31
+ return undefined;
32
+ }
33
+
34
+ /**
35
+ * @param {import('#utils/project.js').Project} project
36
+ * @returns {string | undefined} undefined when the layer is not selected
37
+ */
38
+ function wantedPreset(project) {
39
+ if (!project.wantsLayer("eslint-bundled")) return undefined;
40
+
41
+ return project.getLayerOptions("eslint-bundled").preset;
42
+ }
43
+
44
+ /**
45
+ * @type {import('#types').Layer}
46
+ */
47
+ export default {
48
+ label: formatLabel("ESLint", "encapsulated config"),
49
+ hint: `Minimal dependencies added to package.json`,
50
+
51
+ options: {
52
+ preset: {
53
+ type: "select",
54
+ prompt: "Which ESLint config?",
55
+ default: "ember",
56
+ options: [
57
+ {
58
+ value: "ember",
59
+ label: "ember-eslint",
60
+ hint: "Not official, but follows the official configuration",
61
+ },
62
+ {
63
+ value: "nvp",
64
+ label: "@nullvoxpopuli/eslint-configs",
65
+ hint: "NullVoxPopuli's config for apps, libraries, TypeScript, and more",
66
+ },
67
+ ],
68
+ async detect(project) {
69
+ return presetIn(await readManifest(project));
70
+ },
71
+ },
72
+ },
73
+
74
+ async run(project, { preset }) {
75
+ await files.applyFolder(join(import.meta.dirname, "files", preset), project.directory);
76
+
77
+ // Only one preset at a time, so that switching presets leaves nothing behind
78
+ for (const [other, deps] of Object.entries(PRESETS)) {
79
+ if (other === preset) continue;
80
+
81
+ await packageJson.removeDevDependencies(Object.keys(deps), project.directory);
82
+ }
83
+
84
+ await packageJson.addDevDependencies(
85
+ await getLatest({
86
+ ...PRESETS[preset],
87
+ eslint: "^10.9.1",
88
+ }),
89
+ project.directory,
90
+ );
91
+
92
+ await packageJson.addScripts(
93
+ {
94
+ "lint:eslint": "eslint . --cache",
95
+ "lint:eslint:fix": "eslint . --fix",
96
+ },
97
+ project.directory,
98
+ );
99
+
100
+ // typescript-eslint needs TypeScript 6
101
+ await syncTypeScript6(project);
102
+
103
+ await maybeLintWithConcurrently(project);
104
+ },
105
+
106
+ /**
107
+ * @overload
108
+ * @param {import('#utils/project.js').Project} project
109
+ * @param {true} explain
110
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
111
+ */
112
+ /**
113
+ * @overload
114
+ * @param {import('#utils/project.js').Project} project
115
+ * @param {boolean | undefined} [explain]
116
+ * @returns {Promise<boolean>}
117
+ */
118
+ async isSetup(project, explain) {
119
+ const reasons = [];
120
+
121
+ if (!existsSync(join(project.directory, "eslint.config.js"))) {
122
+ reasons.push("eslint.config.js is missing");
123
+
124
+ if (!explain) return false;
125
+ }
126
+
127
+ let manifest = await packageJson.read(project.directory);
128
+ let wanted = wantedPreset(project);
129
+
130
+ if (!hasDevDeps(manifest, ["eslint"])) {
131
+ reasons.push("missing required dependency: eslint");
132
+
133
+ if (!explain) return false;
134
+ }
135
+
136
+ if (wanted ? !hasDevDeps(manifest, PRESETS[wanted] ?? {}) : !presetIn(manifest)) {
137
+ reasons.push(`missing the config package of the ${wanted ?? "ember or nvp"} preset`);
138
+
139
+ if (!explain) return false;
140
+ }
141
+
142
+ if (usesTypeScript7(project, manifest) && !hasTypeScript6(manifest)) {
143
+ reasons.push("missing typescript (TypeScript 6) for typescript-eslint");
144
+
145
+ if (!explain) return false;
146
+ }
147
+
148
+ let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
149
+ script.includes("eslint"),
150
+ );
151
+
152
+ if (!scripts.some((script) => script.includes("--cache"))) {
153
+ reasons.push("missing eslint script with --cache flag");
154
+
155
+ if (!explain) return false;
156
+ }
157
+ if (!scripts.some((script) => script.includes("--fix"))) {
158
+ reasons.push("missing eslint script with --fix flag");
159
+
160
+ if (!explain) return false;
161
+ }
162
+
163
+ if (explain) {
164
+ return {
165
+ isSetup: reasons.length === 0,
166
+ reasons,
167
+ };
168
+ }
169
+
170
+ return reasons.length === 0;
171
+ },
172
+ };
@@ -2,6 +2,7 @@ import { packageJson } from "ember-apply";
2
2
  import { join } from "node:path";
3
3
  import { applyFolderTo } from "#utils/fs.js";
4
4
  import { getLatest } from "#utils/npm.js";
5
+ import { usesTypeScript7 } from "#utils/typescript.js";
5
6
 
6
7
  const deps = {
7
8
  "expect-type": "^1.3.0",
@@ -15,6 +16,10 @@ const scripts = {
15
16
  "lint:type-tests": "ember-tsc --noEmit --project type-tests",
16
17
  };
17
18
 
19
+ const typescript7Scripts = {
20
+ "lint:type-tests": "tsc --noEmit --runExternalCode --project type-tests",
21
+ };
22
+
18
23
  /**
19
24
  * @type {import('#types').Layer}
20
25
  */
@@ -30,7 +35,12 @@ export default {
30
35
 
31
36
  await applyFolderTo(join(import.meta.dirname, "files"), project);
32
37
  await packageJson.addDevDependencies(await getLatest(deps), project.directory);
33
- await packageJson.addScripts(scripts, project.directory);
38
+ await packageJson.addScripts(
39
+ usesTypeScript7(project, await packageJson.read(project.directory))
40
+ ? typescript7Scripts
41
+ : scripts,
42
+ project.directory,
43
+ );
34
44
  },
35
45
 
36
46
  /**
@@ -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-nvp", "github-actions"]);
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 { js, packageJson } from "ember-apply";
2
- import { join } from "node:path";
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
- const bases = join(import.meta.dirname, "../../bases");
10
- const appBase = join(bases, "minimal-app/files");
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 depsFor(project) {
37
- return {
38
- ...sharedDeps,
39
- ...(project.isLibrary ? libraryDeps : appDeps),
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
- * TODO:
62
- * - if jsconfig exists, switch to tsconfig
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
- if (!manifest.scripts?.["lint:types"]) {
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
- return `### TypeScript
100
+ async readme(project) {
101
+ let setup = await setupFor(project);
129
102
 
130
- This project uses TypeScript and Glint for static type checking.
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
+ }