ember.nvp 1.0.0 → 1.2.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 (48) hide show
  1. package/.github/workflows/ci.yml +6 -0
  2. package/.release-plan.json +11 -15
  3. package/CHANGELOG.md +47 -0
  4. package/package.json +1 -1
  5. package/packages/rolldown/README.md +101 -1
  6. package/packages/rolldown/package.json +13 -4
  7. package/packages/rolldown/rolldown.config.js +5 -3
  8. package/packages/rolldown/src/app-reexports.test.ts +220 -0
  9. package/packages/rolldown/src/app-reexports.ts +152 -0
  10. package/packages/rolldown/src/babel.test.ts +17 -0
  11. package/packages/rolldown/src/babel.ts +21 -3
  12. package/packages/rolldown/src/config.ts +1 -1
  13. package/packages/rolldown/src/dts.test.ts +366 -0
  14. package/packages/rolldown/src/transform.test.ts +208 -0
  15. package/packages/rolldown/src/transform.ts +64 -31
  16. package/src/bases/minimal-extension/extension.test.ts +251 -0
  17. package/src/bases/minimal-extension/files/.env.development +8 -0
  18. package/src/bases/minimal-extension/files/README.md +36 -0
  19. package/src/bases/minimal-extension/files/app/app.ts +21 -0
  20. package/src/bases/minimal-extension/files/app/boot.ts +8 -0
  21. package/src/bases/minimal-extension/files/app/config.ts +22 -0
  22. package/src/bases/minimal-extension/files/app/router.ts +10 -0
  23. package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
  24. package/src/bases/minimal-extension/files/babel.config.js +49 -0
  25. package/src/bases/minimal-extension/files/index.html +17 -0
  26. package/src/bases/minimal-extension/files/package.json +58 -0
  27. package/src/bases/minimal-extension/files/public/manifest.json +9 -0
  28. package/src/bases/minimal-extension/files/tsconfig.json +13 -0
  29. package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
  30. package/src/bases/minimal-extension/index.js +142 -0
  31. package/src/cli/generator.js +4 -0
  32. package/src/cli/questions/project-type.js +9 -2
  33. package/src/layers/renovate/index.js +4 -3
  34. package/src/layers/typescript/index.js +6 -0
  35. package/src/utils/types.ts +1 -1
  36. package/test/ember.nvp/base-library.test.ts +39 -32
  37. package/test/ember.nvp/layer-publish-checks.test.ts +4 -13
  38. package/test/ember.nvp/layer-qunit.test.ts +13 -30
  39. package/test/ember.nvp/layer-vitest.test.ts +4 -18
  40. package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
  41. package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
  42. package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
  43. package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
  44. package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
  45. package/test/ember.nvp/permutations/shared.ts +68 -19
  46. package/test/ember.nvp/scoped-css-library.test.ts +211 -0
  47. package/test/test-helpers.ts +25 -30
  48. package/vitest.config.js +1 -1
