ember.nvp 0.7.0 → 1.1.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.
Files changed (47) hide show
  1. package/.github/workflows/ci.yml +6 -0
  2. package/.release-plan.json +7 -59
  3. package/CHANGELOG.md +46 -0
  4. package/package.json +1 -1
  5. package/packages/rolldown/README.md +68 -1
  6. package/packages/rolldown/index.ts +8 -4
  7. package/packages/rolldown/package.json +13 -4
  8. package/packages/rolldown/rolldown.config.js +5 -3
  9. package/packages/rolldown/src/app-reexports.test.ts +220 -0
  10. package/packages/rolldown/src/app-reexports.ts +152 -0
  11. package/packages/rolldown/src/config.ts +50 -0
  12. package/packages/rolldown/src/dts.test.ts +166 -0
  13. package/packages/rolldown/src/transform.test.ts +173 -0
  14. package/src/bases/minimal-extension/extension.test.ts +251 -0
  15. package/src/bases/minimal-extension/files/.env.development +8 -0
  16. package/src/bases/minimal-extension/files/README.md +36 -0
  17. package/src/bases/minimal-extension/files/app/app.ts +21 -0
  18. package/src/bases/minimal-extension/files/app/boot.ts +8 -0
  19. package/src/bases/minimal-extension/files/app/config.ts +22 -0
  20. package/src/bases/minimal-extension/files/app/router.ts +10 -0
  21. package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
  22. package/src/bases/minimal-extension/files/babel.config.js +49 -0
  23. package/src/bases/minimal-extension/files/index.html +17 -0
  24. package/src/bases/minimal-extension/files/package.json +58 -0
  25. package/src/bases/minimal-extension/files/public/manifest.json +9 -0
  26. package/src/bases/minimal-extension/files/tsconfig.json +13 -0
  27. package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
  28. package/src/bases/minimal-extension/index.js +142 -0
  29. package/src/bases/minimal-library/files/tsdown.config.js +2 -1
  30. package/src/cli/generator.js +4 -0
  31. package/src/cli/questions/project-type.js +9 -2
  32. package/src/layers/renovate/index.js +4 -3
  33. package/src/layers/typescript/index.js +6 -0
  34. package/src/utils/types.ts +1 -1
  35. package/test/ember.nvp/base-library.test.ts +41 -30
  36. package/test/ember.nvp/layer-publish-checks.test.ts +7 -10
  37. package/test/ember.nvp/layer-qunit.test.ts +17 -30
  38. package/test/ember.nvp/layer-vitest.test.ts +4 -18
  39. package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
  40. package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
  41. package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
  42. package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
  43. package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
  44. package/test/ember.nvp/permutations/shared.ts +68 -19
  45. package/test/test-helpers.ts +25 -5
  46. package/vitest.config.js +1 -1
  47. package/packages/rolldown/src/define-config.ts +0 -32
@@ -1,6 +1,7 @@
1
1
  import { mkdir, rm } from "node:fs/promises";
2
2
 
3
3
  import baseApp from "#bases/minimal-app";
4
+ import baseExtension from "#bases/minimal-extension";
4
5
  import baseLibrary from "#bases/minimal-library";
5
6
  import { consolidateLintingScripts } from "../consolidators/linting.js";
6
7
  import { hasGit } from "#utils/git.js";
@@ -24,6 +25,9 @@ export async function generateProject(project, replaceOrUpdate) {
24
25
  case "library":
25
26
  await baseLibrary.run(project);
26
27
  break;
28
+ case "extension":
29
+ await baseExtension.run(project);
30
+ break;
27
31
  }
28
32
 
29
33
  /**
@@ -3,12 +3,13 @@ import { answers, printArgInUse } from "#args";
3
3
 
4
4
  /**
5
5
  * addon is alias for library
6
+ * web-extension is alias for extension
6
7
  */
7
- const SUPPORTED = new Set(["app", "library", "addon"]);
8
+ const SUPPORTED = new Set(["app", "library", "addon", "extension", "web-extension"]);
8
9
 
9
10
  /**
10
11
  * @param {string | undefined} selected
11
- * @returns {selected is "app" | "library" | "addon"}
12
+ * @returns {selected is "app" | "library" | "addon" | "extension" | "web-extension"}
12
13
  */
