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,152 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateOption } from "../../src/cli/questions/validate-option.js";
3
+ import type { LayerOptionSchema } from "../../src/utils/types.js";
4
+
5
+ describe("validateOption", () => {
6
+ describe("number validation", () => {
7
+ const schema: LayerOptionSchema = {
8
+ type: "number",
9
+ prompt: "Enter number",
10
+ default: 10,
11
+ validate: (val: number) => (val > 0 ? undefined : "Must be greater than 0"),
12
+ };
13
+
14
+ it("validates valid numbers", () => {
15
+ expect(validateOption(schema, 5)).toEqual({ ok: true, value: 5 });
16
+ expect(validateOption(schema, "42")).toEqual({ ok: true, value: 42 });
17
+ expect(validateOption(schema, " 2.5 ")).toEqual({ ok: true, value: 2.5 });
18
+ });
19
+
20
+ it.each(["", "abc", "0x10", "1e3"])("fails on '%s'", (input) => {
21
+ expect(validateOption(schema, input)).toEqual({
22
+ ok: false,
23
+ error: `'${input}' is not a number`,
24
+ });
25
+ });
26
+
27
+ it("fails custom validate function", () => {
28
+ expect(validateOption(schema, -5)).toEqual({ ok: false, error: "Must be greater than 0" });
29
+ });
30
+ });
31
+
32
+ describe("text validation", () => {
33
+ const schema: LayerOptionSchema = {
34
+ type: "text",
35
+ prompt: "Enter text",
36
+ default: "default-title",
37
+ validate: (val: string) => (val.length >= 3 ? undefined : "Minimum 3 chars"),
38
+ };
39
+
40
+ it("validates valid text", () => {
41
+ expect(validateOption(schema, "hello")).toEqual({ ok: true, value: "hello" });
42
+ });
43
+
44
+ it("fails custom validate function", () => {
45
+ expect(validateOption(schema, "hi")).toEqual({ ok: false, error: "Minimum 3 chars" });
46
+ });
47
+
48
+ it("fails with the message of an Error", () => {
49
+ const strict: LayerOptionSchema = { ...schema, validate: () => new Error("Nope") };
50
+
51
+ expect(validateOption(strict, "hello")).toEqual({ ok: false, error: "Nope" });
52
+ });
53
+ });
54
+
55
+ describe("confirm validation", () => {
56
+ const schema: LayerOptionSchema = {
57
+ type: "confirm",
58
+ prompt: "Confirm action",
59
+ default: true,
60
+ };
61
+
62
+ it("validates booleans", () => {
63
+ expect(validateOption(schema, true)).toEqual({ ok: true, value: true });
64
+ expect(validateOption(schema, false)).toEqual({ ok: true, value: false });
65
+ });
66
+
67
+ it("fails on strings", () => {
68
+ expect(validateOption(schema, "yes")).toEqual({
69
+ ok: false,
70
+ error: "'yes' is not true or false",
71
+ });
72
+ });
73
+ });
74
+
75
+ describe("select validation", () => {
76
+ const schema: LayerOptionSchema = {
77
+ type: "select",
78
+ prompt: "Select option",
79
+ default: "standard",
80
+ options: [
81
+ { label: "Standard", value: "standard" },
82
+ { label: "Deluxe", value: "deluxe" },
83
+ ],
84
+ };
85
+
86
+ it("validates valid option choice", () => {
87
+ expect(validateOption(schema, "deluxe")).toEqual({ ok: true, value: "deluxe" });
88
+ });
89
+
90
+ it("fails unlisted choice", () => {
91
+ expect(validateOption(schema, "ultra")).toEqual({
92
+ ok: false,
93
+ error: "Invalid option 'ultra'. Must be one of: standard, deluxe",
94
+ });
95
+ });
96
+ });
97
+
98
+ describe("multiselect validation", () => {
99
+ const schema: LayerOptionSchema = {
100
+ type: "multiselect",
101
+ prompt: "Select options",
102
+ options: [
103
+ { label: "A", value: "a" },
104
+ { label: "B", value: "b" },
105
+ ],
106
+ validate: (val: string[]) => (val.length > 0 ? undefined : "Select at least one"),
107
+ };
108
+
109
+ it("validates listed choices", () => {
110
+ expect(validateOption(schema, ["a", "b"])).toEqual({ ok: true, value: ["a", "b"] });
111
+ });
112
+
113
+ it("fails unlisted choice", () => {
114
+ expect(validateOption(schema, ["a", "c"])).toEqual({
115
+ ok: false,
116
+ error: "Invalid option 'c'. Must be one of: a, b",
117
+ });
118
+ });
119
+
120
+ it("fails custom validate function", () => {
121
+ expect(validateOption(schema, [])).toEqual({ ok: false, error: "Select at least one" });
122
+ });
123
+ });
124
+
125
+ describe("Standard Schema validation", () => {
126
+ // A hand-written schema, so the test needs no schema library
127
+ const positive = {
128
+ "~standard": {
129
+ version: 1 as const,
130
+ vendor: "test",
131
+ validate: (value: unknown) =>
132
+ typeof value === "number" && value > 0
133
+ ? { value }
134
+ : { issues: [{ message: "Must be positive" }] },
135
+ },
136
+ };
137
+
138
+ const schema: LayerOptionSchema = {
139
+ type: "number",
140
+ prompt: "Enter number",
141
+ validate: positive,
142
+ };
143
+
144
+ it("passes values the schema accepts", () => {
145
+ expect(validateOption(schema, "3")).toEqual({ ok: true, value: 3 });
146
+ });
147
+
148
+ it("fails with the schema's first issue", () => {
149
+ expect(validateOption(schema, "-3")).toEqual({ ok: false, error: "Must be positive" });
150
+ });
151
+ });
152
+ });
@@ -25,12 +25,7 @@ describe("permutate", () => {
25
25
  });
