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.
- package/.github/workflows/ci.yml +6 -0
- package/.release-plan.json +7 -59
- package/CHANGELOG.md +46 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +68 -1
- package/packages/rolldown/index.ts +8 -4
- package/packages/rolldown/package.json +13 -4
- package/packages/rolldown/rolldown.config.js +5 -3
- package/packages/rolldown/src/app-reexports.test.ts +220 -0
- package/packages/rolldown/src/app-reexports.ts +152 -0
- package/packages/rolldown/src/config.ts +50 -0
- package/packages/rolldown/src/dts.test.ts +166 -0
- package/packages/rolldown/src/transform.test.ts +173 -0
- package/src/bases/minimal-extension/extension.test.ts +251 -0
- package/src/bases/minimal-extension/files/.env.development +8 -0
- package/src/bases/minimal-extension/files/README.md +36 -0
- package/src/bases/minimal-extension/files/app/app.ts +21 -0
- package/src/bases/minimal-extension/files/app/boot.ts +8 -0
- package/src/bases/minimal-extension/files/app/config.ts +22 -0
- package/src/bases/minimal-extension/files/app/router.ts +10 -0
- package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
- package/src/bases/minimal-extension/files/babel.config.js +49 -0
- package/src/bases/minimal-extension/files/index.html +17 -0
- package/src/bases/minimal-extension/files/package.json +58 -0
- package/src/bases/minimal-extension/files/public/manifest.json +9 -0
- package/src/bases/minimal-extension/files/tsconfig.json +13 -0
- package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
- package/src/bases/minimal-extension/index.js +142 -0
- package/src/bases/minimal-library/files/tsdown.config.js +2 -1
- package/src/cli/generator.js +4 -0
- package/src/cli/questions/project-type.js +9 -2
- package/src/layers/renovate/index.js +4 -3
- package/src/layers/typescript/index.js +6 -0
- package/src/utils/types.ts +1 -1
- package/test/ember.nvp/base-library.test.ts +41 -30
- package/test/ember.nvp/layer-publish-checks.test.ts +7 -10
- package/test/ember.nvp/layer-qunit.test.ts +17 -30
- package/test/ember.nvp/layer-vitest.test.ts +4 -18
- package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +68 -19
- package/test/test-helpers.ts +25 -5
- package/vitest.config.js +1 -1
- package/packages/rolldown/src/define-config.ts +0 -32
package/src/cli/generator.js
CHANGED
|
@@ -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 === "
|
|
19
|
-
? join(import.meta.dirname, "files/
|
|
20
|
-
: join(import.meta.dirname, "files/
|
|
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
|
}
|
package/src/utils/types.ts
CHANGED
|
@@ -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,
|
|
6
|
-
import { join
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
89
|
-
"import { defineConfig
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
189
|
-
|
|
190
|
-
expect(declarations).
|
|
191
|
-
|
|
192
|
-
|
|
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 {
|
|
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(
|
|
44
|
-
"import { defineConfig
|
|
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(
|
|
81
|
-
"import { defineConfig
|
|
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,
|
|
6
|
-
import { dirname, join
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
278
|
-
"import { defineConfig
|
|
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(
|
|
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(
|
|
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(
|
|
368
|
-
"import { defineConfig
|
|
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(
|
|
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,
|
|
6
|
-
import { dirname, join
|
|
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(
|
|
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";
|
|
@@ -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
|
|
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
|
|
41
|
-
|
|
66
|
+
const toPermutations = (layerNames: string[], withBaseline: boolean) => {
|
|
67
|
+
const permutations = permutate(layerNames);
|
|
68
|
+
|
|
69
|
+
if (withBaseline) {
|
|
70
|
+
permutations.push([baseline]);
|
|
71
|
+
}
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
// Not implemented yet
|
|
80
|
+
if (permutation.some((x) => TODO.has(x))) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
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 %
|
|
129
|
+
const eachPermutation = allPermutations[group].filter((_, i) => i % total === index);
|
|
81
130
|
|
|
82
131
|
describe(base, () => {
|
|
83
132
|
describe.each(eachPermutation)("layers: $name", ({ permutation }) => {
|
package/test/test-helpers.ts
CHANGED
|
@@ -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?:
|
|
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
|
+
}
|