@@ -0,0 +1,142 @@
1
+ import { packageJson } from "ember-apply";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getLatest } from "#utils/npm.js";
5
+ import { applyFolderTo } from "#utils/fs.js";
6
+ import { removeConfiguredPlugin } from "#utils/babel.js";
7
+
8
+ /**
9
+ * Minimal Extension Base
10
+ *
11
+ * Provides the absolute bare minimum for a browser extension (Manifest V3)
12
+ * whose popup is an Ember app:
13
+ * - type: "module" in package.json
14
+ * - public/manifest.json copied verbatim into dist
15
+ * - index.html popup booting from an external script (extension pages
16
+ * forbid inline scripts via CSP)
17
+ * - No testing framework
18
+ * - No linting or formatting
19
+ * - No ember-welcome-page
20
+ */
21
+ export default {
22
+ label: "Minimal Extension Base",
23
+ description: "Bare minimum browser extension structure with an Ember popup",
24
+
25
+ /**
26
+ * 1. Apply files
27
+ * a. Remove TS if needed
28
+ * 2. Remove TS deps/files if needed
29
+ * 3. Update name(s)
30
+ * 4. Upgrade in-range dependencies
31
+ * 5. Update an existing babel config, if any
32
+ *
33
+ * @param {import('#utils/project.js').Project} project
34
+ */
35
+ async run(project) {
36
+ await applyFiles(project);
37
+ await updateName(project);
38
+ await makeJavaScript(project);
39
+ await upgradeDependencies(project);
40
+ await updateBabelConfig(project);
41
+ },
42
+ };
43
+
44
+ /**
45
+ * @param {import('#utils/project.js').Project} project
46
+ */
47
+ async function updateBabelConfig(project) {
48
+ if (await project.hasOrWantsLayer("typescript")) return;
49
+
50
+ await removeConfiguredPlugin(project, "@babel/plugin-transform-typescript");
51
+ }
52
+
53
+ /**
54
+ * @param {import('#utils/project.js').Project} project
55
+ */
56
+ async function applyFiles(project) {
57
+ let filePath = join(import.meta.dirname, "files");
58
+
59
+ await applyFolderTo(filePath, project);
60
+ }
61
+
62
+ /**
63
+ * Operates on known files where the name matters
64
+ *
65
+ * @param {import('#utils/project.js').Project} project
66
+ */
67
+ async function updateName(project) {
68
+ await packageJson.modify((json) => {
69
+ json.name = project.desires.name;
70
+ }, project.directory);
71
+
72
+ if (!project.hasFile("public/manifest.json")) return;
73
+
74
+ let manifestPath = project.path("public/manifest.json");
75
+ let manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
76
+
77
+ manifest.name = project.desires.name;
78
+
79
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
80
+ }
81
+
82
+ /**
83
+ * @param {import('#utils/project.js').Project} project
84
+ */
85
+ async function makeJavaScript(project) {
86
+ if (await project.hasOrWantsLayer("typescript")) return;
87
+
88
+ /**
89
+ * We don't actually remove anything, because we want intellisense for JS
90
+ *
91
+ * But perhaps for the sake of minimal, we do remoev it.
92
+ * And we add a layer later for JSDoc ased TS or something
93
+ */
94
+ await packageJson.removeDevDependencies(
95
+ [
96
+ "@ember/app-tsconfig",
97
+ "@glint/ember-tsc",
98
+ "@glint/template",
99
+ "@glint/tsserver-plugin",
100
+ "typescript",
101
+ ],
102
+ project.directory,
103
+ );
104
+
105
+ await project.removeFile("tsconfig.json");
106
+
107
+ await packageJson.modify((json) => {
108
+ json.imports ||= {};
109
+ json.imports["#config"] = "./app/config.js";
110
+ }, project.directory);
111
+
112
+ /**
113
+ * The import-rewriting pass only handles module files; the popup's
114
+ * <script src> points at the boot module by path, so it has to follow
115
+ * the .ts -> .js rename by hand.
116
+ */
117
+ if (!project.hasFile("index.html")) return;
118
+
119
+ let indexPath = project.path("index.html");
120
+ let indexHtml = await readFile(indexPath, "utf-8");
121
+
122
+ await writeFile(indexPath, indexHtml.replace("./app/boot.ts", "./app/boot.js"));
123
+ }
124
+
125
+ /**
126
+ * Bumps in-range only.
127
+ * Majors will need to go through PR to this repo.
128
+ *
129
+ * @param {import('#utils/project.js').Project} project
130
+ */
131
+ async function upgradeDependencies(project) {
132
+ let existing = await packageJson.read(project.directory);
133
+
134
+ await packageJson.modify(async (json) => {
135
+ if (json.dependencies) {
136
+ Object.assign(json.dependencies, await getLatest(existing.dependencies));
137
+ }
138
+ if (json.devDependencies) {
139
+ Object.assign(json.devDependencies, await getLatest(existing.devDependencies));
140
+ }
141
+ }, project.directory);
142
+ }
@@ -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, pinYukuParser } 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,23 +14,7 @@ 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
- await pinYukuParser(project);
33
-
34
18
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
35
19
  expect(install.exitCode).toBe(0);
36
20
 
