ember.nvp 1.8.0 → 1.9.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.
@@ -2,8 +2,8 @@
2
2
  "solution": {
3
3
  "ember.nvp": {
4
4
  "impact": "minor",
5
- "oldVersion": "1.7.1",
6
- "newVersion": "1.8.0",
5
+ "oldVersion": "1.8.0",
6
+ "newVersion": "1.9.0",
7
7
  "tagName": "latest",
8
8
  "constraints": [
9
9
  {
@@ -14,52 +14,14 @@
14
14
  "pkgJSONPath": "./package.json"
15
15
  },
16
16
  "@nullvoxpopuli/ember-build-tooling-utils": {
17
- "impact": "minor",
18
- "oldVersion": "1.1.1",
19
- "newVersion": "1.2.0",
20
- "tagName": "latest",
21
- "constraints": [
22
- {
23
- "impact": "minor",
24
- "reason": "Appears in changelog section :rocket: Enhancement"
25
- }
26
- ],
27
- "pkgJSONPath": "./packages/build-tooling-utils/package.json"
17
+ "oldVersion": "1.2.0"
28
18
  },
29
19
  "@nullvoxpopuli/ember-rolldown": {
30
- "impact": "minor",
31
- "oldVersion": "2.8.1",
32
- "newVersion": "2.9.0",
33
- "tagName": "latest",
34
- "constraints": [
35
- {
36
- "impact": "patch",
37
- "reason": "Has dependency `workspace:*` on @nullvoxpopuli/ember-build-tooling-utils"
38
- },
39
- {
40
- "impact": "minor",
41
- "reason": "Appears in changelog section :rocket: Enhancement"
42
- }
43
- ],
44
- "pkgJSONPath": "./packages/rolldown/package.json"
20
+ "oldVersion": "2.9.0"
45
21
  },
46
22
  "@nullvoxpopuli/ember-vite": {
47
- "impact": "minor",
48
- "oldVersion": "1.1.1",
49
- "newVersion": "1.2.0",
50
- "tagName": "latest",
51
- "constraints": [
52
- {
53
- "impact": "patch",
54
- "reason": "Has dependency `workspace:*` on @nullvoxpopuli/ember-build-tooling-utils"
55
- },
56
- {
57
- "impact": "minor",
58
- "reason": "Appears in changelog section :rocket: Enhancement"
59
- }
60
- ],
61
- "pkgJSONPath": "./packages/vite/package.json"
23
+ "oldVersion": "1.2.0"
62
24
  }
63
25
  },
64
- "description": "## Release (2026-09-21)\n\n* ember.nvp 1.8.0 (minor)\n* @nullvoxpopuli/ember-build-tooling-utils 1.2.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.9.0 (minor)\n* @nullvoxpopuli/ember-vite 1.2.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`, `@nullvoxpopuli/ember-build-tooling-utils`, `@nullvoxpopuli/ember-rolldown`, `@nullvoxpopuli/ember-vite`\n * [#135](https://github.com/NullVoxPopuli/ember.nvp/pull/135) Accept @babel/core 8 in the plugin packages ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 1\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
26
+ "description": "## Release (2026-09-28)\n\n* ember.nvp 1.9.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#139](https://github.com/NullVoxPopuli/ember.nvp/pull/139) CLI help ([@tcjr](https://github.com/tcjr))\n * [#137](https://github.com/NullVoxPopuli/ember.nvp/pull/137) Add an expect-type layer for type tests ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n\n#### Committers: 2\n- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n- Tom Carter ([@tcjr](https://github.com/tcjr))\n"
65
27
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## Release (2026-09-28)
4
+
5
+ * ember.nvp 1.9.0 (minor)
6
+
7
+ #### :rocket: Enhancement
8
+ * `ember.nvp`
9
+ * [#139](https://github.com/NullVoxPopuli/ember.nvp/pull/139) CLI help ([@tcjr](https://github.com/tcjr))
10
+ * [#137](https://github.com/NullVoxPopuli/ember.nvp/pull/137) Add an expect-type layer for type tests ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
11
+
12
+ #### Committers: 2
13
+ - @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
14
+ - Tom Carter ([@tcjr](https://github.com/tcjr))
15
+
3
16
  ## Release (2026-09-21)
4
17
 
5
18
  * ember.nvp 1.8.0 (minor)
package/README.md CHANGED
@@ -172,6 +172,14 @@ Code formatting with:
172
172
 
173
173
  Super experimental vitest setup using [ember-vitest](https://github.com/NullVoxPopuli/ember-vitest)
174
174
 
175
+ ### expect-type (optional)
176
+
177
+ Type tests for TypeScript libraries with [expect-type](https://github.com/mmkal/expect-type).
178
+
179
+ - Tests live in `type-tests/`, with their own tsconfig.
180
+ - `lint:type-tests` typechecks them, and `lint` runs it too.
181
+ - JavaScript projects and apps get nothing.
182
+
175
183
  ## Architecture
176
184
 
177
185
  ### How Layers Work
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember.nvp",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "ember project generator, alternative to the official blueprints, but more modern -- hopefully one day to upstream back in to ember-cli / official blueprints",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/args.js CHANGED
@@ -1,48 +1,81 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { styleText, parseArgs } from "node:util";
3
+ import { printHelp } from "./help.js";
3
4
 
4
- const { values } = parseArgs({
5
- options: {
6
- name: {
7
- type: "string",
8
- },
5
+ export const coreOptions = /** @type {const} */ ({
6
+ help: {
7
+ type: "boolean",
8
+ short: "h",
9
+ description: "Show CLI help and option details",
10
+ },
9
11
 
10
- path: {
11
- type: "string",
12
- },
12
+ name: {
13
+ type: "string",
14
+ description: "Name of the project",
15
+ },
16
+
17
+ path: {
18
+ type: "string",
19
+ description: "Target directory path for the project",
20
+ },
13
21
 
14
- type: {
15
- type: "string",
16
- choices: ["app", "addon", "library", "extension", "custom-element"],
17
- },
22
+ type: {
23
+ type: "string",
24
+ choices: ["app", "addon", "library", "extension", "custom-element"],
25
+ description: "Type of project to generate",
26
+ },
18
27
 
19
- confirm: {
20
- type: "string",
21
- choices: ["yes", "no"],
22
- },
28
+ confirm: {
29
+ type: "string",
30
+ choices: ["yes", "no"],
31
+ description: "Bypass target confirmation step",
32
+ },
23
33
 
24
- layers: {
25
- type: "string",
26
- multiple: true,
27
- },
34
+ layers: {
35
+ type: "string",
36
+ multiple: true,
37
+ description: "Layers to apply to the project (repeat for multiple layers)",
38
+ },
28
39
 
29
- packageManager: {
30
- type: "string",
31
- choices: ["npm", "pnpm"],
32
- },
40
+ packageManager: {
41
+ type: "string",
42
+ choices: ["npm", "pnpm"],
43
+ description: "Package manager to configure for the project",
44
+ },
33
45
 
34
- replaceOrUpdate: {
35
- type: "string",
36
- choices: ["replace", "update"],
37
- },
46
+ replaceOrUpdate: {
47
+ type: "string",
48
+ choices: ["replace", "update"],
49
+ description: "Strategy to use if target directory exists",
50
+ },
38
51
 
39
- write: {
40
- type: "string",
41
- choices: ["yes", "no"],
42
- },
52
+ write: {
53
+ type: "string",
54
+ choices: ["yes", "no"],
55
+ description: "Confirm writing changes to disk",
43
56
  },
44
57
  });
45
58
 
59
+ /**
60
+ * This lightweight check is here because Node's `parseArgs` is in strict mode.
61
+ * If a user runs `npx ember.nvp --help` alongside invalid or unknown flags,
62
+ * standard strict parsing would throw a `TypeError (ERR_PARSE_ARGS_UNKNOWN_OPTION)`
63
+ * before reaching any help handler. Pre-checking raw arguments ensures `--help`
64
+ * always prints successfully regardless of invalid flags.
65
+ */
66
+ const isHelpRequested = process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h");
67
+ if (isHelpRequested) {
68
+ printHelp(coreOptions);
69
+ process.exit(0);
70
+ }
71
+
72
+ /**
73
+ * The CLI options are parsed using Node's `parseArgs` in default strict mode.
74
+ */
75
+ const { values } = parseArgs({
76
+ options: coreOptions,
77
+ });
78
+
46
79
  const { replaceOrUpdate, name, type, layers = [], packageManager, path, confirm, write } = values;
47
80
 
48
81
  export const answers = {
@@ -0,0 +1,32 @@
1
+ import { styleText } from "node:util";
2
+
3
+ /**
4
+ * Print help text to stdout.
5
+ *
6
+ * @param {Record<string, any>} coreOptions
7
+ */
8
+ export function printHelp(coreOptions) {
9
+ const title = styleText(["bgCyan", "black"], " ember.nvp ");
10
+ console.log(`${title}\n`);
11
+ console.log(`${styleText("bold", "Usage:")} npx ember.nvp [options]\n`);
12
+
13
+ console.log(styleText("bold", "Core Options:"));
14
+ for (const [name, config] of Object.entries(coreOptions)) {
15
+ const flag = `--${name}`;
16
+ const alias = config.short ? `-${config.short}, ` : " ";
17
+ const typeStr = config.type ? styleText("dim", `<${config.type}>`) : "";
18
+ const desc = config.description || "";
19
+ const choices = config.choices
20
+ ? styleText(
21
+ "yellow",
22
+ ` [choices: ${config.choices.map((/** @type {string} */ c) => `"${c}"`).join(", ")}]`,
23
+ )
24
+ : "";
25
+
26
+ console.log(` ${styleText("cyan", alias)}${styleText("cyan", flag)} ${typeStr}`);
27
+ if (desc || choices) {
28
+ console.log(` ${desc}${choices}`);
29
+ }
30
+ console.log("");
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ import { expectTypeOf } from "expect-type";
2
+
3
+ // Replace this with type tests for your library's exports
4
+ expectTypeOf(1).toEqualTypeOf<number>();
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": ["."],
4
+ "compilerOptions": {
5
+ "rootDir": "..",
6
+ "noEmit": true
7
+ }
8
+ }
@@ -0,0 +1,104 @@
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
+
6
+ const deps = {
7
+ "expect-type": "^1.3.0",
8
+ };
9
+
10
+ /**
11
+ * The library tsconfig only includes `src` (it drives declaration output),
12
+ * so type tests get their own project.
13
+ */
14
+ const scripts = {
15
+ "lint:type-tests": "ember-tsc --noEmit --project type-tests",
16
+ };
17
+
18
+ /**
19
+ * @type {import('#types').Layer}
20
+ */
21
+ export default {
22
+ label: "expect-type",
23
+ hint: "type tests for the published API",
24
+
25
+ async run(project) {
26
+ // Apps aren't published, so they have no public API to pin
27
+ if (!project.isLibrary) return;
28
+
29
+ if (!(await project.hasOrWantsLayer("typescript"))) return;
30
+
31
+ await applyFolderTo(join(import.meta.dirname, "files"), project);
32
+ await packageJson.addDevDependencies(await getLatest(deps), project.directory);
33
+ await packageJson.addScripts(scripts, project.directory);
34
+ },
35
+
36
+ /**
37
+ * @overload
38
+ * @param {import('#utils/project.js').Project} project
39
+ * @param {true} explain
40
+ * @returns {Promise<{ isSetup: boolean; reasons: string[] }>}
41
+ */
42
+ /**
43
+ * @overload
44
+ * @param {import('#utils/project.js').Project} project
45
+ * @param {boolean | undefined} [explain]
46
+ * @returns {Promise<boolean>}
47
+ */
48
+ async isSetup(project, explain) {
49
+ /** @type {string[]} */
50
+ const reasons = [];
51
+
52
+ // Nothing to set up outside TypeScript libraries,
53
+ // so nothing can be missing
54
+ if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) {
55
+ return explain ? { isSetup: true, reasons } : true;
56
+ }
57
+
58
+ if (!project.hasFile("type-tests/tsconfig.json")) {
59
+ if (!explain) return false;
60
+
61
+ reasons.push("type-tests/tsconfig.json is missing");
62
+ }
63
+
64
+ let manifest = await packageJson.read(project.directory);
65
+
66
+ for (let script of Object.keys(scripts)) {
67
+ if (!manifest.scripts?.[script]) {
68
+ if (!explain) return false;
69
+
70
+ reasons.push(`package.json is missing the "${script}" script`);
71
+ }
72
+ }
73
+
74
+ for (let dep of Object.keys(deps)) {
75
+ if (!manifest.devDependencies?.[dep]) {
76
+ if (!explain) return false;
77
+
78
+ reasons.push(`package.json is missing ${dep} in devDependencies`);
79
+ }
80
+ }
81
+
82
+ if (explain) {
83
+ return {
84
+ isSetup: reasons.length === 0,
85
+ reasons,
86
+ };
87
+ }
88
+
89
+ return reasons.length === 0;
90
+ },
91
+
92
+ /**
93
+ * @param {import('#utils/project.js').Project} project
94
+ */
95
+ async readme(project) {
96
+ if (!project.isLibrary || !(await project.hasOrWantsLayer("typescript"))) return;
97
+
98
+ return `### Type tests
99
+
100
+ Type tests in \`type-tests/\` use [expect-type](https://github.com/mmkal/expect-type) to pin the types of the public API.
101
+
102
+ - \`${project.runPrefix} lint:type-tests\` - Typecheck the type tests`;
103
+ },
104
+ };
@@ -1,5 +1,6 @@
1
1
  import { test, expect as hardExpect } from "vitest";
2
2
  import { cli } from "#test-helpers";
3
+ import { stripVTControlCharacters } from "node:util";
3
4
 
4
5
  const expect = hardExpect.soft;
5
6
 
@@ -10,3 +11,58 @@ test.skip("cli works", async () => {
10
11
 
11
12
  expect(execaPromise.exitCode).toBe(0);
12
13
  });
14
+
15
+ const BASIC_HELP_OUTPUT = `
16
+ " ember.nvp
17
+
18
+ Usage: npx ember.nvp [options]
19
+
20
+ Core Options:
21
+ -h, --help <boolean>
22
+ Show CLI help and option details
23
+
24
+ --name <string>
25
+ Name of the project
26
+
27
+ --path <string>
28
+ Target directory path for the project
29
+
30
+ --type <string>
31
+ Type of project to generate [choices: "app", "addon", "library", "extension", "custom-element"]
32
+
33
+ --confirm <string>
34
+ Bypass target confirmation step [choices: "yes", "no"]
35
+
36
+ --layers <string>
37
+ Layers to apply to the project (repeat for multiple layers)
38
+
39
+ --packageManager <string>
40
+ Package manager to configure for the project [choices: "npm", "pnpm"]
41
+
42
+ --replaceOrUpdate <string>
43
+ Strategy to use if target directory exists [choices: "replace", "update"]
44
+
45
+ --write <string>
46
+ Confirm writing changes to disk [choices: "yes", "no"]
47
+ "
48
+ `;
49
+
50
+ test("cli --help outputs usage and core options", async () => {
51
+ let { execaPromise } = cli(["--help"]);
52
+
53
+ const res = await execaPromise;
54
+
55
+ expect(res.exitCode).toBe(0);
56
+ const outStr = stripVTControlCharacters(res.stdout);
57
+ expect(outStr).toMatchInlineSnapshot(BASIC_HELP_OUTPUT);
58
+ });
59
+
60
+ test("cli -h alias works", async () => {
61
+ let { execaPromise } = cli(["-h"]);
62
+
63
+ const res = await execaPromise;
64
+
65
+ expect(res.exitCode).toBe(0);
66
+ const outStr = stripVTControlCharacters(res.stdout);
67
+ expect(outStr).toMatchInlineSnapshot(BASIC_HELP_OUTPUT);
68
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
+ import { generate, expectIsSetup, layers } from "#test-helpers";
3
+ import { writeLibrarySource } from "./library-src-fixtures.ts";
4
+ import { execa } from "execa";
5
+ import { existsSync } from "node:fs";
6
+ import { rm, writeFile } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+
9
+ import type { Project } from "ember.nvp";
10
+
11
+ const expectType = layers.find((layer) => layer.name === "expect-type")!;
12
+
13
+ async function typecheck(project: Project) {
14
+ return execa("pnpm lint:type-tests", {
15
+ cwd: project.directory,
16
+ shell: true,
17
+ all: true,
18
+ reject: false,
19
+ });
20
+ }
21
+
22
+ describe("layer: expect-type", () => {
23
+ const dirs: string[] = [];
24
+
25
+ afterAll(async () => {
26
+ if (process.env.CI) return;
27
+
28
+ for (const dir of dirs) {
29
+ await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
30
+ }
31
+ });
32
+
33
+ describe("TypeScript library", () => {
34
+ let project: Project;
35
+
36
+ beforeAll(async () => {
37
+ project = await generate({
38
+ type: "library",
39
+ name: "my-lib",
40
+ layers: ["typescript", "expect-type"],
41
+ });
42
+ dirs.push(project.directory);
43
+
44
+ await writeLibrarySource(project, "typescript");
45
+
46
+ let install = await execa("pnpm install", { cwd: project.directory, shell: true });
47
+ expect(install.exitCode).toBe(0);
48
+ });
49
+
50
+ it("is setup", async () => {
51
+ await expectIsSetup(project, expectType);
52
+ });
53
+
54
+ it("the generated type tests pass", async () => {
55
+ let result = await typecheck(project);
56
+
57
+ expect(result.exitCode, result.all).toBe(0);
58
+ });
59
+
60
+ it("checks the library's real exports", async () => {
61
+ await writeFile(
62
+ join(project.directory, "type-tests/math.test.ts"),
63
+ `import { expectTypeOf } from "expect-type";
64
+ import { add } from "../src/index.ts";
65
+
66
+ expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();
67
+ expectTypeOf(add).returns.toEqualTypeOf<number>();
68
+ `,
69
+ );
70
+
71
+ let result = await typecheck(project);
72
+
73
+ expect(result.exitCode, result.all).toBe(0);
74
+ });
75
+
76
+ it("fails on a wrong type assertion", async () => {
77
+ await writeFile(
78
+ join(project.directory, "type-tests/wrong.test.ts"),
79
+ `import { expectTypeOf } from "expect-type";
80
+ import { add } from "../src/index.ts";
81
+
82
+ expectTypeOf(add).returns.toEqualTypeOf<string>();
83
+ `,
84
+ );
85
+
86
+ let result = await typecheck(project);
87
+
88
+ expect(result.exitCode).not.toBe(0);
89
+ expect(result.all).toContain("wrong.test.ts");
90
+ });
91
+
92
+ it("keeps type tests out of the build", async () => {
93
+ await rm(join(project.directory, "type-tests/wrong.test.ts"));
94
+
95
+ let build = await execa("pnpm build", { cwd: project.directory, shell: true });
96
+ expect(build.exitCode).toBe(0);
97
+
98
+ expect(existsSync(join(project.directory, "dist/type-tests"))).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe("JavaScript library", () => {
103
+ it("adds nothing (no declarations to test)", async () => {
104
+ let project = await generate({ type: "library", name: "my-lib", layers: ["expect-type"] });
105
+ dirs.push(project.directory);
106
+
107
+ await expectIsSetup(project, expectType);
108
+
109
+ expect(existsSync(join(project.directory, "type-tests"))).toBe(false);
110
+ expect(await project.read("package.json")).not.toContain("expect-type");
111
+ });
112
+ });
113
+ });
@@ -13,6 +13,7 @@ describe("discoverLayers", () => {
13
13
  "are-the-types-wrong",
14
14
  "eslint-bundled-ember",
15
15
  "eslint-bundled-nvp",
16
+ "expect-type",
16
17
  "git",
17
18
  "github-actions",
18
19
  "inspector-support",
@@ -28,11 +28,11 @@ const INITIAL_ONLY = new Set<string>([
28
28
  */
29
29
  const NOT_YET_SUPPORTED: Record<ProjectType, Set<string>> = {
30
30
  // publish checks: apps aren't published
31
- app: new Set(["publint", "are-the-types-wrong"]),
31
+ app: new Set(["publint", "are-the-types-wrong", "expect-type"]),
32
32
  // libraries have no Application to wire inspector support into
33
33
  library: new Set(["inspector-support"]),
34
34
  // publish checks: extensions aren't published (to npm)
35
- extension: new Set(["publint", "are-the-types-wrong"]),
35
+ extension: new Set(["publint", "are-the-types-wrong", "expect-type"]),
36
36
  // same toolchain as libraries: no Application either
37
37
  "custom-element": new Set(["inspector-support"]),
38
38
  };
@@ -61,6 +61,7 @@ const CHECK_LAYERS = new Set<string>([
61
61
  "prettier",
62
62
  "publint",
63
63
  "are-the-types-wrong",
64
+ "expect-type",
64
65
  ]);
65
66
 
66
67
  type PermutationGroup = "main" | "checks";
@@ -120,7 +120,10 @@ export function cli(args: string[] = []) {
120
120
  let output = new MockWritable();
121
121
  let input = new MockReadable();
122
122
 
123
- let execaPromise = execa("node", args, { shell: true, stdout: output, stdin: input });
123
+ let execaPromise = execa("node", [cliPath, ...args]);
124
+ if (execaPromise.stdout) {
125
+ execaPromise.stdout.pipe(output);
126
+ }
124
127
 
125
128
  return {
126
129
  execaPromise,
package/vitest.config.js CHANGED
@@ -1,9 +1,15 @@
1
- import { defineConfig } from "vitest/config";
1
+ import { configDefaults, defineConfig } from "vitest/config";
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
5
  testTimeout: 60_000,
6
6
  hookTimeout: 60_000,
7
7
  include: ["test/**/*.test.ts", "src/**/*.test.ts", "packages/*/src/**/*.test.ts"],
8
+ // Templates copied into generated projects, which run their own tests
9
+ exclude: configDefaults.exclude.concat([
10
+ "src/**/layers/*/files/**",
11
+ "src/**/layers/*/library-files/**",
12
+ "src/**/bases/*/files/**",
13
+ ]),
8
14
  },
9
15
  });