ember.nvp 0.2.6 → 0.4.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.
@@ -1,31 +1,29 @@
1
1
  {
2
2
  "solution": {
3
3
  "ember.nvp": {
4
- "impact": "patch",
5
- "oldVersion": "0.2.5",
6
- "newVersion": "0.2.6",
4
+ "impact": "minor",
5
+ "oldVersion": "0.3.0",
6
+ "newVersion": "0.4.0",
7
7
  "tagName": "latest",
8
8
  "constraints": [
9
+ {
10
+ "impact": "minor",
11
+ "reason": "Appears in changelog section :rocket: Enhancement"
12
+ },
9
13
  {
10
14
  "impact": "patch",
11
15
  "reason": "Appears in changelog section :bug: Bug Fix"
16
+ },
17
+ {
18
+ "impact": "patch",
19
+ "reason": "Appears in changelog section :house: Internal"
12
20
  }
13
21
  ],
14
22
  "pkgJSONPath": "./package.json"
15
23
  },
16
24
  "@nullvoxpopuli/ember-vite": {
17
- "impact": "patch",
18
- "oldVersion": "1.0.2",
19
- "newVersion": "1.0.3",
20
- "tagName": "latest",
21
- "constraints": [
22
- {
23
- "impact": "patch",
24
- "reason": "Appears in changelog section :bug: Bug Fix"
25
- }
26
- ],
27
- "pkgJSONPath": "./packages/vite/package.json"
25
+ "oldVersion": "1.0.3"
28
26
  }
29
27
  },
30
- "description": "## Release (2026-06-22)\n\n* ember.nvp 0.2.6 (patch)\n* @nullvoxpopuli/ember-vite 1.0.3 (patch)\n\n#### :bug: Bug Fix\n* `@nullvoxpopuli/ember-vite`, `ember.nvp`\n * [#46](https://github.com/NullVoxPopuli/ember.nvp/pull/46) compile ts authored packages ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 1\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
28
+ "description": "## Release (2026-06-23)\n\n* ember.nvp 0.4.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#48](https://github.com/NullVoxPopuli/ember.nvp/pull/48) replaceOrUpdate ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :bug: Bug Fix\n* `ember.nvp`\n * [#52](https://github.com/NullVoxPopuli/ember.nvp/pull/52) Fix test-helper ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :house: Internal\n* `ember.nvp`\n * [#50](https://github.com/NullVoxPopuli/ember.nvp/pull/50) Tests for qunit ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#49](https://github.com/NullVoxPopuli/ember.nvp/pull/49) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions))\n\n#### Committers: 2\n- GitHub Actions [Bot] ([@github-actions](https://github.com/apps/github-actions))\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
31
29
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## Release (2026-06-23)
4
+
5
+ * ember.nvp 0.4.0 (minor)
6
+
7
+ #### :rocket: Enhancement
8
+ * `ember.nvp`
9
+ * [#48](https://github.com/NullVoxPopuli/ember.nvp/pull/48) replaceOrUpdate ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
10
+
11
+ #### :bug: Bug Fix
12
+ * `ember.nvp`
13
+ * [#52](https://github.com/NullVoxPopuli/ember.nvp/pull/52) Fix test-helper ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
14
+
15
+ #### :house: Internal
16
+ * `ember.nvp`
17
+ * [#50](https://github.com/NullVoxPopuli/ember.nvp/pull/50) Tests for qunit ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
18
+ * [#49](https://github.com/NullVoxPopuli/ember.nvp/pull/49) Prepare Release ([@github-actions[bot]](https://github.com/apps/github-actions))
19
+
20
+ #### Committers: 2
21
+ - GitHub Actions [Bot] ([@github-actions](https://github.com/apps/github-actions))
22
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
23
+
24
+ ## Release (2026-06-22)
25
+
26
+ * ember.nvp 0.3.0 (minor)
27
+
28
+ #### :rocket: Enhancement
29
+ * `ember.nvp`
30
+ * [#48](https://github.com/NullVoxPopuli/ember.nvp/pull/48) replaceOrUpdate ([@NullVoxPopuli](https://github.com/NullVoxPopuli))
31
+
32
+ #### Committers: 1
33
+ - [@NullVoxPopuli](https://github.com/NullVoxPopuli)
34
+
3
35
  ## Release (2026-06-22)
4
36
 
5
37
  * ember.nvp 0.2.6 (patch)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember.nvp",
3
- "version": "0.2.6",
3
+ "version": "0.4.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
@@ -30,10 +30,15 @@ const { values } = parseArgs({
30
30
  type: "string",
31
31
  choices: ["npm", "pnpm"],
32
32
  },
33
+
34
+ replaceOrUpdate: {
35
+ type: "string",
36
+ choices: ["replace", "update"],
37
+ },
33
38
  },
34
39
  });
35
40
 
36
- const { name, type, layers = [], packageManager, path, confirm } = values;
41
+ const { replaceOrUpdate, name, type, layers = [], packageManager, path, confirm } = values;
37
42
 
38
43
  export const answers = {
39
44
  name,
@@ -42,6 +47,7 @@ export const answers = {
42
47
  packageManager,
43
48
  path,
44
49
  confirm,
50
+ replaceOrUpdate,
45
51
  };
46
52
 
47
53
  /**
@@ -1,4 +1,4 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { mkdir, rm } from "node:fs/promises";
2
2
 
3
3
  import baseApp from "#bases/minimal-app";
4
4
  import baseLibrary from "#bases/minimal-library";
@@ -8,8 +8,13 @@ import { hasGit } from "#utils/git.js";
8
8
  * Generate project files by running layer functions
9
9
  *
10
10
  * @param {import('#utils/project.js').Project} project
11
+ * @param {string} [replaceOrUpdate]
11
12
  */
12
- export async function generateProject(project) {
13
+ export async function generateProject(project, replaceOrUpdate) {
14
+ if (replaceOrUpdate === "replace") {
15
+ await rm(project.directory, { recursive: true });
16
+ }
17
+
13
18
  await mkdir(project.directory, { recursive: true });
14
19
 
15
20
  switch (project.desires.type) {
package/src/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import { askPath } from "./questions/path.js";
10
10
  import { askIfOK } from "./questions/ok.js";
11
11
  import { styleText } from "node:util";
12
12
  import { Project } from "#utils/project.js";
13
+ import { askReplaceOrUpdate } from "./questions/replace-or-update.js";
13
14
 
14
15
  /**
15
16
  * This whole file's primary purpose is to be an interactive CLI
@@ -25,6 +26,7 @@ async function main() {
25
26
 
26
27
  const projectName = await askName();
27
28
  const projectPath = await askPath(projectName);
29
+ const replaceOrUpdate = await askReplaceOrUpdate(projectPath);
28
30
  const projectType = await askProjectType();
29
31
  const selectedLayers = await askLayers();
30
32
  const packageManager = await askPackageManager();
@@ -44,7 +46,7 @@ async function main() {
44
46
 
45
47
  try {
46
48
  // Generate the project
47
- await generateProject(project);
49
+ await generateProject(project, replaceOrUpdate);
48
50
 
49
51
  s.stop("Project created!");
50
52
 
@@ -0,0 +1,56 @@
1
+ import * as p from "@clack/prompts";
2
+
3
+ import { answers, printArgInUse } from "#args";
4
+ import { existsSync } from "node:fs";
5
+
6
+ /**
7
+ *
8
+ * @param {string | undefined} value
9
+ * @returns
10
+ */
11
+ function isValid(value) {
12
+ if (!value) return false;
13
+
14
+ return value === "replace" || value === "update";
15
+ }
16
+
17
+ /**
18
+ * @param {string} projectPath -- the path the project will be generated in
19
+ */
20
+ export async function askReplaceOrUpdate(projectPath) {
21
+ if (!existsSync(projectPath)) {
22
+ return;
23
+ }
24
+
25
+ if (answers.replaceOrUpdate) {
26
+ if (isValid(answers.replaceOrUpdate)) {
27
+ printArgInUse("replaceOrUpdate", answers.replaceOrUpdate);
28
+
29
+ return answers.replaceOrUpdate;
30
+ }
31
+ }
32
+
33
+ const answer = await p.select({
34
+ message: "Replace or update at the selected path",
35
+ initialValue: "update",
36
+ options: [
37
+ {
38
+ value: "update",
39
+ label: "update",
40
+ hint: "Updates the project in the target directory, if one exists",
41
+ },
42
+ {
43
+ value: "replace",
44
+ label: "replace",
45
+ hint: "Deletes the target directory and generates a new project",
46
+ },
47
+ ],
48
+ });
49
+
50
+ if (p.isCancel(answer)) {
51
+ p.cancel("Operation cancelled");
52
+ return process.exit(0);
53
+ }
54
+
55
+ return answer;
56
+ }
@@ -12,9 +12,6 @@ import * as QUnit from "qunit";
12
12
  import { setup } from "qunit-dom";
13
13
  import { setupEmberOnerrorValidation, start as qunitStart } from "ember-qunit";
14
14
  import { setTesting } from "@embroider/macros";
15
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
16
- // @ts-expect-error
17
- import { getGlobalConfig } from "@embroider/macros/src/addon/runtime";
18
15
 
19
16
  import Application from "#app/app";
20
17
  import config from "#config";
@@ -43,19 +40,8 @@ export function start() {
43
40
  config.locationType = "none";
44
41
  config.APP.rootElement = "#ember-testing";
45
42
  config.APP.autoboot = false;
46
- setTesting(true);
47
-
48
- const theMacrosGlobal = getGlobalConfig();
49
-
50
- /**
51
- * Caveats:
52
- * - https://github.com/embroider-build/embroider/issues/2660
53
- * - https://github.com/embroider-build/embroider/issues/1998
54
- *
55
- */
56
- theMacrosGlobal["@embroider/macros"] ||= {};
57
- theMacrosGlobal["@embroider/macros"].isTesting ||= true;
58
43
 
44
+ setTesting(true);
59
45
  setApplication(Application.create(config.APP));
60
46
  setup(QUnit.assert);
61
47
  setupEmberOnerrorValidation();
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, afterAll } from "vitest";
2
+ import { generate, layers, expectIsSetup } from "#test-helpers";
3
+ import { generateProject } from "ember.nvp";
4
+ import { writeFile } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { rimraf } from "rimraf";
8
+
9
+ /**
10
+ * The replace-or-update question only matters when a project already exists
11
+ * at the target path. These tests generate a project, assert the baseline
12
+ * state, then regenerate over it the way the CLI does once the user has
13
+ * answered the question, and assert the resulting project output is correct
14
+ * for each answer.
15
+ */
16
+ describe("generating over an existing project", () => {
17
+ const prettier = layers.find((layer) => layer.name === "prettier")!;
18
+ const dirs: string[] = [];
19
+
20
+ afterAll(async () => {
21
+ if (process.env.CI) return;
22
+
23
+ for (const dir of dirs) {
24
+ await rimraf(dir, { maxRetries: 3, retryDelay: 100 });
25
+ }
26
+ });
27
+
28
+ it("'update' regenerates in place, keeping unrelated files", async () => {
29
+ const project = await generate({ type: "app", layers: ["prettier"] });
30
+ dirs.push(project.directory);
31
+
32
+ const userFile = join(project.directory, "my-untracked-file.txt");
33
+ await writeFile(userFile, "the user added this");
34
+
35
+ // Baseline: a valid project exists, with the desired layer and the
36
+ // user's own file present.
37
+ expect(existsSync(join(project.directory, "package.json")), "baseline package.json").toBe(true);
38
+ expect(existsSync(userFile), "baseline user file").toBe(true);
39
+ await expectIsSetup(project, prettier);
40
+
41
+ await generateProject(project, "update");
42
+
43
+ // After update: the pre-existing project and the user's file are
44
+ // preserved, and it is still a valid, fully-set-up project.
45
+ expect(existsSync(userFile), "unrelated file preserved by update").toBe(true);
46
+ expect(existsSync(join(project.directory, "package.json"))).toBe(true);
47
+ await expectIsSetup(project, prettier);
48
+ });
49
+
50
+ it("'replace' wipes the directory and produces a fresh project", async () => {
51
+ const project = await generate({ type: "app", layers: ["prettier"] });
52
+ dirs.push(project.directory);
53
+
54
+ const userFile = join(project.directory, "my-untracked-file.txt");
55
+ await writeFile(userFile, "the user added this");
56
+
57
+ // Baseline: a valid project exists, with the desired layer and the
58
+ // user's own file present.
59
+ expect(existsSync(join(project.directory, "package.json")), "baseline package.json").toBe(true);
60
+ expect(existsSync(userFile), "baseline user file").toBe(true);
61
+ await expectIsSetup(project, prettier);
62
+
63
+ await generateProject(project, "replace");
64
+
65
+ // After replace: everything that was there before is gone, replaced by a
66
+ // freshly generated, valid project with the desired layers.
67
+ expect(existsSync(userFile), "stale file removed by replace").toBe(false);
68
+ expect(existsSync(join(project.directory, "package.json"))).toBe(true);
69
+ await expectIsSetup(project, prettier);
70
+ });
71
+
72
+ it("defaults to a non-destructive update when no answer is given", async () => {
73
+ const project = await generate({ type: "app", layers: ["prettier"] });
74
+ dirs.push(project.directory);
75
+
76
+ const userFile = join(project.directory, "my-untracked-file.txt");
77
+ await writeFile(userFile, "the user added this");
78
+
79
+ // Baseline: a valid project exists, with the desired layer and the
80
+ // user's own file present.
81
+ expect(existsSync(join(project.directory, "package.json")), "baseline package.json").toBe(true);
82
+ expect(existsSync(userFile), "baseline user file").toBe(true);
83
+ await expectIsSetup(project, prettier);
84
+
85
+ await generateProject(project);
86
+
87
+ // After a no-arg regenerate: nothing is destroyed.
88
+ expect(existsSync(userFile), "unrelated file preserved by default").toBe(true);
89
+ expect(existsSync(join(project.directory, "package.json"))).toBe(true);
90
+ await expectIsSetup(project, prettier);
91
+ });
92
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, it, afterAll, expect } from "vitest";
2
+ import { generate } from "#test-helpers";
3
+ import { rimraf } from "rimraf";
4
+ import { execa } from "execa";
5
+
6
+ /**
7
+ * The replace-or-update question only matters when a project already exists
8
+ * at the target path. These tests generate a project, assert the baseline
9
+ * state, then regenerate over it the way the CLI does once the user has
10
+ * answered the question, and assert the resulting project output is correct
11
+ * for each answer.
12
+ */
13
+ describe("layer: qunit", () => {
14
+ const dirs: string[] = [];
15
+
16
+ afterAll(async () => {
17
+ if (process.env.CI) return;
18
+
19
+ for (const dir of dirs) {
20
+ await rimraf(dir, { maxRetries: 3, retryDelay: 100 });
21
+ }
22
+ });
23
+
24
+ it("it boots and run tests", async () => {
25
+ const project = await generate({ type: "app", layers: ["qunit"] });
26
+ dirs.push(project.directory);
27
+
28
+ let install = await execa("pnpm install", { cwd: project.directory, shell: true });
29
+ expect(install.exitCode).toBe(0);
30
+
31
+ let test = await execa("pnpm test", { cwd: project.directory, shell: true });
32
+ expect(test.exitCode).toBe(0);
33
+ });
34
+ });