@@ -68,11 +52,11 @@ describe("base: minimal-library", () => {
68
52
  ]
69
53
  `);
70
54
 
71
- expect(await read(project, "src/index.js")).toBe("");
55
+ expect(await project.read("src/index.js")).toBe("");
72
56
  });
73
57
 
74
58
  it("is publishable", async () => {
75
- let manifest = JSON.parse(await read(project, "package.json"));
59
+ let manifest = JSON.parse((await project.read("package.json"))!);
76
60
 
77
61
  // The template is private (it lives in a workspace); the generated
78
62
  // library must not be
@@ -80,14 +64,14 @@ describe("base: minimal-library", () => {
80
64
  });
81
65
 
82
66
  it("has no TypeScript leftovers", async () => {
83
- let manifest = JSON.parse(await read(project, "package.json"));
67
+ let manifest = JSON.parse((await project.read("package.json"))!);
84
68
 
85
69
  expect(manifest.devDependencies).not.toHaveProperty("typescript");
86
70
  expect(manifest.devDependencies).not.toHaveProperty("@ember/library-tsconfig");
87
71
  // No declarations are emitted without types
88
72
  expect(JSON.stringify(manifest.exports)).not.toContain("types");
89
73
 
90
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
74
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
91
75
  "import { defineConfig } from "tsdown";
92
76
  import { ember } from "@nullvoxpopuli/ember-rolldown";
93
77
 
@@ -131,7 +115,7 @@ describe("base: minimal-library", () => {
131
115
  await writeLibrarySource(project, "javascript");
132
116
  await installAndBuild(project);
133
117
 
134
- let output = await read(project, "dist/index.js");
118
+ let output = await project.read("dist/index.js");
135
119
 
136
120
  // Published libraries ship precompileTemplate (the consuming app does
137
121
  // the final compile), never wire format
@@ -167,12 +151,11 @@ describe("base: minimal-library", () => {
167
151
  ]
168
152
  `);
169
153
 
170
- expect(await read(project, "src/index.ts")).toBe("");
154
+ expect(await project.read("src/index.ts")).toBe("");
171
155
  });
172
156
 
