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.
@@ -2,8 +2,8 @@
2
2
  "solution": {
3
3
  "ember.nvp": {
4
4
  "impact": "minor",
5
- "oldVersion": "1.11.2",
6
- "newVersion": "1.12.0",
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.12.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`\n * [#152](https://github.com/NullVoxPopuli/ember.nvp/pull/152) Ask about the project in the current directory first ([@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"
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 project name starts as the `package.json` name.
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.12.0",
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
@@ -1,9 +1,6 @@
1
1
  import { mkdir, rm } from "node:fs/promises";
2
2
 
3
- import baseApp from "#bases/minimal-app";
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
- switch (project.desires.type) {
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 { packageJson } from "ember-apply";
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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": ["../src"],
4
+ "compilerOptions": {
5
+ "rootDir": "../src",
6
+ "isolatedDeclarations": true
7
+ }
8
+ }
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
  *
@@ -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 fills in the project's name", async () => {
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(join(dir, "package.json"), JSON.stringify({ name: "@scope/my-lib" }));
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 --path, so the CLI asks about the project in this directory
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 = ["This directory has a package.json", "What is your project name?"];
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 name
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("leaves the publish build config alone", async () => {
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
- export default defineConfig({
267
- entry: ["./src/index.ts"],
268
- plugins: [ember()],
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("installs, and tests generated against the library's exports pass", async () => {
275
- await writeLibrarySource(project, "typescript");
276
- await writeExampleTests(project, "typescript");
277
- await installAndTest(project);
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("installs, and tests generated against the library's exports pass", async () => {
367
- await writeLibrarySource(project, "javascript");
368
- await writeExampleTests(project, "javascript");
369
- await installAndTest(project);
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 });