13
14
  function isValid(selected) {
14
15
  if (!selected) return false;
@@ -25,6 +26,7 @@ export async function askProjectType() {
25
26
  printArgInUse("type", answers.type);
26
27
 
27
28
  if (answers.type === "addon") return "library";
29
+ if (answers.type === "web-extension") return "extension";
28
30
 
29
31
  return answers.type;
30
32
  }
@@ -38,6 +40,11 @@ export async function askProjectType() {
38
40
  label: "library",
39
41
  hint: "sharable code, usable by other libraries and apps alike",
40
42
  },
43
+ {
44
+ value: "extension",
45
+ label: "browser extension",
46
+ hint: "Manifest V3 extension whose popup is an Ember app",
47
+ },
41
48
  ],
42
49
  });
43
50
 
@@ -14,10 +14,11 @@ export default {
14
14
  return;
15
15
  }
16
16
 
17
+ // extensions aren't published to npm either, so they follow the app config
17
18
  let source =
18
- project.type === "app"
19
- ? join(import.meta.dirname, "files/app.json5")
20
- : join(import.meta.dirname, "files/library.json5");
19
+ project.type === "library"
20
+ ? join(import.meta.dirname, "files/library.json5")
21
+ : join(import.meta.dirname, "files/app.json5");
21
22
 
22
23
  let destination = join(project.directory, ".github/renovate.json5");
23
24
 
@@ -7,6 +7,7 @@ import { getLatest } from "#utils/npm.js";
7
7
 
8
8
  const bases = join(import.meta.dirname, "../../bases");
9
9
  const appBase = join(bases, "minimal-app/files");
10
+ const extensionBase = join(bases, "minimal-extension/files");
10
11
  const libraryBase = join(bases, "minimal-library/files");
11
12
 
