ember.nvp 1.12.0 → 1.14.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 +3 -3
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/bases/index.js +32 -0
- package/src/bases/minimal-app/index.js +14 -1
- package/src/bases/minimal-custom-element/index.js +14 -0
- package/src/bases/minimal-extension/index.js +15 -1
- package/src/bases/minimal-library/index.js +20 -1
- package/src/cli/generator.js +2 -18
- package/src/cli/index.js +1 -1
- package/src/cli/questions/project-type.js +4 -2
- package/src/layers/qunit/index.js +55 -1
- package/src/layers/qunit/library-ts-files/config/tsconfig.publish.json +8 -0
- package/src/utils/fs.js +18 -1
- package/src/utils/types.ts +12 -0
- package/test/ember.nvp/cli.test.ts +14 -7
- package/test/ember.nvp/detect-project-type.test.ts +86 -0
- package/test/ember.nvp/layer-qunit.test.ts +72 -17
package/.release-plan.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"solution": {
|
|
3
3
|
"ember.nvp": {
|
|
4
4
|
"impact": "minor",
|
|
5
|
-
"oldVersion": "1.
|
|
6
|
-
"newVersion": "1.
|
|
5
|
+
"oldVersion": "1.13.0",
|
|
6
|
+
"newVersion": "1.14.0",
|
|
7
7
|
"tagName": "latest",
|
|
8
8
|
"constraints": [
|
|
9
9
|
{
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
"oldVersion": "1.2.0"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
|
-
"description": "## Release (2026-09-30)\n\n* ember.nvp 1.
|
|
26
|
+
"description": "## Release (2026-09-30)\n\n* ember.nvp 1.14.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#156](https://github.com/NullVoxPopuli/ember.nvp/pull/156) Give TypeScript libraries with tests a publish tsconfig ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n\n#### Committers: 1\n- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n"
|
|
27
27
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Release (2026-09-30)
|
|
4
4
|
|
|
5
|
+
* ember.nvp 1.14.0 (minor)
|
|
6
|
+
|
|
7
|
+
#### :rocket: Enhancement
|
|
8
|
+
* `ember.nvp`
|
|
9
|
+
* [#156](https://github.com/NullVoxPopuli/ember.nvp/pull/156) Give TypeScript libraries with tests a publish tsconfig ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
10
|
+
|
|
11
|
+
#### Committers: 1
|
|
12
|
+
- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
13
|
+
|
|
14
|
+
## Release (2026-09-30)
|
|
15
|
+
|
|
16
|
+
* ember.nvp 1.13.0 (minor)
|
|
17
|
+
|
|
18
|
+
#### :rocket: Enhancement
|
|
19
|
+
* `ember.nvp`
|
|
20
|
+
* [#154](https://github.com/NullVoxPopuli/ember.nvp/pull/154) Default the project type to the detected type on update ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
21
|
+
|
|
22
|
+
#### Committers: 1
|
|
23
|
+
- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
|
|
24
|
+
|
|
25
|
+
## Release (2026-09-30)
|
|
26
|
+
|
|
5
27
|
* ember.nvp 1.12.0 (minor)
|
|
6
28
|
|
|
7
29
|
#### :rocket: Enhancement
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ You can run `ember.nvp` on top of an existing project to add layers to it.
|
|
|
40
40
|
|
|
41
41
|
Run it inside the project. When the current directory has a `package.json`, the first question is what to do with it:
|
|
42
42
|
|
|
43
|
-
- **update**: update the project in this directory. The
|
|
43
|
+
- **update**: update the project in this directory. The name starts as the `package.json` name, and the type starts as the type that ember.nvp detects (app, library, browser extension, or custom element).
|
|
44
44
|
- **replace**: delete this directory and generate a new project in it.
|
|
45
45
|
- **new**: generate a new project in another directory.
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ember.nvp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
"imports": {
|
|
11
11
|
"#args": "./src/cli/args.js",
|
|
12
|
+
"#bases": "./src/bases/index.js",
|
|
12
13
|
"#bases/*": "./src/bases/*/index.js",
|
|
13
14
|
"#consolidators/*": "./src/consolidators/*",
|
|
14
15
|
"#layers": "./src/layers/index.js",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import app from "./minimal-app/index.js";
|
|
2
|
+
import customElement from "./minimal-custom-element/index.js";
|
|
3
|
+
import extension from "./minimal-extension/index.js";
|
|
4
|
+
import library from "./minimal-library/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The base for each project type.
|
|
8
|
+
*
|
|
9
|
+
* Detection asks the bases in this order,
|
|
10
|
+
* so each base comes before the bases whose markers it also has:
|
|
11
|
+
* - an extension has the index.html and app/ folder of an app
|
|
12
|
+
* - a custom element builds with tsdown, like a library
|
|
13
|
+
* - a library can have an app/ folder (classic addons) or an index.html (a docs app)
|
|
14
|
+
*
|
|
15
|
+
* @type {Record<import('#types').ProjectType, import('#types').Base>}
|
|
16
|
+
*/
|
|
17
|
+
export const bases = {
|
|
18
|
+
extension,
|
|
19
|
+
"custom-element": customElement,
|
|
20
|
+
library,
|
|
21
|
+
app,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} directory an existing project
|
|
26
|
+
* @returns {import('#types').ProjectType | undefined}
|
|
27
|
+
*/
|
|
28
|
+
export function detectProjectType(directory) {
|
|
29
|
+
for (let [type, base] of Object.entries(bases)) {
|
|
30
|
+
if (base.detect(directory)) return /** @type {import('#types').ProjectType} */ (type);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { packageJson, js } from "ember-apply";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getLatest } from "#utils/npm.js";
|
|
4
|
-
import { applyFolderTo } from "#utils/fs.js";
|
|
5
|
+
import { applyFolderTo, readJSON } from "#utils/fs.js";
|
|
5
6
|
import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -14,11 +15,23 @@ import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
|
14
15
|
* - No linting or formatting
|
|
15
16
|
* - No ember-welcome-page
|
|
16
17
|
* - No warp-drive (opt-in if needed)
|
|
18
|
+
*
|
|
19
|
+
* @type {import('#types').Base}
|
|
17
20
|
*/
|
|
18
21
|
export default {
|
|
19
22
|
label: "Minimal App Base",
|
|
20
23
|
description: "Bare minimum Ember app structure",
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} directory
|
|
27
|
+
*/
|
|
28
|
+
detect(directory) {
|
|
29
|
+
return (
|
|
30
|
+
readJSON(join(directory, "package.json"))?.["ember-addon"]?.type === "app" ||
|
|
31
|
+
existsSync(join(directory, "app"))
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* 1. Apply files
|
|
24
37
|
* a. Remove TS if needed
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { packageJson } from "ember-apply";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { getLatest } from "#utils/npm.js";
|
|
@@ -26,11 +27,24 @@ const TEMPLATE_TAG_NAME = "minimal-custom-element";
|
|
|
26
27
|
* - No linting or formatting
|
|
27
28
|
*
|
|
28
29
|
* Testing, linting, formatting, etc. are opt-in via layers.
|
|
30
|
+
*
|
|
31
|
+
* @type {import('#types').Base}
|
|
29
32
|
*/
|
|
30
33
|
export default {
|
|
31
34
|
label: "Minimal Custom Element Base",
|
|
32
35
|
description: "An Ember component packaged as a custom element (web component)",
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} directory
|
|
39
|
+
*/
|
|
40
|
+
detect(directory) {
|
|
41
|
+
return ["src/register.ts", "src/register.js"].some((file) => {
|
|
42
|
+
let path = join(directory, file);
|
|
43
|
+
|
|
44
|
+
return existsSync(path) && readFileSync(path, "utf-8").includes("customElements.define");
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* 1. Apply files
|
|
36
50
|
* a. Remove TS if needed
|
|
@@ -2,7 +2,7 @@ import { packageJson } from "ember-apply";
|
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getLatest } from "#utils/npm.js";
|
|
5
|
-
import { applyFolderTo } from "#utils/fs.js";
|
|
5
|
+
import { applyFolderTo, readJSON } from "#utils/fs.js";
|
|
6
6
|
import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -17,11 +17,25 @@ import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
|
17
17
|
* - No testing framework
|
|
18
18
|
* - No linting or formatting
|
|
19
19
|
* - No ember-welcome-page
|
|
20
|
+
*
|
|
21
|
+
* @type {import('#types').Base}
|
|
20
22
|
*/
|
|
21
23
|
export default {
|
|
22
24
|
label: "Minimal Extension Base",
|
|
23
25
|
description: "Bare minimum browser extension structure with an Ember popup",
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A web app can have a manifest.json too (for a PWA),
|
|
29
|
+
* but only a browser extension's manifest has `manifest_version`.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} directory
|
|
32
|
+
*/
|
|
33
|
+
detect(directory) {
|
|
34
|
+
return ["manifest.json", "public/manifest.json"].some(
|
|
35
|
+
(file) => readJSON(join(directory, file))?.manifest_version !== undefined,
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
|
|
25
39
|
/**
|
|
26
40
|
* 1. Apply files
|
|
27
41
|
* a. Remove TS if needed
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { packageJson } from "ember-apply";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { getLatest } from "#utils/npm.js";
|
|
5
|
-
import { applyFolderTo } from "#utils/fs.js";
|
|
6
|
+
import { applyFolderTo, readJSON } from "#utils/fs.js";
|
|
6
7
|
import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -18,11 +19,29 @@ import { removeConfiguredPlugin } from "#utils/babel.js";
|
|
|
18
19
|
* - No linting or formatting
|
|
19
20
|
*
|
|
20
21
|
* Testing, linting, formatting, etc. are opt-in via layers.
|
|
22
|
+
*
|
|
23
|
+
* @type {import('#types').Base}
|
|
21
24
|
*/
|
|
22
25
|
export default {
|
|
23
26
|
label: "Minimal Library Base",
|
|
24
27
|
description: "Bare minimum Ember v2 library (addon) structure",
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* `ember-addon` marks v2 and classic addons.
|
|
31
|
+
* A library from this base has no `ember-addon`, only a tsdown config.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} directory
|
|
34
|
+
*/
|
|
35
|
+
detect(directory) {
|
|
36
|
+
let manifest = readJSON(join(directory, "package.json"));
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
manifest?.["ember-addon"]?.type === "addon" ||
|
|
40
|
+
Boolean(manifest?.keywords?.includes("ember-addon")) ||
|
|
41
|
+
existsSync(join(directory, "tsdown.config.js"))
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
|
|
26
45
|
/**
|
|
27
46
|
* 1. Apply files
|
|
28
47
|
* a. Remove TS if needed
|
package/src/cli/generator.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { mkdir, rm } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import baseExtension from "#bases/minimal-extension";
|
|
5
|
-
import baseLibrary from "#bases/minimal-library";
|
|
6
|
-
import baseCustomElement from "#bases/minimal-custom-element";
|
|
3
|
+
import { bases } from "#bases";
|
|
7
4
|
import { consolidateLintingScripts } from "../consolidators/linting.js";
|
|
8
5
|
import { hasGit } from "#utils/git.js";
|
|
9
6
|
/**
|
|
@@ -19,20 +16,7 @@ export async function generateProject(project, replaceOrUpdate) {
|
|
|
19
16
|
|
|
20
17
|
await mkdir(project.directory, { recursive: true });
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
case "app":
|
|
24
|
-
await baseApp.run(project);
|
|
25
|
-
break;
|
|
26
|
-
case "library":
|
|
27
|
-
await baseLibrary.run(project);
|
|
28
|
-
break;
|
|
29
|
-
case "extension":
|
|
30
|
-
await baseExtension.run(project);
|
|
31
|
-
break;
|
|
32
|
-
case "custom-element":
|
|
33
|
-
await baseCustomElement.run(project);
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
19
|
+
await bases[project.desires.type].run(project);
|
|
36
20
|
|
|
37
21
|
/**
|
|
38
22
|
* We could run these in a loop until there is no git diff
|
package/src/cli/index.js
CHANGED
|
@@ -34,7 +34,7 @@ async function main() {
|
|
|
34
34
|
const projectName = await askName(here === "update" ? cwd : undefined);
|
|
35
35
|
const projectPath = here ? cwd : await askPath(projectName);
|
|
36
36
|
const replaceOrUpdate = here ?? (await askReplaceOrUpdate(projectPath));
|
|
37
|
-
const projectType = await askProjectType();
|
|
37
|
+
const projectType = await askProjectType(replaceOrUpdate === "update" ? projectPath : undefined);
|
|
38
38
|
const selectedLayers = await askLayers(projectType);
|
|
39
39
|
const packageManager = await askPackageManager();
|
|
40
40
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { answers, printArgInUse } from "#args";
|
|
3
|
+
import { detectProjectType } from "#bases";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* addon is alias for library
|
|
@@ -27,10 +28,10 @@ function isValid(selected) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
|
-
*
|
|
31
|
+
* @param {string} [existingProject] a directory whose project type becomes the default answer
|
|
31
32
|
* @return {Promise<import('#types').ProjectType>}
|
|
32
33
|
*/
|
|
33
|
-
export async function askProjectType() {
|
|
34
|
+
export async function askProjectType(existingProject) {
|
|
34
35
|
if (isValid(answers.type)) {
|
|
35
36
|
printArgInUse("type", answers.type);
|
|
36
37
|
|
|
@@ -43,6 +44,7 @@ export async function askProjectType() {
|
|
|
43
44
|
|
|
44
45
|
const answer = await p.select({
|
|
45
46
|
message: "Which type of project?",
|
|
47
|
+
initialValue: existingProject ? detectProjectType(existingProject) : undefined,
|
|
46
48
|
options: [
|
|
47
49
|
{ value: "app", label: "web app", hint: "generates html, js, and css to deploy to the web" },
|
|
48
50
|
{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { applyFolderTo } from "#utils/fs.js";
|
|
2
2
|
import { getLatest } from "#utils/npm.js";
|
|
3
3
|
import { hasConfiguredPlugin, prependPlugin, removeConfiguredPlugin } from "#utils/babel.js";
|
|
4
|
-
import {
|
|
4
|
+
import { addTsdownConfigProperty } from "#utils/tsdown-config.js";
|
|
5
|
+
import { packageJson, tsconfig } from "ember-apply";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
|
|
7
8
|
const deps = {
|
|
@@ -39,6 +40,7 @@ const libraryTsDeps = {
|
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
const TEST_BABEL_CONFIG = "config/test/babel.config.js";
|
|
43
|
+
const PUBLISH_TSCONFIG = "config/tsconfig.publish.json";
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* @param {import('#utils/project.js').Project} project
|
|
@@ -72,6 +74,10 @@ export default {
|
|
|
72
74
|
|
|
73
75
|
if (isLibrary) {
|
|
74
76
|
await syncTestBabelConfig(project, ts);
|
|
77
|
+
|
|
78
|
+
if (ts) {
|
|
79
|
+
await usePublishTsconfig(project);
|
|
80
|
+
}
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
await packageJson.addDevDependencies(
|
|
@@ -141,6 +147,12 @@ export default {
|
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
149
|
|
|
150
|
+
if (project.isLibrary && project.wantsTypeScript && !project.hasFile(PUBLISH_TSCONFIG)) {
|
|
151
|
+
if (!explain) return false;
|
|
152
|
+
|
|
153
|
+
reasons.push(`${PUBLISH_TSCONFIG} is missing`);
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
if (explain) {
|
|
145
157
|
return {
|
|
146
158
|
isSetup: reasons.length === 0,
|
|
@@ -203,3 +215,45 @@ async function syncTestBabelConfig(project, ts) {
|
|
|
203
215
|
TEST_BABEL_CONFIG,
|
|
204
216
|
);
|
|
205
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* A library's tsconfig.json starts as its publish config:
|
|
221
|
+
* it covers only `src`, with `isolatedDeclarations` for the declarations tsdown emits.
|
|
222
|
+
*
|
|
223
|
+
* Tests live outside `src`, and editors, `lint:types`, and eslint read tsconfig.json.
|
|
224
|
+
* So in a TypeScript library, this layer:
|
|
225
|
+
* - widens tsconfig.json to `src` and `tests`, without the publish-only settings
|
|
226
|
+
* - moves those settings to `config/tsconfig.publish.json`, which the build uses
|
|
227
|
+
*
|
|
228
|
+
* @param {import('#utils/project.js').Project} project
|
|
229
|
+
*/
|
|
230
|
+
async function usePublishTsconfig(project) {
|
|
231
|
+
await applyFolderTo(join(import.meta.dirname, "library-ts-files"), project);
|
|
232
|
+
await addTsdownConfigProperty(project, "tsconfig", `tsconfig: "./${PUBLISH_TSCONFIG}"`);
|
|
233
|
+
|
|
234
|
+
let config = await tsconfig.read(project.directory);
|
|
235
|
+
|
|
236
|
+
if (!needsWidening(config)) return;
|
|
237
|
+
|
|
238
|
+
await tsconfig.modify((json) => {
|
|
239
|
+
json.include ||= [];
|
|
240
|
+
|
|
241
|
+
if (!json.include.includes("tests")) {
|
|
242
|
+
json.include.push("tests");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
delete json.compilerOptions?.rootDir;
|
|
246
|
+
delete json.compilerOptions?.isolatedDeclarations;
|
|
247
|
+
}, project.directory);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {Record<string, any>} config
|
|
252
|
+
*/
|
|
253
|
+
function needsWidening(config) {
|
|
254
|
+
return (
|
|
255
|
+
!config.include?.includes("tests") ||
|
|
256
|
+
config.compilerOptions?.rootDir !== undefined ||
|
|
257
|
+
config.compilerOptions?.isolatedDeclarations !== undefined
|
|
258
|
+
);
|
|
259
|
+
}
|
package/src/utils/fs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
2
|
import { readFile, glob, mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { parse as parsePath } from "node:path";
|
|
@@ -15,6 +15,23 @@ import { rewriteImportsToMatchFiles } from "./rewrite-imports.js";
|
|
|
15
15
|
*/
|
|
16
16
|
const EVERY_FILE = ["**/*", "**/.*", "**/.*/**/*"];
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Project detection reads files that can be missing or broken,
|
|
20
|
+
* and a guess must not stop the CLI.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} path
|
|
23
|
+
* @returns {any} undefined when the file is missing or is not JSON
|
|
24
|
+
*/
|
|
25
|
+
export function readJSON(path) {
|
|
26
|
+
if (!existsSync(path)) return;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Modified version of applyFolder from ember-apply.
|
|
20
37
|
*
|
package/src/utils/types.ts
CHANGED
|
@@ -89,6 +89,18 @@ export interface Layer {
|
|
|
89
89
|
>;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export interface Base {
|
|
93
|
+
label: string;
|
|
94
|
+
description: string;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the directory holds a project of this base's type.
|
|
97
|
+
*
|
|
98
|
+
* When updating a project, the type question starts at the first base that detects it.
|
|
99
|
+
*/
|
|
100
|
+
detect: (directory: string) => boolean;
|
|
101
|
+
run: (project: Project) => Promise<void>;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
export interface DiscoveredLayer extends Layer {
|
|
93
105
|
/**
|
|
94
106
|
* The unique name of the layer.
|
|
@@ -154,14 +154,15 @@ test("cli exits on an invalid layer option flag, before writing anything", async
|
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
test("in a project directory, asks to update first and
|
|
157
|
+
test("in a project directory, asks to update first and defaults to the project's name and type", async () => {
|
|
158
158
|
let dir = await mktemp("cli-update-here");
|
|
159
|
-
await writeFile(
|
|
159
|
+
await writeFile(
|
|
160
|
+
join(dir, "package.json"),
|
|
161
|
+
JSON.stringify({ name: "@scope/my-lib", "ember-addon": { version: 2, type: "addon" } }),
|
|
162
|
+
);
|
|
160
163
|
|
|
161
|
-
// No --name or --
|
|
164
|
+
// No --name, --path, or --type, so the CLI asks about the project in this directory
|
|
162
165
|
let args = [
|
|
163
|
-
"--type",
|
|
164
|
-
"library",
|
|
165
166
|
"--layers",
|
|
166
167
|
"prettier",
|
|
167
168
|
"--packageManager",
|
|
@@ -171,7 +172,11 @@ test("in a project directory, asks to update first and fills in the project's na
|
|
|
171
172
|
"--write",
|
|
172
173
|
"yes",
|
|
173
174
|
];
|
|
174
|
-
let prompts = [
|
|
175
|
+
let prompts = [
|
|
176
|
+
"This directory has a package.json",
|
|
177
|
+
"What is your project name?",
|
|
178
|
+
"Which type of project?",
|
|
179
|
+
];
|
|
175
180
|
|
|
176
181
|
try {
|
|
177
182
|
let { execaPromise } = cli(args, { cwd: dir });
|
|
@@ -180,7 +185,7 @@ test("in a project directory, asks to update first and fills in the project's na
|
|
|
180
185
|
execaPromise.stdout?.on("data", (chunk) => {
|
|
181
186
|
output += chunk;
|
|
182
187
|
|
|
183
|
-
// Enter keeps the answer each prompt starts with: "update", then the
|
|
188
|
+
// Enter keeps the answer each prompt starts with: "update", the name, then the type
|
|
184
189
|
if (prompts[0] && stripVTControlCharacters(output).includes(prompts[0])) {
|
|
185
190
|
prompts.shift();
|
|
186
191
|
execaPromise.stdin?.write("\r");
|
|
@@ -195,6 +200,8 @@ test("in a project directory, asks to update first and fills in the project's na
|
|
|
195
200
|
text.indexOf("What is your project name?"),
|
|
196
201
|
);
|
|
197
202
|
expect(text).not.toContain("Where would you like to place your project?");
|
|
203
|
+
expect(text).toMatch(/Which type of project\?\n│ library\n/);
|
|
204
|
+
expect(existsSync(join(dir, "tsdown.config.js")), "the library base ran").toBe(true);
|
|
198
205
|
expect(text).not.toContain("cd ");
|
|
199
206
|
|
|
200
207
|
let manifest = JSON.parse(await readFile(join(dir, "package.json"), "utf-8"));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { generate, mktemp } from "#test-helpers";
|
|
3
|
+
import { detectProjectType } from "#bases";
|
|
4
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import type { ProjectType } from "#types";
|
|
7
|
+
|
|
8
|
+
const dirs: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterAll(async () => {
|
|
11
|
+
if (process.env.CI) return;
|
|
12
|
+
|
|
13
|
+
for (const dir of dirs) {
|
|
14
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function fixture(files: Record<string, string>) {
|
|
19
|
+
let dir = await mktemp("detect-project-type");
|
|
20
|
+
dirs.push(dir);
|
|
21
|
+
|
|
22
|
+
for (let [file, contents] of Object.entries(files)) {
|
|
23
|
+
await mkdir(dirname(join(dir, file)), { recursive: true });
|
|
24
|
+
await writeFile(join(dir, file), contents);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("detectProjectType", () => {
|
|
31
|
+
const types: ProjectType[] = ["app", "library", "extension", "custom-element"];
|
|
32
|
+
|
|
33
|
+
for (const type of types) {
|
|
34
|
+
it(`detects a generated ${type}`, async () => {
|
|
35
|
+
const project = await generate({ type });
|
|
36
|
+
dirs.push(project.directory);
|
|
37
|
+
|
|
38
|
+
expect(detectProjectType(project.directory)).toBe(type);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it("detects a v2 addon with a docs app as a library", async () => {
|
|
43
|
+
const dir = await fixture({
|
|
44
|
+
"package.json": JSON.stringify({ "ember-addon": { version: 2, type: "addon" } }),
|
|
45
|
+
"index.html": "<!doctype html>",
|
|
46
|
+
"src/index.ts": "",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(detectProjectType(dir)).toBe("library");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects a classic addon as a library", async () => {
|
|
53
|
+
const dir = await fixture({
|
|
54
|
+
"package.json": JSON.stringify({ keywords: ["ember-addon"] }),
|
|
55
|
+
"addon/index.js": "",
|
|
56
|
+
"app/index.js": "",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(detectProjectType(dir)).toBe("library");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("detects a vite app", async () => {
|
|
63
|
+
const dir = await fixture({
|
|
64
|
+
"package.json": JSON.stringify({ "ember-addon": { version: 2, type: "app" } }),
|
|
65
|
+
"index.html": "<!doctype html>",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(detectProjectType(dir)).toBe("app");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("detects an app with a web app manifest as an app", async () => {
|
|
72
|
+
const dir = await fixture({
|
|
73
|
+
"package.json": "{}",
|
|
74
|
+
"app/app.ts": "",
|
|
75
|
+
"public/manifest.json": JSON.stringify({ name: "my-pwa", display: "standalone" }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(detectProjectType(dir)).toBe("app");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("makes no guess for a project it does not know", async () => {
|
|
82
|
+
const dir = await fixture({ "package.json": JSON.stringify({ name: "a-node-script" }) });
|
|
83
|
+
|
|
84
|
+
expect(detectProjectType(dir)).toBe(undefined);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -131,7 +131,7 @@ describe("layer: qunit", () => {
|
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
describe("app", () => {
|
|
134
|
-
it("it boots and run tests", async () => {
|
|
134
|
+
it("it boots and run tests", { timeout: 300_000 }, async () => {
|
|
135
135
|
const project = await generate({ type: "app", layers: ["qunit"] });
|
|
136
136
|
dirs.push(project.directory);
|
|
137
137
|
|
|
@@ -161,6 +161,7 @@ describe("layer: qunit", () => {
|
|
|
161
161
|
"README.md",
|
|
162
162
|
"config/test/babel.config.js",
|
|
163
163
|
"config/test/testem.cjs",
|
|
164
|
+
"config/tsconfig.publish.json",
|
|
164
165
|
"package.json",
|
|
165
166
|
"src/index.ts",
|
|
166
167
|
"tests/rendering/.gitkeep",
|
|
@@ -258,24 +259,51 @@ describe("layer: qunit", () => {
|
|
|
258
259
|
`);
|
|
259
260
|
});
|
|
260
261
|
|
|
261
|
-
it("
|
|
262
|
+
it("builds with a publish tsconfig, so tsconfig.json can cover the tests", async () => {
|
|
262
263
|
expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
|
|
263
264
|
"import { defineConfig } from "tsdown";
|
|
264
265
|
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
export default defineConfig({ entry: ["./src/index.ts"], plugins: [ember()], tsconfig: "./config/tsconfig.publish.json" })"
|
|
267
|
+
`);
|
|
268
|
+
expect(await project.read("config/tsconfig.publish.json")).toMatchInlineSnapshot(`
|
|
269
|
+
"{
|
|
270
|
+
"extends": "../tsconfig.json",
|
|
271
|
+
"include": ["../src"],
|
|
272
|
+
"compilerOptions": {
|
|
273
|
+
"rootDir": "../src",
|
|
274
|
+
"isolatedDeclarations": true
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
"
|
|
278
|
+
`);
|
|
279
|
+
expect(await project.read("tsconfig.json")).toMatchInlineSnapshot(`
|
|
280
|
+
"{
|
|
281
|
+
"extends": "@ember/library-tsconfig",
|
|
282
|
+
"include": ["src", "tests"],
|
|
283
|
+
"compilerOptions": {
|
|
284
|
+
"lib": ["esnext", "dom", "dom.iterable"],
|
|
285
|
+
"types": ["ember-source/types", "@glint/ember-tsc/types"],
|
|
286
|
+
},
|
|
287
|
+
"contentMappers": [
|
|
288
|
+
{
|
|
289
|
+
"package": "ember-content-mapper",
|
|
290
|
+
"extensions": [".gts", ".gjs"],
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
}
|
|
270
294
|
"
|
|
271
295
|
`);
|
|
272
296
|
});
|
|
273
297
|
|
|
274
|
-
it(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
it(
|
|
299
|
+
"installs, and tests generated against the library's exports pass",
|
|
300
|
+
{ timeout: 300_000 },
|
|
301
|
+
async () => {
|
|
302
|
+
await writeLibrarySource(project, "typescript");
|
|
303
|
+
await writeExampleTests(project, "typescript");
|
|
304
|
+
await installAndTest(project);
|
|
305
|
+
},
|
|
306
|
+
);
|
|
279
307
|
|
|
280
308
|
it("the publish build still ships precompileTemplate", async () => {
|
|
281
309
|
let build = await execa("pnpm build", { cwd: project.directory, shell: true });
|
|
@@ -288,6 +316,29 @@ describe("layer: qunit", () => {
|
|
|
288
316
|
});
|
|
289
317
|
});
|
|
290
318
|
|
|
319
|
+
describe("library (TypeScript) with eslint", () => {
|
|
320
|
+
it("lints the tests", { timeout: 300_000 }, async () => {
|
|
321
|
+
const project = await generate({
|
|
322
|
+
type: "library",
|
|
323
|
+
name: "my-lib",
|
|
324
|
+
layers: ["eslint-bundled", "qunit", "typescript"],
|
|
325
|
+
});
|
|
326
|
+
dirs.push(project.directory);
|
|
327
|
+
|
|
328
|
+
let install = await execa("pnpm install", { cwd: project.directory, shell: true });
|
|
329
|
+
expect(install.exitCode).toBe(0);
|
|
330
|
+
|
|
331
|
+
// eslint's project service cannot parse a file that no tsconfig includes
|
|
332
|
+
let lint = await execa("pnpm lint:eslint", {
|
|
333
|
+
cwd: project.directory,
|
|
334
|
+
shell: true,
|
|
335
|
+
reject: false,
|
|
336
|
+
all: true,
|
|
337
|
+
});
|
|
338
|
+
expect(lint.exitCode, lint.all).toBe(0);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
291
342
|
describe("library (JavaScript)", () => {
|
|
292
343
|
let project: Project;
|
|
293
344
|
|
|
@@ -363,11 +414,15 @@ describe("layer: qunit", () => {
|
|
|
363
414
|
`);
|
|
364
415
|
});
|
|
365
416
|
|
|
366
|
-
it(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
417
|
+
it(
|
|
418
|
+
"installs, and tests generated against the library's exports pass",
|
|
419
|
+
{ timeout: 300_000 },
|
|
420
|
+
async () => {
|
|
421
|
+
await writeLibrarySource(project, "javascript");
|
|
422
|
+
await writeExampleTests(project, "javascript");
|
|
423
|
+
await installAndTest(project);
|
|
424
|
+
},
|
|
425
|
+
);
|
|
371
426
|
|
|
372
427
|
it("the publish build still ships precompileTemplate", async () => {
|
|
373
428
|
let build = await execa("pnpm build", { cwd: project.directory, shell: true });
|