ember.nvp 1.9.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.
Files changed (43) hide show
  1. package/.release-plan.json +22 -4
  2. package/CHANGELOG.md +20 -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 +103 -14
  11. package/src/cli/generator.js +2 -1
  12. package/src/cli/help.js +66 -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 +86 -1
  31. package/test/ember.nvp/layer-options.test.ts +248 -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
package/src/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import * as p from "@clack/prompts";
4
4
  import { generateProject } from "./generator.js";
5
5
  import { askName } from "./questions/name.js";
6
6
  import { askLayers } from "./questions/layers.js";
7
+ import { askLayerOptions } from "./questions/layer-options.js";
7
8
  import { askProjectType } from "./questions/project-type.js";
8
9
  import { askPackageManager } from "./questions/package-manager.js";
9
10
  import { askPath } from "./questions/path.js";
@@ -35,6 +36,19 @@ async function main() {
35
36
  const selectedLayers = await askLayers(projectType);
36
37
  const packageManager = await askPackageManager();
37
38
 
39
+ // Layer options start from what the project already uses
40
+ const existing =
41
+ replaceOrUpdate === "update"
42
+ ? new Project(projectPath, {
43
+ name: projectName,
44
+ type: projectType,
45
+ path: projectPath,
46
+ layers: selectedLayers,
47
+ packageManager,
48
+ })
49
+ : undefined;
50
+ const layerOptions = await askLayerOptions(selectedLayers, existing);
51
+
38
52
  await askIfOK();
39
53
 
40
54
  /**
@@ -60,6 +74,7 @@ async function main() {
60
74
  path: projectPath,
61
75
  layers: selectedLayers,
62
76
  packageManager,
77
+ options: layerOptions,
63
78
  });
64
79
 
65
80
  const s = p.spinner();
@@ -0,0 +1,130 @@
1
+ import { styleText } from "node:util";
2
+ import * as p from "@clack/prompts";
3
+ import { printArgInUse, parseLayerOptionsFromParsedArgs } from "#args";
4
+ import { validateOption } from "./validate-option.js";
5
+
6
+ /**
7
+ * Prompt the user for options defined on the selected layers.
8
+ * Skips options that were explicitly provided via CLI flags.
9
+ *
10
+ * @param {import('#types').DiscoveredLayer[]} selectedLayers
11
+ * @param {import('#utils/project.js').Project} [existing] the project being updated, for `detect`
12
+ * @returns {Promise<Record<string, Record<string, any>>>}
13
+ */
14
+ export async function askLayerOptions(selectedLayers, existing) {
15
+ const cliOptions = parseLayerOptionsFromParsedArgs(selectedLayers);
16
+ /** @type {Record<string, Record<string, any>>} */
17
+ const result = {};
18
+
19
+ for (const layer of selectedLayers) {
20
+ if (!layer.options) continue;
21
+
22
+ const layerResult = (result[layer.name] ??= {});
23
+
24
+ for (const [key, schema] of Object.entries(layer.options)) {
25
+ const fromCli = cliOptions[layer.name]?.[key];
26
+
27
+ if (fromCli !== undefined) {
28
+ layerResult[key] = fromCli;
29
+ printArgInUse(`${layer.name}.${key}`, String(fromCli));
30
+ continue;
31
+ }
32
+
33
+ const message = `${styleText("magentaBright", layer.name)}: ${schema.prompt}`;
34
+ const detected = existing && schema.detect ? await schema.detect(existing) : undefined;
35
+
36
+ layerResult[key] = await ask(message, schema, detected ?? schema.default);
37
+ }
38
+ }
39
+
40
+ return result;
41
+ }
42
+
43
+ /**
44
+ * @param {string} message
45
+ * @param {import('#types').LayerOptionSchema} schema
46
+ * @param {any} initial
47
+ * @returns {Promise<unknown>}
48
+ */
49
+ async function ask(message, schema, initial) {
50
+ switch (schema.type) {
51
+ case "text":
52
+ case "number": {
53
+ const fallback = initial === undefined ? undefined : String(initial);
54
+
55
+ const answer = await p.text({
56
+ message,
57
+ placeholder: fallback,
58
+ defaultValue: fallback,
59
+ /**
60
+ * clack swaps in the defaultValue after validation,
61
+ * so an empty answer is checked as the default.
62
+ */
63
+ validate: (input) => {
64
+ const result = validateOption(schema, input || fallback || "");
65
+
66
+ return result.ok ? undefined : result.error;
67
+ },
68
+ });
69
+
70
+ // converts "number" answers, which clack returns as strings
71
+ const result = validateOption(schema, exitIfCancelled(answer));
72
+
73
+ if (!result.ok) throw new Error(result.error);
74
+
75
+ return result.value;
76
+ }
77
+
78
+ case "confirm":
79
+ return exitIfCancelled(
80
+ await p.confirm({
81
+ message,
82
+ initialValue: initial ?? true,
83
+ }),
84
+ );
85
+
86
+ case "select":
87
+ return exitIfCancelled(
88
+ await p.select({
89
+ message,
90
+ options: schema.options ?? [],
91
+ initialValue: initial,
92
+ }),
93
+ );
94
+
95
+ case "multiselect": {
96
+ // clack's multiselect has no validate, so ask again until the selection passes
97
+ while (true) {
98
+ const answer = await p.multiselect({
99
+ message,
100
+ options: schema.options ?? [],
101
+ initialValues: initial ?? [],
102
+ required: false,
103
+ });
104
+
105
+ const result = validateOption(schema, exitIfCancelled(answer));
106
+
107
+ if (result.ok) return result.value;
108
+
109
+ p.log.error(result.error);
110
+ }
111
+ }
112
+
113
+ default:
114
+ throw new Error(`Unknown option type '${String(schema.type)}'`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @template T
120
+ * @param {T | symbol} answer
121
+ * @returns {T}
122
+ */
123
+ function exitIfCancelled(answer) {
124
+ if (p.isCancel(answer)) {
125
+ p.cancel("Operation cancelled");
126
+ return process.exit(0);
127
+ }
128
+
129
+ return answer;
130
+ }
@@ -0,0 +1,106 @@
1
+ import { runValidation } from "@clack/core";
2
+
3
+ /**
4
+ * @typedef {import('#types').LayerOptionSchema} LayerOptionSchema
5
+ *
6
+ * @typedef {{ ok: true, value: any }} ValidationSuccess
7
+ * @typedef {{ ok: false, error: string }} ValidationFailure
8
+ * @typedef {ValidationSuccess | ValidationFailure} ValidationResult
9
+ */
10
+
11
+ /**
12
+ * Convert a value from a CLI flag or a prompt to the option's type,
13
+ * then run the option's own `validate`.
14
+ *
15
+ * Defaults are not applied here.
16
+ * See `Project#getLayerOptions`.
17
+ *
18
+ * @param {LayerOptionSchema} schema
19
+ * @param {unknown} rawValue
20
+ * @returns {ValidationResult}
21
+ */
22
+ export function validateOption(schema, rawValue) {
23
+ const converted = convert(schema, rawValue);
24
+
25
+ if (!converted.ok || !schema.validate) return converted;
26
+
27
+ const error = runValidation(schema.validate, converted.value);
28
+
29
+ if (!error) return converted;
30
+
31
+ return { ok: false, error: error instanceof Error ? error.message : error };
32
+ }
33
+
34
+ /**
35
+ * @param {LayerOptionSchema} schema
36
+ * @param {unknown} rawValue
37
+ * @returns {ValidationResult}
38
+ */
39
+ function convert(schema, rawValue) {
40
+ switch (schema.type) {
41
+ case "text":
42
+ return { ok: true, value: String(rawValue) };
43
+
44
+ case "number": {
45
+ const text = String(rawValue).trim();
46
+
47
+ /**
48
+ * Number() also accepts "", "0x10", and "1e3",
49
+ * which nobody means when they type a count.
50
+ */
51
+ if (!/^-?\d+(\.\d+)?$/.test(text)) {
52
+ return { ok: false, error: `'${text}' is not a number` };
53
+ }
54
+
55
+ return { ok: true, value: Number(text) };
56
+ }
57
+
58
+ case "confirm":
59
+ return typeof rawValue === "boolean"
60
+ ? { ok: true, value: rawValue }
61
+ : { ok: false, error: `'${String(rawValue)}' is not true or false` };
62
+
63
+ case "select": {
64
+ const allowed = choicesOf(schema);
65
+
66
+ return typeof rawValue === "string" && allowed.includes(rawValue)
67
+ ? { ok: true, value: rawValue }
68
+ : { ok: false, error: notAChoice(rawValue, allowed) };
69
+ }
70
+
71
+ case "multiselect": {
72
+ const allowed = choicesOf(schema);
73
+
74
+ if (!Array.isArray(rawValue)) {
75
+ return { ok: false, error: `'${String(rawValue)}' is not a list` };
76
+ }
77
+
78
+ for (const item of rawValue) {
79
+ if (!allowed.includes(item)) {
80
+ return { ok: false, error: notAChoice(item, allowed) };
81
+ }
82
+ }
83
+
84
+ return { ok: true, value: rawValue };
85
+ }
86
+
87
+ default:
88
+ return { ok: false, error: `Unknown option type '${String(schema.type)}'` };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @param {LayerOptionSchema} schema
94
+ * @returns {string[]}
95
+ */
96
+ function choicesOf(schema) {
97
+ return (schema.options ?? []).map((choice) => choice.value);
98
+ }
99
+
100
+ /**
101
+ * @param {unknown} value
102
+ * @param {string[]} allowed
103
+ */
104
+ function notAChoice(value, allowed) {
105
+ return `Invalid option '${String(value)}'. Must be one of: ${allowed.join(", ")}`;
106
+ }
@@ -0,0 +1,182 @@
1
+ import { beforeAll, describe, it, expect as hardExpect, afterAll } from "vitest";
2
+ import { expectIsSetup, generate, layers, reapply } from "#test-helpers";
3
+ import { packageJson } from "ember-apply";
4
+
5
+ import type { Project } from "ember.nvp";
6
+ import { rm } from "node:fs/promises";
7
+
8
+ const expect = hardExpect.soft;
9
+
10
+ let layer = layers.find((layer) => layer.name === "eslint-bundled")!;
11
+
12
+ const CONFIG_PACKAGES = {
13
+ ember: "ember-eslint",
14
+ nvp: "@nullvoxpopuli/eslint-configs",
15
+ };
16
+
17
+ describe.each(Object.entries(CONFIG_PACKAGES))("preset: %s", (preset, configPackage) => {
18
+ const options = { "eslint-bundled": { preset } };
19
+
20
+ describe("starting without eslint-bundled", () => {
21
+ let project: Project;
22
+
23
+ beforeAll(async () => {
24
+ project = await generate({
25
+ type: "app",
26
+ packageManager: "pnpm",
27
+ layers: ["git"],
28
+ options,
29
+ });
30
+ });
31
+
32
+ afterAll(async () => {
33
+ await rm(project.directory, { recursive: true, force: true });
34
+ });
35
+
36
+ it("applying eslint-bundled", async () => {
37
+ await reapply(project, ["eslint-bundled"]);
38
+
39
+ await expectIsSetup(project, layer);
40
+ expect(await project.gitHasDiff()).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe("starting with eslint-bundled", () => {
45
+ let project: Project;
46
+
47
+ beforeAll(async () => {
48
+ project = await generate({
49
+ type: "app",
50
+ packageManager: "pnpm",
51
+ layers: ["eslint-bundled", "git"],
52
+ options,
53
+ });
54
+ });
55
+
56
+ afterAll(async () => {
57
+ await rm(project.directory, { recursive: true, force: true });
58
+ });
59
+
60
+ it("installs only this preset's config package", async () => {
61
+ let manifest = await packageJson.read(project.directory);
62
+ let installed = Object.values(CONFIG_PACKAGES).filter(
63
+ (name) => manifest.devDependencies?.[name],
64
+ );
65
+
66
+ expect(installed).toEqual([configPackage]);
67
+ });
68
+
69
+ it("detects the preset", async () => {
70
+ expect(await layer.options?.preset?.detect?.(project)).toBe(preset);
71
+ });
72
+
73
+ it("reapplying eslint-bundled causes no changes", async () => {
74
+ await reapply(project, ["eslint-bundled"]);
75
+
76
+ await expectIsSetup(project, layer);
77
+ expect(await project.gitHasDiff()).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe("package.json scripts", () => {
82
+ let project: Project;
83
+
84
+ beforeAll(async () => {
85
+ project = await generate({
86
+ type: "app",
87
+ packageManager: "pnpm",
88
+ layers: ["eslint-bundled", "git"],
89
+ options,
90
+ });
91
+
92
+ let { exitCode } = await project.run("pnpm install");
93
+
94
+ hardExpect(exitCode).toBe(0);
95
+
96
+ await expectIsSetup(project, layer);
97
+ });
98
+
99
+ afterAll(async () => {
100
+ await rm(project.directory, { recursive: true, force: true });
101
+ });
102
+
103
+ it("linting and fixing works", async () => {
104
+ {
105
+ let { exitCode } = await project.run("pnpm lint:eslint --fix");
106
+
107
+ expect(exitCode).toBe(0);
108
+ }
109
+
110
+ {
111
+ let { exitCode } = await project.run("pnpm lint:eslint");
112
+
113
+ expect(exitCode).toBe(0);
114
+ }
115
+ });
116
+ });
117
+ });
118
+
119
+ describe.each([
120
+ ["ember", "nvp"],
121
+ ["nvp", "ember"],
122
+ ] as const)("switching presets: %s to %s", (from, to) => {
123
+ let project: Project;
124
+
125
+ beforeAll(async () => {
126
+ let before = await generate({
127
+ type: "app",
128
+ packageManager: "pnpm",
129
+ layers: ["eslint-bundled", "git"],
130
+ options: { "eslint-bundled": { preset: from } },
131
+ });
132
+
133
+ project = await generate({
134
+ directory: before.directory,
135
+ type: "app",
136
+ packageManager: "pnpm",
137
+ layers: ["eslint-bundled", "git"],
138
+ options: { "eslint-bundled": { preset: to } },
139
+ });
140
+ });
141
+
142
+ afterAll(async () => {
143
+ await rm(project.directory, { recursive: true, force: true });
144
+ });
145
+
146
+ it("replaces the config package and the config", async () => {
147
+ let manifest = await packageJson.read(project.directory);
148
+
149
+ expect(manifest.devDependencies).toHaveProperty(CONFIG_PACKAGES[to]);
150
+ expect(manifest.devDependencies).not.toHaveProperty(CONFIG_PACKAGES[from]);
151
+ expect(await project.read("eslint.config.js")).toContain(CONFIG_PACKAGES[to]);
152
+ expect(await project.read("eslint.config.js")).not.toContain(CONFIG_PACKAGES[from]);
153
+ });
154
+
155
+ it("is setup, and detects the new preset", async () => {
156
+ await expectIsSetup(project, layer);
157
+ expect(await layer.options?.preset?.detect?.(project)).toBe(to);
158
+ });
159
+
160
+ it("reapplying causes no changes", async () => {
161
+ await reapply(project, ["eslint-bundled", "git"]);
162
+
163
+ expect(await project.gitHasDiff()).toBe(false);
164
+ });
165
+
166
+ it("linting and fixing works", async () => {
167
+ let install = await project.run("pnpm install");
168
+ hardExpect(install.exitCode).toBe(0);
169
+
170
+ {
171
+ let { exitCode } = await project.run("pnpm lint:eslint --fix");
172
+
173
+ expect(exitCode).toBe(0);
174
+ }
175
+
176
+ {
177
+ let { exitCode } = await project.run("pnpm lint:eslint");
178
+
179
+ expect(exitCode).toBe(0);
180
+ }
181
+ });
182
+ });
@@ -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