ember.nvp 1.0.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 (44) hide show
  1. package/.github/workflows/ci.yml +6 -0
  2. package/.release-plan.json +23 -15
  3. package/CHANGELOG.md +30 -0
  4. package/package.json +1 -1
  5. package/packages/rolldown/README.md +62 -0
  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/config.ts +1 -1
  11. package/packages/rolldown/src/dts.test.ts +166 -0
  12. package/packages/rolldown/src/transform.test.ts +173 -0
  13. package/src/bases/minimal-extension/extension.test.ts +251 -0
  14. package/src/bases/minimal-extension/files/.env.development +8 -0
  15. package/src/bases/minimal-extension/files/README.md +36 -0
  16. package/src/bases/minimal-extension/files/app/app.ts +21 -0
  17. package/src/bases/minimal-extension/files/app/boot.ts +8 -0
  18. package/src/bases/minimal-extension/files/app/config.ts +22 -0
  19. package/src/bases/minimal-extension/files/app/router.ts +10 -0
  20. package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
  21. package/src/bases/minimal-extension/files/babel.config.js +49 -0
  22. package/src/bases/minimal-extension/files/index.html +17 -0
  23. package/src/bases/minimal-extension/files/package.json +58 -0
  24. package/src/bases/minimal-extension/files/public/manifest.json +9 -0
  25. package/src/bases/minimal-extension/files/tsconfig.json +13 -0
  26. package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
  27. package/src/bases/minimal-extension/index.js +142 -0
  28. package/src/cli/generator.js +4 -0
  29. package/src/cli/questions/project-type.js +9 -2
  30. package/src/layers/renovate/index.js +4 -3
  31. package/src/layers/typescript/index.js +6 -0
  32. package/src/utils/types.ts +1 -1
  33. package/test/ember.nvp/base-library.test.ts +39 -32
  34. package/test/ember.nvp/layer-publish-checks.test.ts +4 -13
  35. package/test/ember.nvp/layer-qunit.test.ts +13 -30
  36. package/test/ember.nvp/layer-vitest.test.ts +4 -18
  37. package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
  38. package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
  39. package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
  40. package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
  41. package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
  42. package/test/ember.nvp/permutations/shared.ts +68 -19
  43. package/test/test-helpers.ts +25 -30
  44. package/vitest.config.js +1 -1
@@ -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" });
@@ -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, readFile, writeFile } 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
 
@@ -67,31 +68,6 @@ export async function mktemp(name = "ember.nvp_test-") {
67
68
  return await mkdtemp(join(tmpdir(), `${name}-`));
68
69
  }
69
70
 
70
- /**
71
- * Temporary: `rolldown-plugin-dts` (used by tsdown for `.d.ts` generation)
72
- * imports `walk` from `yuku-parser`, but yuku-parser >= 0.6.6 moved `walk` to
73
- * `yuku-ast`. Its dependency range still floats to the broken versions, so a
74
- * fresh install of a generated library crashes when tsdown builds.
75
- *
76
- * The yuku toolchain publishes in lockstep on a shared `@yuku-toolchain/types`,
77
- * so `yuku-parser` and `yuku-codegen` must be pinned together (pinning only one
78
- * splits the toolchain and the dts/publint pipeline silently bails). Pin the
79
- * last working set before install -- here in the test harness only, so the
80
- * generated blueprint stays override-free. Remove once the plugin is fixed:
81
- * https://github.com/sxzz/rolldown-plugin-dts/issues/277
82
- */
83
- export async function pinYukuParser(project: Project): Promise<void> {
84
- const manifestPath = join(project.directory, "package.json");
85
- const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
86
-
87
- manifest.pnpm ??= {};
88
- manifest.pnpm.overrides ??= {};
89
- manifest.pnpm.overrides["yuku-parser"] = "0.6.5";
90
- manifest.pnpm.overrides["yuku-codegen"] = "0.6.5";
91
-
92
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
93
- }
94
-
95
71
  export async function reapply(project: Project, layers: string[]) {
96
72
  return await generate({
97
73
  directory: project.directory,
@@ -112,7 +88,7 @@ export async function generate({
112
88
  directory?: string;
113
89
  layers?: string[];
114
90
  name?: string;
115
- type?: "app" | "library";
91
+ type?: ProjectType;
116
92
  packageManager?: "pnpm" | "npm";
117
93
  }): Promise<Project> {
118
94
  const tempDir = directory ?? (await mktemp(name));
@@ -211,3 +187,22 @@ export async function expectIsSetup(project: Project, layer: DiscoveredLayer) {
211
187
 
212
188
  expect(result, `${layer.name} is setup`).toBe(true);
213
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
  });