173
157
  it("type checks", async () => {
174
158
  await writeLibrarySource(project, "typescript");
175
- await pinYukuParser(project);
176
159
 
177
160
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
178
161
  expect(install.exitCode).toBe(0);
@@ -184,16 +167,40 @@ describe("base: minimal-library", () => {
184
167
  it("builds, including declarations", async () => {
185
168
  await installAndBuild(project);
186
169
 
187
- let output = await read(project, "dist/index.js");
170
+ let output = await project.read("dist/index.js");
188
171
 
189
172
  expect(output).toContain("precompileTemplate");
190
173
  expect(output).not.toContain("createTemplateFactory");
191
174
 
192
- let declarations = await read(project, "dist/index.d.ts");
193
-
194
- expect(declarations).toContain("declare class Greeting extends Component<GreetingSignature>");
195
- expect(declarations).toContain("declare const Badge: TOC<BadgeSignature>");
196
- 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
+ `);
197
204
 
198
205
  expect(await listFiles(join(project.directory, "dist"))).toMatchInlineSnapshot(`
199
206
  [
@@ -1,8 +1,7 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
- import { generate, pinYukuParser } from "#test-helpers";
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,7 +35,7 @@ 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(`
38
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
44
39
  "import { defineConfig } from "tsdown";
45
40
  import { ember } from "@nullvoxpopuli/ember-rolldown";
46
41
  export default defineConfig({ entry: ["./src/index.ts"], plugins: [ember()], attw: { profile: "esm-only" }, publint: true })"
@@ -48,8 +43,6 @@ describe("layers: publint + are-the-types-wrong", () => {
48
43
  });
49
44
 
50
45
  it("both checks run and pass in the build", async () => {
51
- await pinYukuParser(project);
52
-
53
46
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
54
47
  expect(install.exitCode).toBe(0);
55
48
 
@@ -80,7 +73,7 @@ describe("layers: publint + are-the-types-wrong", () => {
80
73
  });
81
74
 
82
75
  it("wires publint only (no declarations to check)", async () => {
83
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
76
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
84
77
  "import { defineConfig } from "tsdown";
85
78
  import { ember } from "@nullvoxpopuli/ember-rolldown";
86
79
  export default defineConfig({ entry: ["./src/index.js"], dts: false, plugins: [ember()], publint: true })"
@@ -88,8 +81,6 @@ describe("layers: publint + are-the-types-wrong", () => {
88
81
  });
89
82
 
90
83
  it("publint runs and passes in the build", async () => {
91
- await pinYukuParser(project);
92
-
93
84
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
94
85
  expect(install.exitCode).toBe(0);
95
86
 
@@ -1,30 +1,13 @@
1
1
  import { describe, it, beforeAll, afterAll, expect } from "vitest";
2
- import { generate, pinYukuParser } 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
- await pinYukuParser(project);
27
-
28
11
  let install = await execa("pnpm install", { cwd: project.directory, shell: true });
29
12
  expect(install.exitCode).toBe(0);
30
13
 
@@ -169,7 +152,7 @@ describe("layer: qunit", () => {
169
152
  });
170
153
 
171
154
  it("generates the expected files, without any index.html", async () => {
172
- let files = await listFiles(project.directory);
155
+ let files = await listFiles(project.directory, { includeDist: false });
173
156
 
174
157
  expect(files.some((file) => file.endsWith("index.html"))).toBe(false);
175
158
  expect(files).toMatchInlineSnapshot(`
@@ -191,7 +174,7 @@ describe("layer: qunit", () => {
191
174
  });
192
175
 
193
176
  it("generates the test build config", async () => {
194
- expect(await read(project, "vite.config.mjs")).toMatchInlineSnapshot(`
177
+ expect(await project.read("vite.config.mjs")).toMatchInlineSnapshot(`
195
178
  "import { defineConfig } from "vite";
196
179
  import { ember } from "@nullvoxpopuli/ember-vite";
197
180
 
@@ -200,7 +183,7 @@ describe("layer: qunit", () => {
200
183
  });
201
184
  "
202
185
  `);
203
- expect(await read(project, "config/test/babel.config.js")).toMatchInlineSnapshot(`
186
+ expect(await project.read("config/test/babel.config.js")).toMatchInlineSnapshot(`
204
187
  "import { buildMacros } from "@embroider/macros/babel";
205
188
 
206
189
  const macros = buildMacros({
@@ -244,7 +227,7 @@ describe("layer: qunit", () => {
244
227
  };
245
228
  "
246
229
  `);
247
- expect(await read(project, "config/test/testem.cjs")).toMatchInlineSnapshot(`
230
+ expect(await project.read("config/test/testem.cjs")).toMatchInlineSnapshot(`
248
231
  ""use strict";
249
232
 
250
233
  if (typeof module !== "undefined") {
@@ -276,7 +259,7 @@ describe("layer: qunit", () => {
276
259
  });
277
260
 
278
261
  it("leaves the publish build config alone", async () => {
279
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
262
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
280
263
  "import { defineConfig } from "tsdown";
281
264
  import { ember } from "@nullvoxpopuli/ember-rolldown";
282
265
 
@@ -298,7 +281,7 @@ describe("layer: qunit", () => {
298
281
  let build = await execa("pnpm build", { cwd: project.directory, shell: true });
299
282
  expect(build.exitCode).toBe(0);
300
283
 
301
- let output = await read(project, "dist/index.js");
284
+ let output = await project.read("dist/index.js");
302
285
 
303
286
  expect(output).toContain("precompileTemplate");
304
287
  expect(output).not.toContain("createTemplateFactory");
@@ -314,7 +297,7 @@ describe("layer: qunit", () => {
314
297
  });
315
298
 
316
299
  it("generates the expected files, without any index.html", async () => {
317
- let files = await listFiles(project.directory);
300
+ let files = await listFiles(project.directory, { includeDist: false });
318
301
 
319
302
  expect(files.some((file) => file.endsWith("index.html"))).toBe(false);
320
303
  expect(files).toMatchInlineSnapshot(`
@@ -335,7 +318,7 @@ describe("layer: qunit", () => {
335
318
  });
336
319
 
337
320
  it("has no TypeScript leftovers in the test build config", async () => {
338
- expect(await read(project, "config/test/babel.config.js")).toMatchInlineSnapshot(`
321
+ expect(await project.read("config/test/babel.config.js")).toMatchInlineSnapshot(`
339
322
  "import { buildMacros } from "@embroider/macros/babel";
340
323
 
341
324
  const macros = buildMacros({
@@ -367,7 +350,7 @@ describe("layer: qunit", () => {
367
350
  };
368
351
  "
369
352
  `);
370
- expect(await read(project, "tsdown.config.js")).toMatchInlineSnapshot(`
353
+ expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
371
354
  "import { defineConfig } from "tsdown";
372
355
  import { ember } from "@nullvoxpopuli/ember-rolldown";
373
356
 
@@ -390,7 +373,7 @@ describe("layer: qunit", () => {
390
373
  let build = await execa("pnpm build", { cwd: project.directory, shell: true });
391
374
  expect(build.exitCode).toBe(0);
392
375
 
393
- let output = await read(project, "dist/index.js");
376
+ let output = await project.read("dist/index.js");
394
377
 
395
378
  expect(output).toContain("precompileTemplate");
396
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" });