26
26
 
27
27
  it("enforces mutual exclusivity for eslint-prefixed entries", () => {
28
- const result = permutate([
29
- "eslint-bundled-ember",
30
- "eslint-bundled-nvp",
31
- "eslint-ejected",
32
- "other",
33
- ]);
28
+ const result = permutate(["eslint-bundled", "eslint-ejected", "other"]);
34
29
 
35
30
  // Check that no permutation has more than one eslint-prefixed entry
36
31
  for (const permutation of result) {
@@ -40,7 +35,7 @@ describe("permutate", () => {
40
35
  });
41
36
 
42
37
  it("allows permutations with 0 or 1 eslint entry", () => {
43
- const result = permutate(["eslint-bundled-ember", "eslint-bundled-nvp", "other"]);
38
+ const result = permutate(["eslint-bundled", "eslint-ejected", "other"]);
44
39
 
45
40
  const hasNoEslint = result.some((perm) => !perm.some((item) => item.startsWith("eslint-")));
46
41
  const hasOneEslint = result.some(
@@ -6,7 +6,7 @@ import { mkdtemp, readdir } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { generateProject, Project } from "ember.nvp";
8
8
  import { discoverLayers } from "#layers";
9
- import type { DiscoveredLayer, ProjectType } from "#types";
9
+ import type { Answers, DiscoveredLayer, ProjectType } from "#types";
10
10
  import { expect } from "vitest";
11
11
 
12
12
  const minimalApp = "minimal-app";
@@ -78,6 +78,7 @@ export async function reapply(project: Project, layers: string[]) {
78
78
  name: project.name,
79
79
  type: project.type,
80
80
  packageManager: project.packageManager,
81
+ options: project.desires.options,
81
82
  });
82
83
  }
83
84
 
@@ -87,12 +88,17 @@ export async function generate({
87
88
  type = "app",
88
89
  packageManager = "pnpm",
89
90
  directory,
91
+ options,
90
92
  }: {
91
93
  directory?: string;
92
94
  layers?: string[];
93
95
  name?: string;
94
96
  type?: ProjectType;
95
97
  packageManager?: "pnpm" | "npm";
98
+ /**
99
+ * Layer options, keyed by layer name
100
+ */
101
+ options?: Answers["options"];
96
102
  }): Promise<Project> {
97
103
  const tempDir = directory ?? (await mktemp(name));
98
104
 
@@ -103,6 +109,7 @@ export async function generate({
103
109
  path: tempDir,
104
110
  layers: selectedLayers,
105
111
  packageManager,
112
+ options,
106
113
  });
107
114
 
108
115
  await generateProject(project);
@@ -1,91 +0,0 @@
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 } from "#utils/manifest.js";
8
-
9
- /**
10
- * @type {import('#types').Layer}
11
- */
12
- export default {
13
- label: formatLabel("ESLint", "ember's encapsulated config"),
14
- hint: `Minimal dependencies added to package.json. Not official, but follows the official configuration`,
15
-
16
- async run(project) {
17
- await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
18
-
19
- await packageJson.addDevDependencies(
20
- await getLatest({
21
- "ember-eslint": "^1.0.0",
22
- eslint: "^10.9.1",
23
- }),
24
- project.directory,
25
- );
26
-
27
- await packageJson.addScripts(
28
- {
29
- "lint:eslint": "eslint . --cache",
30
- "lint:eslint:fix": "eslint . --fix",
31
- },
32
- project.directory,
33
- );
34
-
35
- await maybeLintWithConcurrently(project);
36
- },
37
-
38
- /**
39
- * @overload
40
- * @param {import('#utils/project.js').Project} project
41
- * @param {true} explain
42
- * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
43
- */
44
- /**
45
- * @overload
46
- * @param {import('#utils/project.js').Project} project
47
- * @param {boolean | undefined} [explain]
48
- * @returns {Promise<boolean>}
49
- */
50
- async isSetup(project, explain) {
51
- const reasons = [];
52
-
53
- if (!existsSync(join(project.directory, "eslint.config.js"))) {
54
- reasons.push("eslint.config.js is missing");
55
-
56
- if (!explain) return false;
57
- }
58
-
59
- let manifest = await packageJson.read(project.directory);
60
-
61
- if (!hasDevDeps(manifest, ["ember-eslint", "eslint"])) {
62
- reasons.push("missing required dependencies: ember-eslint, eslint");
63
-
64
- if (!explain) return false;
65
- }
66
-
67
- let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
68
- script.includes("eslint"),
69
- );
70
-
71
- if (!scripts.some((script) => script.includes("--cache"))) {
72
- reasons.push("missing eslint script with --cache flag");
73
-
74
- if (!explain) return false;
75
- }
76
- if (!scripts.some((script) => script.includes("--fix"))) {
77
- reasons.push("missing eslint script with --fix flag");
78
-
79
- if (!explain) return false;
80
- }
81
-
82
- if (explain) {
83
- return {
84
- isSetup: reasons.length === 0,
85
- reasons,
86
- };
87
- }
88
-
89
- return reasons.length === 0;
90
- },
91
- };
@@ -1,91 +0,0 @@
1
- import { beforeAll, describe, it, expect as hardExpect, afterAll } from "vitest";
2
- import { expectIsSetup, generate, layers, reapply } from "#test-helpers";
3
-
4
- import type { Project } from "ember.nvp";
5
- import { rm } from "node:fs/promises";
6
-
7
- const expect = hardExpect.soft;
8
-
9
- let layer = layers.find((layer) => layer.name === "eslint-bundled-nvp")!;
10
-
11
- describe("starting without eslint-bundled-nvp", () => {
12
- let project: Project;
13
-
14
- beforeAll(async () => {
15
- project = await generate({
16
- type: "app",
17
- packageManager: "pnpm",
18
- layers: ["git"],
19
- });
20
- });
21
-
22
- afterAll(async () => {
23
- await rm(project.directory, { recursive: true, force: true });
24
- });
25
-
26
- it("applying eslint-bundled-nvp", async () => {
27
- await reapply(project, ["eslint-bundled-nvp"]);
28
-
29
- await expectIsSetup(project, layer);
30
- expect(await project.gitHasDiff()).toBe(false);
31
- });
32
- });
33
-
34
- describe("starting with eslint-bundled-nvp", () => {
35
- let project: Project;
36
-
37
- beforeAll(async () => {
38
- project = await generate({
39
- type: "app",
40
- packageManager: "pnpm",
41
- layers: ["eslint-bundled-nvp", "git"],
42
- });
43
- });
44
-
45
- afterAll(async () => {
46
- await rm(project.directory, { recursive: true, force: true });
47
- });
48
-
49
- it("reapplying eslint-bundled-nvp causes no changes", async () => {
50
- await reapply(project, ["eslint-bundled-nvp"]);
51
-
52
- await expectIsSetup(project, layer);
53
- expect(await project.gitHasDiff()).toBe(false);
54
- });
55
- });
56
-
57
- describe("package.json scripts", () => {
58
- let project: Project;
59
-
60
- beforeAll(async () => {
61
- project = await generate({
62
- type: "app",
63
- packageManager: "pnpm",
64
- layers: ["eslint-bundled-nvp", "git"],
65
- });
66
-
67
- let { exitCode } = await project.run("pnpm install");
68
-
69
- hardExpect(exitCode).toBe(0);
70
-
71
- await expectIsSetup(project, layer);
72
- });
73
-
74
- afterAll(async () => {
75
- await rm(project.directory, { recursive: true, force: true });
76
- });
77
-
78
- it("linting and fixing works", async () => {
79
- {
80
- let { exitCode } = await project.run("pnpm lint:eslint --fix");
81
-
82
- expect(exitCode).toBe(0);
83
- }
84
-
85
- {
86
- let { exitCode } = await project.run("pnpm lint:eslint");
87
-
88
- expect(exitCode).toBe(0);
89
- }
90
- });
91
- });
@@ -1,91 +0,0 @@
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 } from "#utils/manifest.js";
8
-
9
- /**
10
- * @type {import('#types').Layer}
11
- */
12
- export default {
13
- label: formatLabel("ESLint", "NullVoxPopuli's encapsulated config"),
14
- hint: `Minimal dependencies added to package.json`,
15
-
16
- async run(project) {
17
- await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
18
-
19
- await packageJson.addDevDependencies(
20
- await getLatest({
21
- "@nullvoxpopuli/eslint-configs": "^7.0.0",
22
- eslint: "^10.9.1",
23
- }),
24
- project.directory,
25
- );
26
-
27
- await packageJson.addScripts(
28
- {
29
- "lint:eslint": "eslint . --cache",
30
- "lint:eslint:fix": "eslint . --fix",
31
- },
32
- project.directory,
33
- );
34
-
35
- await maybeLintWithConcurrently(project);
36
- },
37
-
38
- /**
39
- * @overload
40
- * @param {import('#utils/project.js').Project} project
41
- * @param {true} explain
42
- * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
43
- */
44
- /**
45
- * @overload
46
- * @param {import('#utils/project.js').Project} project
47
- * @param {boolean | undefined} [explain]
48
- * @returns {Promise<boolean>}
49
- */
50
- async isSetup(project, explain) {
51
- const reasons = [];
52
-
53
- if (!existsSync(join(project.directory, "eslint.config.js"))) {
54
- reasons.push("eslint.config.js is missing");
55
-
56
- if (!explain) return false;
57
- }
58
-
59
- let manifest = await packageJson.read(project.directory);
60
-
61
- if (!hasDevDeps(manifest, ["@nullvoxpopuli/eslint-configs", "eslint"])) {
62
- reasons.push("missing required dependencies: @nullvoxpopuli/eslint-configs, eslint");
63
-
64
- if (!explain) return false;
65
- }
66
-
67
- let scripts = Object.values(manifest.scripts ?? {}).filter((script) =>
68
- script.includes("eslint"),
69
- );
70
-
71
- if (!scripts.some((script) => script.includes("--cache"))) {
72
- reasons.push("missing eslint script with --cache flag");
73
-
74
- if (!explain) return false;
75
- }
76
- if (!scripts.some((script) => script.includes("--fix"))) {
77
- reasons.push("missing eslint script with --fix flag");
78
-
79
- if (!explain) return false;
80
- }
81
-
82
- if (explain) {
83
- return {
84
- isSetup: reasons.length === 0,
85
- reasons,
86
- };
87
- }
88
-
89
- return reasons.length === 0;
90
- },
91
- };