12
13
  const sharedDeps = {
@@ -149,6 +150,11 @@ async function addTSConfig(project) {
149
150
  return;
150
151
  }
151
152
 
153
+ if (project.type === "extension") {
154
+ await cp(join(extensionBase, "tsconfig.json"), project.path("tsconfig.json"));
155
+ return;
156
+ }
157
+
152
158
  if (project.type === "library") {
153
159
  await cp(join(libraryBase, "tsconfig.json"), project.path("tsconfig.json"));
154
160
  }
@@ -1,6 +1,6 @@
1
1
  import { Project } from "./project.js";
2
2
  export type PackageManager = "pnpm" | "npm";
3
- export type ProjectType = "app" | "library";
3
+ export type ProjectType = "app" | "library" | "extension";
4
4
 
5
5
  export interface Layer {
6
6
  /**
@@ -1,9 +1,9 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
- import { generate, mktemp } from "#test-helpers";
2
+ import { generate, listFiles, mktemp } from "#test-helpers";
3
3
  import { writeLibrarySource } from "./library-src-fixtures.ts";
4
4
  import { execa } from "execa";
5
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6
- import { join, relative, sep } from "node:path";
5
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
7
 
8
8
  import type { Project } from "ember.nvp";
9
9
 
@@ -14,20 +14,6 @@ import type { Project } from "ember.nvp";
14
14
  * them.
15
15
  */
16
16
 
17
- async function listFiles(directory: string): Promise<string[]> {
18
- const entries = await readdir(directory, { withFileTypes: true, recursive: true });
19
-
20
- return entries
21
- .filter((entry) => entry.isFile())
22
- .map((entry) => relative(directory, join(entry.parentPath, entry.name)))
23
- .filter((path) => !path.split(sep).includes("node_modules"))
24
- .sort();
25
- }
26
-
27
- async function read(project: Project, filePath: string): Promise<string> {
28
- return readFile(join(project.directory, filePath), "utf-8");
29
- }
30
-
31
17
  async function installAndBuild(project: Project) {
32
18
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
33
19
  expect(install.exitCode).toBe(0);
@@ -66,11 +52,11 @@ describe("base: minimal-library", () => {
66
52
  ]
67
53
  `);
68
54
 
69
- expect(await read(project, "src/index.js")).toBe("");
55
+ expect(await project.read("src/index.js")).toBe("");
70
56
  });
71
57
 
72
58
  it("is publishable", async () => {
73
- let manifest = JSON.parse(await read(project, "package.json"));
59
+ let manifest = JSON.parse((await project.read("package.json"))!);
74
60
 
75
61
  // The template is private (it lives in a workspace); the generated
76
62
  // library must not be
@@ -78,15 +64,16 @@ describe("base: minimal-library", () => {
78
64
  });
79
65
 
80
66
  it("has no TypeScript leftovers", async () => {
81
- let manifest = JSON.parse(await read(project, "package.json"));
67
+ let manifest = JSON.parse((await project.read("package.json"))!);
82
68
 
83
69
  expect(manifest.devDependencies).not.toHaveProperty("typescript");
84
70
  expect(manifest.devDependencies).not.toHaveProperty("@ember/library-tsconfig");
85
71
  // No declarations are emitted without types
86
72
  expect(JSON.stringify(manifest.exports)).not.toContain("types");
87
73
 
88
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
89
- "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
74
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
75
+ "import { defineConfig } from "tsdown";
76
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
90
77
 
91
78
  export default defineConfig({
92
79
  entry: ["./src/index.js"],
@@ -128,7 +115,7 @@ describe("base: minimal-library", () => {
128
115
  await writeLibrarySource(project, "javascript");
129
116
  await installAndBuild(project);
130
117
 
131
- let output = await read(project, "dist/index.js");
118
+ let output = await project.read("dist/index.js");
132
119
 
133
120
  // Published libraries ship precompileTemplate (the consuming app does
134
121
  // the final compile), never wire format
@@ -164,7 +151,7 @@ describe("base: minimal-library", () => {
164
151
  ]
165
152
  `);
166
153
 
167
- expect(await read(project, "src/index.ts")).toBe("");
154
+ expect(await project.read("src/index.ts")).toBe("");
168
155
  });
169
156
 
170
157
  it("type checks", async () => {
@@ -180,16 +167,40 @@ describe("base: minimal-library", () => {
180
167
  it("builds, including declarations", async () => {
181
168
  await installAndBuild(project);
182
169
 
183
- let output = await read(project, "dist/index.js");
170
+ let output = await project.read("dist/index.js");
184
171
 
185
172
  expect(output).toContain("precompileTemplate");
186
173
  expect(output).not.toContain("createTemplateFactory");
187
174
 
188
- let declarations = await read(project, "dist/index.d.ts");
189
-
190
- expect(declarations).toContain("declare class Greeting extends Component<GreetingSignature>");
191
- expect(declarations).toContain("declare const Badge: TOC<BadgeSignature>");
192
- expect(declarations).toContain("declare function add(a: number, b: number): number");
175
+ let declarations = await project.read("dist/index.d.ts");
176
+
177
+ expect(declarations).toMatchInlineSnapshot(`
178
+ "import { TOC } from "@ember/component/template-only";
179
+ import Component from "@glimmer/component";
180
+ //#region src/components/badge.d.ts
181
+ interface BadgeSignature {
182
+ Element: HTMLSpanElement;
183
+ Blocks: {
184
+ default: [];
185
+ };
186
+ }
187
+ declare const Badge: TOC<BadgeSignature>;
188
+ //#endregion
189
+ //#region src/components/greeting.d.ts
190
+ interface GreetingSignature {
191
+ Element: HTMLParagraphElement;
192
+ Args: {
193
+ name: string;
194
+ };
195
+ }
196
+ declare class Greeting extends Component<GreetingSignature> {}
197
+ //#endregion
198
+ //#region src/utils/math.d.ts
199
+ declare function add(a: number, b: number): number;
200
+ //#endregion
201
+ export { Badge, type BadgeSignature, Greeting, type GreetingSignature, add };
202
+ //# sourceMappingURL=index.d.ts.map"
203
+ `);
193
204
 
194
205
  expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
195
206
  [
@@ -1,8 +1,7 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
2
  import { generate } from "#test-helpers";
3
3
  import { execa } from "execa";
4
- import { readFile, rm } from "node:fs/promises";
5
- import { join } from "node:path";
4
+ import { rm } from "node:fs/promises";
6
5
 
7
6
  import type { Project } from "ember.nvp";
8
7
 
@@ -12,10 +11,6 @@ import type { Project } from "ember.nvp";
12
11
  * output.
13
12
  */
14
13
 
15
- async function read(project: Project, filePath: string): Promise<string> {
16
- return readFile(join(project.directory, filePath), "utf-8");
17
- }
18
-
19
14
  describe("layers: publint + are-the-types-wrong", () => {
20
15
  const dirs: string[] = [];
21
16
 
@@ -40,8 +35,9 @@ describe("layers: publint + are-the-types-wrong", () => {
40
35
  });
41
36
 
42
37
  it("wires both checks into the build", async () => {
43
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
44
- "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
38
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
39
+ "import { defineConfig } from "tsdown";
40
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
45
41
  export default defineConfig({ entry: ["./src/index.ts"], plugins: [ember()], attw: { profile: "esm-only" }, publint: true })"
46
42
  `);
47
43
  });
@@ -77,8 +73,9 @@ describe("layers: publint + are-the-types-wrong", () => {
77
73
  });
78
74
 
79
75
  it("wires publint only (no declarations to check)", async () => {
80
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
81
- "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
76
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
77
+ "import { defineConfig } from "tsdown";
78
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
82
79
  export default defineConfig({ entry: ["./src/index.js"], dts: false, plugins: [ember()], publint: true })"
83
80
  `);
84
81
  });
@@ -1,27 +1,12 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
- import { generate } from "#test-helpers";
2
+ import { generate, listFiles } from "#test-helpers";
3
3
  import { writeLibrarySource } from "./library-src-fixtures.ts";
4
4
  import { execa } from "execa";
5
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6
- import { dirname, join, relative, sep } from "node:path";
5
+ import { mkdir, rm, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
7
 
8
8
  import type { Project } from "ember.nvp";
9
9
 
10
- async function listFiles(directory: string): Promise<string[]> {
11
- const entries = await readdir(directory, { withFileTypes: true, recursive: true });
12
-
13
- return entries
14
- .filter((entry) => entry.isFile())
15
- .map((entry) => relative(directory, join(entry.parentPath, entry.name)))
16
- .filter((path) => !path.split(sep).includes("node_modules"))
17
- .filter((path) => !path.startsWith(`dist${sep}`))
18
- .sort();
19
- }
20
-
21
- async function read(project: Project, filePath: string): Promise<string> {
22
- return readFile(join(project.directory, filePath), "utf-8");
23
- }
24
-
25
10
  async function installAndTest(project: Project) {
26
11
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
27
12
  expect(install.exitCode).toBe(0);
@@ -167,7 +152,7 @@ describe("layer: qunit", () => {
167
152
  });
168
153
 
169
154
  it("generates the expected files, without any index.html", async () => {
170
- let files = await listFiles(project.directory);
155
+ let files = await listFiles(project.directory, { includeDist: false });
171
156
 
172
157
  expect(files.some((file) => file.endsWith("index.html"))).toBe(false);
173
158
  expect(files).toMatchInlineSnapshot(`
@@ -189,7 +174,7 @@ describe("layer: qunit", () => {
189
174
  });
190
175
 
191
176
  it("generates the test build config", async () => {
192
- expect(await read(project, "vite.config.mjs")).toMatchInlineSnapshot(`
177
+ expect(await project.read("vite.config.mjs")).toMatchInlineSnapshot(`
193
178
  "import { defineConfig } from "vite";
194
179
  import { ember } from "@nullvoxpopuli/ember-vite";
195
180
 
@@ -198,7 +183,7 @@ describe("layer: qunit", () => {
198
183
  });
199
184
  "
200
185
  `);
201
- expect(await read(project, "config/test/babel.config.js")).toMatchInlineSnapshot(`
186
+ expect(await project.read("config/test/babel.config.js")).toMatchInlineSnapshot(`
202
187
  "import { buildMacros } from "@embroider/macros/babel";
203
188
 
204
189
  const macros = buildMacros({
@@ -242,7 +227,7 @@ describe("layer: qunit", () => {
242
227
  };
243
228
  "
244
229
  `);
245
- expect(await read(project, "config/test/testem.cjs")).toMatchInlineSnapshot(`
230
+ expect(await project.read("config/test/testem.cjs")).toMatchInlineSnapshot(`
246
231
  ""use strict";
247
232
 
248
233
  if (typeof module !== "undefined") {
@@ -274,8 +259,9 @@ describe("layer: qunit", () => {
274
259
  });
275
260
 
276
261
  it("leaves the publish build config alone", async () => {
277
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
278
- "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
262
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
263
+ "import { defineConfig } from "tsdown";
264
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
279
265
 
280
266
  export default defineConfig({
281
267
  entry: ["./src/index.ts"],
@@ -295,7 +281,7 @@ describe("layer: qunit", () => {
295
281
  let build = await execa("pnpm build", { cwd: project.directory, shell: true });
296
282
  expect(build.exitCode).toBe(0);
297
283
 
298
- let output = await read(project, "dist/index.js");
284
+ let output = await project.read("dist/index.js");
299
285
 
300
286
  expect(output).toContain("precompileTemplate");
301
287
  expect(output).not.toContain("createTemplateFactory");
@@ -311,7 +297,7 @@ describe("layer: qunit", () => {
311
297
  });
312
298
 
313
299
  it("generates the expected files, without any index.html", async () => {
314
- let files = await listFiles(project.directory);
300
+ let files = await listFiles(project.directory, { includeDist: false });
315
301
 
316
302
  expect(files.some((file) => file.endsWith("index.html"))).toBe(false);
317
303
  expect(files).toMatchInlineSnapshot(`
@@ -332,7 +318,7 @@ describe("layer: qunit", () => {
332
318
  });
333
319
 
334
320
  it("has no TypeScript leftovers in the test build config", async () => {
335
- expect(await read(project, "config/test/babel.config.js")).toMatchInlineSnapshot(`
321
+ expect(await project.read("config/test/babel.config.js")).toMatchInlineSnapshot(`
336
322
  "import { buildMacros } from "@embroider/macros/babel";
337
323
 
338
324
  const macros = buildMacros({
@@ -364,8 +350,9 @@ describe("layer: qunit", () => {
364
350
  };
365
351
  "
366
352
  `);
367
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
368
- "import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
353
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
354
+ "import { defineConfig } from "tsdown";
355
+ import { ember } from "@nullvoxpopuli/ember-rolldown";
369
356
 
370
357
  export default defineConfig({
371
358
  entry: ["./src/index.js"],
@@ -386,7 +373,7 @@ describe("layer: qunit", () => {
386
373
  let build = await execa("pnpm build", { cwd: project.directory, shell: true });
387
374
  expect(build.exitCode).toBe(0);
388
375
 
389
- let output = await read(project, "dist/index.js");
376
+ let output = await project.read("dist/index.js");
390
377
 
391
378
  expect(output).toContain("precompileTemplate");
392
379
  expect(output).not.toContain("createTemplateFactory");
@@ -1,9 +1,9 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
- import { generate } from "#test-helpers";
2
+ import { generate, listFiles } from "#test-helpers";
3
3
  import { writeLibrarySource } from "./library-src-fixtures.ts";
4
4
  import { execa } from "execa";
5
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6
- import { dirname, join, relative, sep } from "node:path";
5
+ import { mkdir, rm, writeFile } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
7
 
8
8
  import type { Project } from "ember.nvp";
9
9
 
@@ -14,20 +14,6 @@ import type { Project } from "ember.nvp";
14
14
  * headless-browser mode), asserting the specs genuinely run and pass.
15
15
  */
16
16
 
17
- async function listFiles(directory: string): Promise<string[]> {
18
- const entries = await readdir(directory, { withFileTypes: true, recursive: true });
19
-
20
- return entries
21
- .filter((entry) => entry.isFile())
22
- .map((entry) => relative(directory, join(entry.parentPath, entry.name)))
23
- .filter((path) => !path.split(sep).includes("node_modules"))
24
- .sort();
25
- }
26
-
27
- async function read(project: Project, filePath: string): Promise<string> {
28
- return readFile(join(project.directory, filePath), "utf-8");
29
- }
30
-
31
17
  async function emit(project: Project, files: Record<string, string>) {
32
18
  for (let [path, contents] of Object.entries(files)) {
33
19
  let filePath = join(project.directory, path);
@@ -200,7 +186,7 @@ describe("layer: vitest", () => {
200
186
  });
201
187
 
202
188
  it("generates the vitest config", async () => {
203
- expect(await read(project, "vitest.config.mjs")).toMatchInlineSnapshot(`
189
+ expect(await project.read("vitest.config.mjs")).toMatchInlineSnapshot(`
204
190
  "import { ember } from "@nullvoxpopuli/ember-vite";
205
191
  import { webdriverio } from "@vitest/browser-webdriverio";
206
192
  import { defineConfig } from "vitest/config";
@@ -0,0 +1,3 @@
1
+ import { testPermutations } from "./shared.ts";
2
+
3
+ testPermutations("minimal-app", { group: "checks" });
@@ -0,0 +1,3 @@
1
+ import { testPermutations } from "./shared.ts";
2
+
3
+ testPermutations("minimal-extension", { index: 0, total: 2 });
@@ -0,0 +1,3 @@
1
+ import { testPermutations } from "./shared.ts";
2
+
3
+ testPermutations("minimal-extension", { index: 1, total: 2 });
@@ -0,0 +1,3 @@
1
+ import { testPermutations } from "./shared.ts";
2
+
3
+ testPermutations("minimal-extension", { group: "checks" });
@@ -0,0 +1,3 @@
1
+ import { testPermutations } from "./shared.ts";
2
+
3
+ testPermutations("minimal-library", { group: "checks" });
@@ -30,30 +30,73 @@ const NOT_YET_SUPPORTED: Record<ProjectType, Set<string>> = {
30
30
  app: new Set(["publint", "are-the-types-wrong"]),
31
31
  // libraries have no Application to wire inspector support into
32
32
  library: new Set(["inspector-support"]),
33
+ // publish checks: extensions aren't published (to npm)
34
+ extension: new Set(["publint", "are-the-types-wrong"]),
33
35
  };
34
36
 
37
+ const TYPE_FOR_BASE: Record<(typeof bases)[number], ProjectType> = {
38
+ "minimal-app": "app",
39
+ "minimal-library": "library",
40
+ "minimal-extension": "extension",
41
+ };
42
+
43
+ /**
44
+ * Layers that only add lint / format / publish checks. They don't affect
45
+ * the runtime shape of a project, so they permutate among themselves in a
46
+ * small "checks" matrix instead of multiplying the main one. ("apply anew"
47
+ * still applies each of them on top of every main permutation, so linear
48
+ * cross-group coverage remains.)
49
+ */
50
+ const CHECK_LAYERS = new Set<string>([
51
+ "eslint-bundled-ember",
52
+ "eslint-bundled-nvp",
53
+ "eslint-ejected",
54
+ "prettier",
55
+ "publint",
56
+ "are-the-types-wrong",
57
+ ]);
58
+
59
+ type PermutationGroup = "main" | "checks";
60
+
35
61
  const eachBase = bases.map((base) => {
36
- const type: ProjectType = base === "minimal-app" ? "app" : "library";
62
+ const type = TYPE_FOR_BASE[base]!;
37
63
 
38
64
  const applicableLayers = layers.filter((layer) => !NOT_YET_SUPPORTED[type].has(layer.name));
39
65
 
40
- const permutations = permutate(applicableLayers.map((layer) => layer.name));
41
- permutations.push([baseline]);
66
+ const toPermutations = (layerNames: string[], withBaseline: boolean) => {
67
+ const permutations = permutate(layerNames);
68
+
69
+ if (withBaseline) {
70
+ permutations.push([baseline]);
71
+ }
42
72
 
43
- const eachPermutation = permutations
44
- .filter((permutation) => {
45
- if (INITIAL_ONLY.size > 0 && !permutation.some((x) => INITIAL_ONLY.has(x))) {
46
- return;
47
- }
73
+ return permutations
74
+ .filter((permutation) => {
75
+ if (INITIAL_ONLY.size > 0 && !permutation.some((x) => INITIAL_ONLY.has(x))) {
76
+ return;
77
+ }
48
78
 
49
- // Not implemented yet
50
- if (permutation.some((x) => TODO.has(x))) {
51
- return;
52
- }
79
+ // Not implemented yet
80
+ if (permutation.some((x) => TODO.has(x))) {
81
+ return;
82
+ }
53
83
 
54
- return true;
55
- })
56
- .map((permutation) => ({ permutation, name: permutation.join(", ") }));
84
+ return true;
85
+ })
86
+ .map((permutation) => ({ permutation, name: permutation.join(", ") }));
87
+ };
88
+
89
+ const applicableNames = applicableLayers.map((layer) => layer.name);
90
+ const eachPermutation: Record<PermutationGroup, ReturnType<typeof toPermutations>> = {
91
+ main: toPermutations(
92
+ applicableNames.filter((name) => !CHECK_LAYERS.has(name)),
93
+ true,
94
+ ),
95
+ checks: toPermutations(
96
+ applicableNames.filter((name) => CHECK_LAYERS.has(name)),
97
+ false,
98
+ ),
99
+ };
57
100
 
58
101
  return { name: base, type, applicableLayers, eachPermutation };
59
102
  });
@@ -62,14 +105,20 @@ const eachBase = bases.map((base) => {
62
105
  * The permutation matrix grows exponentially with the layer count, so
63
106
  * each base runs from its own test file(s) -- CI runs them as separate,
64
107
  * parallel jobs. A base whose matrix outgrows one job's budget runs from
65
- * several files, each taking a deterministic 1-of-N slice.
108
+ * several files, each taking a deterministic 1-of-N slice. The "checks"
109
+ * group (CHECK_LAYERS) runs from its own file(s) the same way.
66
110
  *
67
111
  * @param base which base's permutations this file runs
68
- * @param slice which 1-of-N slice of the permutations this file runs
112
+ * @param options.index / options.total which 1-of-N slice of the permutations this file runs
113
+ * @param options.group which permutation group this file runs
69
114
  */
70
115
  export function testPermutations(
71
116
  base: (typeof bases)[number],
72
- slice: { index: number; total: number } = { index: 0, total: 1 },
117
+ {
118
+ index = 0,
119
+ total = 1,
120
+ group = "main",
121
+ }: { index?: number; total?: number; group?: PermutationGroup } = {},
73
122
  ) {
74
123
  const {
75
124
  type,
@@ -77,7 +126,7 @@ export function testPermutations(
77
126
  eachPermutation: allPermutations,
78
127
  } = eachBase.find((b) => b.name === base)!;
79
128
 
80
- const eachPermutation = allPermutations.filter((_, i) => i % slice.total === slice.index);
129
+ const eachPermutation = allPermutations[group].filter((_, i) => i % total === index);
81
130
 
82
131
  describe(base, () => {
83
132
  describe.each(eachPermutation)("layers: $name", ({ permutation }) => {
@@ -1,18 +1,19 @@
1
1
  import { execa } from "execa";
2
- import { join } from "node:path";
2
+ import { join, relative, sep } from "node:path";
3
3
  import { styleText } from "node:util";
4
4
  import { Readable, Writable } from "node:stream";
5
- import { mkdtemp } from "node:fs/promises";
5
+ import { mkdtemp, readdir } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { generateProject, Project } from "ember.nvp";
8
8
  import { discoverLayers } from "#layers";
9
- import type { DiscoveredLayer } from "#types";
9
+ import type { DiscoveredLayer, ProjectType } from "#types";
10
10
  import { expect } from "vitest";
11
11
 
12
12
  const minimalApp = "minimal-app";
13
13
  const minimalAddon = "minimal-library";
14
+ const minimalExtension = "minimal-extension";
14
15
 
15
- export const bases = [minimalApp, minimalAddon];
16
+ export const bases = [minimalApp, minimalAddon, minimalExtension];
16
17
 
17
18
  export const layers = await discoverLayers();
18
19
 
@@ -87,7 +88,7 @@ export async function generate({
87
88
  directory?: string;
88
89
  layers?: string[];
89
90
  name?: string;
90
- type?: "app" | "library";
91
+ type?: ProjectType;
91
92
  packageManager?: "pnpm" | "npm";
92
93
  }): Promise<Project> {
93
94
  const tempDir = directory ?? (await mktemp(name));
@@ -186,3 +187,22 @@ export async function expectIsSetup(project: Project, layer: DiscoveredLayer) {
186
187
 
187
188
  expect(result, `${layer.name} is setup`).toBe(true);
188
189
  }
190
+
191
+ /**
192
+ * List every file under `directory` (recursively) as sorted paths relative
193
+ * to `directory`. `node_modules` is always excluded; pass
194
+ * `{ includeDist: false }` to also exclude the `dist/` build output.
195
+ */
196
+ export async function listFiles(
197
+ directory: string,
198
+ { includeDist = true }: { includeDist?: boolean } = {},
199
+ ): Promise<string[]> {
200
+ const entries = await readdir(directory, { withFileTypes: true, recursive: true });
201
+
202
+ return entries
203
+ .filter((entry) => entry.isFile())
204
+ .map((entry) => relative(directory, join(entry.parentPath, entry.name)))
205
+ .filter((path) => !path.split(sep).includes("node_modules"))
206
+ .filter((path) => includeDist || !path.startsWith(`dist${sep}`))
207
+ .sort();
208
+ }
package/vitest.config.js CHANGED
@@ -4,6 +4,6 @@ export default defineConfig({
4
4
  test: {
5
5
  testTimeout: 60_000,
6
6
  hookTimeout: 60_000,
7
- include: ["test/**/*.test.ts", "src/**/*.test.ts"],
7
+ include: ["test/**/*.test.ts", "src/**/*.test.ts", "packages/*/src/**/*.test.ts"],
8
8
  },
9
9
  });