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.
- package/.release-plan.json +13 -15
- package/CHANGELOG.md +32 -0
- package/package.json +1 -1
- package/src/cli/args.js +7 -1
- package/src/cli/generator.js +7 -2
- package/src/cli/index.js +3 -1
- package/src/cli/questions/replace-or-update.js +56 -0
- package/src/layers/qunit/files/tests/test-helper.ts +1 -15
- package/test/ember.nvp/generator-replace-or-update.test.ts +92 -0
- package/test/ember.nvp/layer-qunit.test.ts +34 -0
package/.release-plan.json
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"solution": {
|
|
3
3
|
"ember.nvp": {
|
|
4
|
-
"impact": "
|
|
5
|
-
"oldVersion": "0.
|
|
6
|
-
"newVersion": "0.
|
|
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
|
-
"
|
|
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-
|
|
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.
|
|
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
|
/**
|
package/src/cli/generator.js
CHANGED
|
@@ -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
|
+
});
|