ember.nvp 1.6.0 → 1.7.1
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 +5 -1
- package/.release-plan.json +51 -9
- package/CHANGELOG.md +30 -0
- package/README.md +41 -28
- package/RELEASE.md +16 -14
- package/package.json +1 -1
- package/packages/build-tooling-utils/README.md +7 -12
- package/packages/build-tooling-utils/package.json +1 -1
- package/packages/build-tooling-utils/src/extensions.ts +4 -3
- package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
- package/packages/rolldown/README.md +138 -98
- package/packages/rolldown/index.ts +66 -28
- package/packages/rolldown/package.json +1 -1
- package/packages/rolldown/src/app-reexports.test.ts +4 -3
- package/packages/rolldown/src/app-reexports.ts +37 -27
- package/packages/rolldown/src/babel.test.ts +10 -5
- package/packages/rolldown/src/babel.ts +117 -72
- package/packages/rolldown/src/bundle.test.ts +149 -0
- package/packages/rolldown/src/bundle.ts +71 -0
- package/packages/rolldown/src/config.ts +42 -18
- package/packages/rolldown/src/dts.test.ts +42 -26
- package/packages/rolldown/src/externals.test.ts +16 -11
- package/packages/rolldown/src/externals.ts +45 -28
- package/packages/rolldown/src/isolated-declarations.ts +56 -43
- package/packages/rolldown/src/plugin-like.ts +18 -15
- package/packages/rolldown/src/published-types.test.ts +18 -13
- package/packages/rolldown/src/transform.test.ts +26 -17
- package/packages/rolldown/src/transform.ts +68 -48
- package/packages/vite/README.md +10 -11
- package/packages/vite/package.json +1 -1
- package/packages/vite/src/ember.ts +44 -36
- package/packages/vite/src/test-html.ts +13 -11
- package/src/bases/README.md +6 -3
- package/src/bases/minimal-app/app.test.ts +7 -4
- package/src/bases/minimal-app/files/app/app.ts +2 -3
- package/src/bases/minimal-app/files/app/router.ts +3 -3
- package/src/bases/minimal-app/index.js +10 -6
- package/src/bases/minimal-custom-element/files/README.md +95 -0
- package/src/bases/minimal-custom-element/files/package.json +47 -0
- package/src/bases/minimal-custom-element/files/src/components/counter.gts +27 -0
- package/src/bases/minimal-custom-element/files/src/element.ts +45 -0
- package/src/bases/minimal-custom-element/files/src/index.ts +2 -0
- package/src/bases/minimal-custom-element/files/src/register.ts +5 -0
- package/src/bases/minimal-custom-element/files/tsconfig.json +10 -0
- package/src/bases/minimal-custom-element/files/tsdown.config.js +7 -0
- package/src/bases/minimal-custom-element/index.js +212 -0
- package/src/bases/minimal-extension/files/README.md +13 -5
- package/src/bases/minimal-extension/files/app/app.ts +2 -3
- package/src/bases/minimal-extension/files/app/config.ts +5 -3
- package/src/bases/minimal-extension/index.js +16 -11
- package/src/bases/minimal-library/files/README.md +8 -7
- package/src/bases/minimal-library/index.js +21 -15
- package/src/cli/args.js +1 -1
- package/src/cli/generator.js +4 -0
- package/src/cli/index.js +23 -18
- package/src/cli/questions/project-type.js +17 -2
- package/src/cli/questions/write.js +12 -10
- package/src/layers/are-the-types-wrong/index.js +6 -6
- package/src/layers/github-actions/index.js +7 -4
- package/src/layers/inspector-support/index.js +2 -2
- package/src/layers/publint/index.js +2 -2
- package/src/layers/qunit/index.js +21 -17
- package/src/layers/readme/index.js +55 -4
- package/src/layers/renovate/index.js +5 -5
- package/src/layers/typescript/index.js +20 -13
- package/src/layers/vitest/index.js +8 -4
- package/src/utils/babel.js +3 -2
- package/src/utils/buffer.js +2 -2
- package/src/utils/cli.js +4 -4
- package/src/utils/fs.js +23 -14
- package/src/utils/inspector-support.js +22 -19
- package/src/utils/npm.js +15 -12
- package/src/utils/project-type.js +19 -0
- package/src/utils/project.js +14 -2
- package/src/utils/remove-types.js +2 -4
- package/src/utils/rewrite-imports.js +43 -31
- package/src/utils/stage.js +44 -33
- package/src/utils/tsdown-config.js +6 -5
- package/src/utils/types.ts +10 -9
- package/test/ember.nvp/base-custom-element.test.ts +440 -0
- package/test/ember.nvp/base-library.test.ts +6 -4
- package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
- package/test/ember.nvp/inspector-support.test.ts +4 -3
- package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
- package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
- package/test/ember.nvp/layer-qunit.test.ts +2 -2
- package/test/ember.nvp/layer-readme.test.ts +63 -3
- package/test/ember.nvp/layer-vitest.test.ts +16 -9
- package/test/ember.nvp/layers-load.test.ts +26 -0
- package/test/ember.nvp/library-src-fixtures.ts +7 -3
- package/test/ember.nvp/permutations/custom-element-1.test.ts +3 -0
- package/test/ember.nvp/permutations/custom-element-2.test.ts +3 -0
- package/test/ember.nvp/permutations/custom-element-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +22 -12
- package/test/ember.nvp/publish-configs.test.ts +44 -31
- package/test/ember.nvp/rewrite-imports.test.ts +4 -3
- package/test/ember.nvp/scoped-css-library.test.ts +6 -4
- package/test/test-helpers/permutate.test.ts +3 -3
- package/test/test-helpers.ts +12 -7
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { describe, it, beforeAll, afterAll, expect } from "vitest";
|
|
2
|
+
import { generate, listFiles } from "#test-helpers";
|
|
3
|
+
import { tagNameFor } from "#bases/minimal-custom-element";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
|
|
8
|
+
import type { Project } from "ember.nvp";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The minimal-custom-element base ships a working pattern:
|
|
12
|
+
* a counter component wrapped in a custom element.
|
|
13
|
+
*
|
|
14
|
+
* So unlike the library base, there is no example source to write in.
|
|
15
|
+
* These tests:
|
|
16
|
+
* - build the generated project as-is
|
|
17
|
+
* - run the element in a real browser, through the vitest layer
|
|
18
|
+
* (from source, and from the built dist)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
async function install(project: Project) {
|
|
22
|
+
let result = await execa("pnpm install", { cwd: project.directory, shell: true });
|
|
23
|
+
expect(result.exitCode).toBe(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function build(project: Project) {
|
|
27
|
+
let result = await execa("pnpm build", { cwd: project.directory, shell: true });
|
|
28
|
+
expect(result.exitCode).toBe(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function emit(project: Project, files: Record<string, string>) {
|
|
32
|
+
for (let [path, contents] of Object.entries(files)) {
|
|
33
|
+
let filePath = join(project.directory, path);
|
|
34
|
+
|
|
35
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
36
|
+
await writeFile(filePath, contents);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The shared chunk carries a content hash,
|
|
42
|
+
* which would change with every dependency bump.
|
|
43
|
+
*
|
|
44
|
+
* Stable names only.
|
|
45
|
+
*/
|
|
46
|
+
async function listDist(project: Project) {
|
|
47
|
+
let files = await listFiles(join(project.directory, "dist"));
|
|
48
|
+
|
|
49
|
+
return files.map((file) => file.replace(/^src-[\w-]+\.js/, "src-[hash].js"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The bundled chunk's own imports are hoisted above its first region marker.
|
|
54
|
+
*
|
|
55
|
+
* ember-source's doc comments below it also start lines with `import`,
|
|
56
|
+
* so only that head is inspected.
|
|
57
|
+
*/
|
|
58
|
+
async function chunkImports(project: Project) {
|
|
59
|
+
let files = await listFiles(join(project.directory, "dist"));
|
|
60
|
+
let chunk = files.find((file) => /^src-[\w-]+\.js$/.test(file));
|
|
61
|
+
|
|
62
|
+
expect(chunk).toBeDefined();
|
|
63
|
+
|
|
64
|
+
let code = (await project.read(`dist/${chunk}`)) ?? "";
|
|
65
|
+
let head = code.split("//#region")[0] ?? "";
|
|
66
|
+
|
|
67
|
+
return head.match(/^import .*$/gm) ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Drives the generated element the way a consumer would:
|
|
72
|
+
* - import the register entry
|
|
73
|
+
* - put the tag on the page
|
|
74
|
+
* - click
|
|
75
|
+
* - change attributes
|
|
76
|
+
*
|
|
77
|
+
* `from` is the module that provides the element:
|
|
78
|
+
* - `src`: the source
|
|
79
|
+
* - `dist`: the self-contained output, after a build
|
|
80
|
+
*/
|
|
81
|
+
function elementTests(ext: "ts" | "js", tagName: string, from: "src" | "dist" = "src") {
|
|
82
|
+
let register = from === "src" ? `../src/register.${ext}` : "../dist/register.js";
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
[`tests/${from}-element-test.${ext}`]: `import { describe, test, expect, afterEach } from "vitest";
|
|
86
|
+
import { renderSettled } from "@ember/renderer";
|
|
87
|
+
|
|
88
|
+
import "${register}";
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
document.body.innerHTML = "";
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function mount(attributes${ext === "ts" ? ": Record<string, string>" : ""} = {}) {
|
|
95
|
+
let element = document.createElement("${tagName}");
|
|
96
|
+
|
|
97
|
+
for (let [name, value] of Object.entries(attributes)) {
|
|
98
|
+
element.setAttribute(name, value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
document.body.appendChild(element);
|
|
102
|
+
|
|
103
|
+
return element;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("<${tagName}>", () => {
|
|
107
|
+
test("renders the counter with default args", async () => {
|
|
108
|
+
let element = mount();
|
|
109
|
+
|
|
110
|
+
await renderSettled();
|
|
111
|
+
|
|
112
|
+
expect(element.querySelector(".label")?.textContent).toBe("Count");
|
|
113
|
+
expect(element.querySelector("output")?.textContent).toBe("0");
|
|
114
|
+
expect(element.querySelector("button")?.textContent).toBe("+1");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("counts clicks", async () => {
|
|
118
|
+
let element = mount();
|
|
119
|
+
|
|
120
|
+
await renderSettled();
|
|
121
|
+
|
|
122
|
+
element.querySelector("button")?.click();
|
|
123
|
+
await renderSettled();
|
|
124
|
+
element.querySelector("button")?.click();
|
|
125
|
+
await renderSettled();
|
|
126
|
+
|
|
127
|
+
expect(element.querySelector("output")?.textContent).toBe("2");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("passes initial attributes as args", async () => {
|
|
131
|
+
let element = mount({ label: "Clicks", step: "5" });
|
|
132
|
+
|
|
133
|
+
await renderSettled();
|
|
134
|
+
|
|
135
|
+
expect(element.querySelector(".label")?.textContent).toBe("Clicks");
|
|
136
|
+
expect(element.querySelector("button")?.textContent).toBe("+5");
|
|
137
|
+
|
|
138
|
+
element.querySelector("button")?.click();
|
|
139
|
+
await renderSettled();
|
|
140
|
+
|
|
141
|
+
expect(element.querySelector("output")?.textContent).toBe("5");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("re-renders when attributes change", async () => {
|
|
145
|
+
let element = mount();
|
|
146
|
+
|
|
147
|
+
await renderSettled();
|
|
148
|
+
|
|
149
|
+
element.setAttribute("label", "Taps");
|
|
150
|
+
element.setAttribute("step", "3");
|
|
151
|
+
await renderSettled();
|
|
152
|
+
|
|
153
|
+
expect(element.querySelector(".label")?.textContent).toBe("Taps");
|
|
154
|
+
expect(element.querySelector("button")?.textContent).toBe("+3");
|
|
155
|
+
|
|
156
|
+
element.querySelector("button")?.click();
|
|
157
|
+
await renderSettled();
|
|
158
|
+
|
|
159
|
+
expect(element.querySelector("output")?.textContent).toBe("3");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("renders again after being moved", async () => {
|
|
163
|
+
let element = mount();
|
|
164
|
+
|
|
165
|
+
await renderSettled();
|
|
166
|
+
|
|
167
|
+
element.querySelector("button")?.click();
|
|
168
|
+
await renderSettled();
|
|
169
|
+
|
|
170
|
+
element.remove();
|
|
171
|
+
document.body.appendChild(element);
|
|
172
|
+
await renderSettled();
|
|
173
|
+
|
|
174
|
+
expect(element.querySelector("output")?.textContent).toBe("0");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
describe("base: minimal-custom-element", () => {
|
|
182
|
+
const dirs: string[] = [];
|
|
183
|
+
|
|
184
|
+
afterAll(async () => {
|
|
185
|
+
if (process.env.CI) return;
|
|
186
|
+
|
|
187
|
+
for (const dir of dirs) {
|
|
188
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("tag name", () => {
|
|
193
|
+
it("is the package name", () => {
|
|
194
|
+
expect(tagNameFor("my-counter")).toBe("my-counter");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("drops the scope", () => {
|
|
198
|
+
expect(tagNameFor("@acme/my-counter")).toBe("my-counter");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("gets a suffix when the name has no hyphen", () => {
|
|
202
|
+
expect(tagNameFor("counter")).toBe("counter-element");
|
|
203
|
+
expect(tagNameFor("@acme/counter")).toBe("counter-element");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("JavaScript", () => {
|
|
208
|
+
let project: Project;
|
|
209
|
+
|
|
210
|
+
beforeAll(async () => {
|
|
211
|
+
project = await generate({ type: "custom-element", name: "my-counter", layers: [] });
|
|
212
|
+
dirs.push(project.directory);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("generates the expected files", async () => {
|
|
216
|
+
expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
|
|
217
|
+
[
|
|
218
|
+
".gitignore",
|
|
219
|
+
"README.md",
|
|
220
|
+
"package.json",
|
|
221
|
+
"src/components/counter.gjs",
|
|
222
|
+
"src/element.js",
|
|
223
|
+
"src/index.js",
|
|
224
|
+
"src/register.js",
|
|
225
|
+
"tsdown.config.js",
|
|
226
|
+
]
|
|
227
|
+
`);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("registers the tag under the project name", async () => {
|
|
231
|
+
expect(await project.read("src/register.js")).toMatchInlineSnapshot(`
|
|
232
|
+
"import { CounterElement } from "./index.js";
|
|
233
|
+
if (!customElements.get('my-counter')) {
|
|
234
|
+
customElements.define('my-counter', CounterElement);
|
|
235
|
+
}"
|
|
236
|
+
`);
|
|
237
|
+
expect(await project.read("README.md")).toContain("<my-counter label=");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("exports the element and the component", async () => {
|
|
241
|
+
expect(await project.read("src/index.js")).toMatchInlineSnapshot(`
|
|
242
|
+
"export { CounterElement } from "./element.js";
|
|
243
|
+
export { default as Counter } from "./components/counter.gjs";"
|
|
244
|
+
`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("is publishable", async () => {
|
|
248
|
+
let manifest = JSON.parse((await project.read("package.json"))!);
|
|
249
|
+
|
|
250
|
+
expect(manifest).not.toHaveProperty("private");
|
|
251
|
+
expect(manifest.exports["./register"]).toEqual({ default: "./dist/register.js" });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("has no TypeScript leftovers", async () => {
|
|
255
|
+
let manifest = JSON.parse((await project.read("package.json"))!);
|
|
256
|
+
|
|
257
|
+
expect(manifest.devDependencies).not.toHaveProperty("typescript");
|
|
258
|
+
expect(manifest.devDependencies).not.toHaveProperty("@ember/library-tsconfig");
|
|
259
|
+
expect(JSON.stringify(manifest.exports)).not.toContain("types");
|
|
260
|
+
|
|
261
|
+
expect(await project.read("tsdown.config.js")).toMatchInlineSnapshot(`
|
|
262
|
+
"import { defineConfig } from "tsdown";
|
|
263
|
+
import { ember } from "@nullvoxpopuli/ember-rolldown";
|
|
264
|
+
|
|
265
|
+
export default defineConfig({
|
|
266
|
+
entry: ["./src/index.js", "./src/register.js"],
|
|
267
|
+
dts: false,
|
|
268
|
+
plugins: [ember({ bundle: true })],
|
|
269
|
+
});
|
|
270
|
+
"
|
|
271
|
+
`);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("builds a self-contained package", async () => {
|
|
275
|
+
await install(project);
|
|
276
|
+
await build(project);
|
|
277
|
+
|
|
278
|
+
expect(await listDist(project)).toMatchInlineSnapshot(`
|
|
279
|
+
[
|
|
280
|
+
"index.js",
|
|
281
|
+
"register.js",
|
|
282
|
+
"register.js.map",
|
|
283
|
+
"src-[hash].js",
|
|
284
|
+
"src-[hash].js.map",
|
|
285
|
+
]
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
let register = await project.read("dist/register.js");
|
|
289
|
+
|
|
290
|
+
expect(register).toContain('customElements.define("my-counter"');
|
|
291
|
+
// Both entries share one chunk holding the element, the component,
|
|
292
|
+
// and ember itself
|
|
293
|
+
expect(register).toMatch(/from "\.\/src-[\w-]+\.js"/);
|
|
294
|
+
expect(await project.read("dist/index.js")).toMatch(/from "\.\/src-[\w-]+\.js"/);
|
|
295
|
+
|
|
296
|
+
// Nothing is left for a consumer to resolve
|
|
297
|
+
expect(await chunkImports(project)).toEqual([]);
|
|
298
|
+
|
|
299
|
+
let files = await listFiles(join(project.directory, "dist"));
|
|
300
|
+
let chunk = (await project.read(`dist/${files.find((file) => file.startsWith("src-"))}`))!;
|
|
301
|
+
|
|
302
|
+
// The template ships compiled for the bundled ember-source, so the
|
|
303
|
+
// page needs no template compiler
|
|
304
|
+
expect(chunk).not.toContain("precompileTemplate(");
|
|
305
|
+
expect(chunk).toContain('"block":');
|
|
306
|
+
expect(chunk).toContain('observedAttributes = ["label", "step"]');
|
|
307
|
+
expect(chunk).toContain("renderComponent");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("TypeScript", () => {
|
|
312
|
+
let project: Project;
|
|
313
|
+
|
|
314
|
+
beforeAll(async () => {
|
|
315
|
+
project = await generate({ type: "custom-element", name: "counter", layers: ["typescript"] });
|
|
316
|
+
dirs.push(project.directory);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("generates the expected files", async () => {
|
|
320
|
+
expect(await listFiles(project.directory)).toMatchInlineSnapshot(`
|
|
321
|
+
[
|
|
322
|
+
".gitignore",
|
|
323
|
+
"README.md",
|
|
324
|
+
"package.json",
|
|
325
|
+
"src/components/counter.gts",
|
|
326
|
+
"src/element.ts",
|
|
327
|
+
"src/index.ts",
|
|
328
|
+
"src/register.ts",
|
|
329
|
+
"tsconfig.json",
|
|
330
|
+
"tsdown.config.js",
|
|
331
|
+
]
|
|
332
|
+
`);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("registers the tag with a suffix when the name has no hyphen", async () => {
|
|
336
|
+
expect(await project.read("src/register.ts")).toMatchInlineSnapshot(`
|
|
337
|
+
"import { CounterElement } from "./index.ts";
|
|
338
|
+
|
|
339
|
+
if (!customElements.get("counter-element")) {
|
|
340
|
+
customElements.define("counter-element", CounterElement);
|
|
341
|
+
}
|
|
342
|
+
"
|
|
343
|
+
`);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("type checks", async () => {
|
|
347
|
+
await install(project);
|
|
348
|
+
|
|
349
|
+
let types = await execa("pnpm lint:types", { cwd: project.directory, shell: true });
|
|
350
|
+
expect(types.exitCode).toBe(0);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("builds, including declarations", async () => {
|
|
354
|
+
await build(project);
|
|
355
|
+
|
|
356
|
+
expect(await listDist(project)).toMatchInlineSnapshot(`
|
|
357
|
+
[
|
|
358
|
+
"index.d.ts",
|
|
359
|
+
"index.d.ts.map",
|
|
360
|
+
"index.js",
|
|
361
|
+
"register.d.ts",
|
|
362
|
+
"register.js",
|
|
363
|
+
"register.js.map",
|
|
364
|
+
"src-[hash].js",
|
|
365
|
+
"src-[hash].js.map",
|
|
366
|
+
]
|
|
367
|
+
`);
|
|
368
|
+
|
|
369
|
+
expect(await chunkImports(project)).toEqual([]);
|
|
370
|
+
|
|
371
|
+
let declarations = (await project.read("dist/index.d.ts"))!;
|
|
372
|
+
|
|
373
|
+
expect(declarations).toContain("declare class CounterElement extends HTMLElement");
|
|
374
|
+
// The component's base class is bundled in, like the runtime.
|
|
375
|
+
// (ember-source's own types are ambient, so those stay by name)
|
|
376
|
+
expect(declarations).toContain("declare class Component<");
|
|
377
|
+
expect(declarations).not.toContain('from "@glimmer/component"');
|
|
378
|
+
expect(await project.read("dist/register.d.ts")).toMatchInlineSnapshot(`"export {}"`);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("in a browser (vitest layer)", () => {
|
|
383
|
+
describe("TypeScript", () => {
|
|
384
|
+
let project: Project;
|
|
385
|
+
|
|
386
|
+
beforeAll(async () => {
|
|
387
|
+
project = await generate({
|
|
388
|
+
type: "custom-element",
|
|
389
|
+
name: "my-counter",
|
|
390
|
+
layers: ["typescript", "vitest"],
|
|
391
|
+
});
|
|
392
|
+
dirs.push(project.directory);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("renders, counts, and reacts to attributes", { timeout: 300_000 }, async () => {
|
|
396
|
+
await emit(project, elementTests("ts", "my-counter"));
|
|
397
|
+
await install(project);
|
|
398
|
+
|
|
399
|
+
let test = await execa("pnpm test", { cwd: project.directory, shell: true });
|
|
400
|
+
expect(test.exitCode).toBe(0);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it(
|
|
404
|
+
"runs from the built package, with no ember on the page",
|
|
405
|
+
{ timeout: 300_000 },
|
|
406
|
+
async () => {
|
|
407
|
+
await build(project);
|
|
408
|
+
// Only the dist spec: the vitest run must not also load the source
|
|
409
|
+
// copy, or the tag would already be defined by whichever wins
|
|
410
|
+
await rm(join(project.directory, "tests"), { recursive: true, force: true });
|
|
411
|
+
await emit(project, elementTests("ts", "my-counter", "dist"));
|
|
412
|
+
|
|
413
|
+
let test = await execa("pnpm test", { cwd: project.directory, shell: true });
|
|
414
|
+
expect(test.exitCode).toBe(0);
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("JavaScript", () => {
|
|
420
|
+
let project: Project;
|
|
421
|
+
|
|
422
|
+
beforeAll(async () => {
|
|
423
|
+
project = await generate({
|
|
424
|
+
type: "custom-element",
|
|
425
|
+
name: "my-counter",
|
|
426
|
+
layers: ["vitest"],
|
|
427
|
+
});
|
|
428
|
+
dirs.push(project.directory);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("renders, counts, and reacts to attributes", { timeout: 300_000 }, async () => {
|
|
432
|
+
await emit(project, elementTests("js", "my-counter"));
|
|
433
|
+
await install(project);
|
|
434
|
+
|
|
435
|
+
let test = await execa("pnpm test", { cwd: project.directory, shell: true });
|
|
436
|
+
expect(test.exitCode).toBe(0);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
@@ -8,10 +8,12 @@ import { join } from "node:path";
|
|
|
8
8
|
import type { Project } from "ember.nvp";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* The minimal-library base has to produce a *buildable* library
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* The minimal-library base has to produce a *buildable* library.
|
|
12
|
+
*
|
|
13
|
+
* These tests:
|
|
14
|
+
* - generate both flavors (JavaScript and TypeScript)
|
|
15
|
+
* - snapshot the interesting generated files
|
|
16
|
+
* - run the real `pnpm build` (tsdown + rolldown) against them
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
async function installAndBuild(project: Project) {
|
|
@@ -7,10 +7,12 @@ import { join } from "node:path";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The replace-or-update question only matters when a project already exists
|
|
10
|
-
* at the target path.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* at the target path.
|
|
11
|
+
*
|
|
12
|
+
* These tests:
|
|
13
|
+
* - generate a project and assert the baseline state
|
|
14
|
+
* - regenerate over it, the way the CLI does once the user has answered
|
|
15
|
+
* - assert the resulting project is correct for each answer
|
|
14
16
|
*/
|
|
15
17
|
describe("generating over an existing project", () => {
|
|
16
18
|
const prettier = layers.find((layer) => layer.name === "prettier")!;
|
|
@@ -3,9 +3,10 @@ import { describe, it, expect } from "vitest";
|
|
|
3
3
|
import { hasInspectorSupport, wireInspectorSupport } from "#utils/inspector-support.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The Application definition is found structurally
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The Application definition is found structurally.
|
|
7
|
+
*
|
|
8
|
+
* So every shape an app author has refactored their app file into
|
|
9
|
+
* gets wired the same way the freshly generated shape does.
|
|
9
10
|
*/
|
|
10
11
|
describe("wireInspectorSupport", () => {
|
|
11
12
|
it("wires the generated shape (export default class extends Application)", () => {
|
|
@@ -43,11 +43,10 @@ describe("layer: inspector-support", () => {
|
|
|
43
43
|
"/**
|
|
44
44
|
* Looking for services that come from addons?
|
|
45
45
|
*
|
|
46
|
+
* App-tree merging from libraries is not supported.
|
|
46
47
|
* See: https://github.com/embroider-build/embroider/issues/2659
|
|
47
48
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* For services, I highly recommend looking in to either of
|
|
49
|
+
* For services, look in to either of:
|
|
51
50
|
* - https://github.com/chancancode/ember-polaris-service-
|
|
52
51
|
* - https://ember-primitives.pages.dev/6-utils/createService.md
|
|
53
52
|
* - https://ember-primitives.pages.dev/6-utils/createAsyncService.md
|
|
@@ -6,9 +6,8 @@ import { rm } from "node:fs/promises";
|
|
|
6
6
|
import type { Project } from "ember.nvp";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* publint and are-the-types-wrong run inside the library's own build
|
|
10
|
-
*
|
|
11
|
-
* output.
|
|
9
|
+
* publint and are-the-types-wrong run inside the library's own build (tsdown),
|
|
10
|
+
* so the real assertion is their report lines in `pnpm build` output.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
describe("layers: publint + are-the-types-wrong", () => {
|
|
@@ -16,8 +16,8 @@ async function installAndTest(project: Project) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The layer ships testing infrastructure only
|
|
20
|
-
* base's example exports are generated here, in both flavors.
|
|
19
|
+
* The layer ships testing infrastructure only.
|
|
20
|
+
* The tests exercising the base's example exports are generated here, in both flavors.
|
|
21
21
|
*/
|
|
22
22
|
const exampleTests = {
|
|
23
23
|
typescript: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { generate } from "#test-helpers";
|
|
2
|
+
import { generate, mktemp } from "#test-helpers";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import type { Project } from "ember.nvp";
|
|
5
5
|
|
|
@@ -168,13 +168,13 @@ describe("layer: readme", () => {
|
|
|
168
168
|
|
|
169
169
|
### Building
|
|
170
170
|
|
|
171
|
-
To build the
|
|
171
|
+
To build the extension:
|
|
172
172
|
|
|
173
173
|
\`\`\`sh
|
|
174
174
|
pnpm build
|
|
175
175
|
\`\`\`
|
|
176
176
|
|
|
177
|
-
or
|
|
177
|
+
or, to rebuild on change:
|
|
178
178
|
|
|
179
179
|
\`\`\`sh
|
|
180
180
|
pnpm build:watch
|
|
@@ -192,4 +192,64 @@ describe("layer: readme", () => {
|
|
|
192
192
|
`);
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
describe("Custom element project README generation", () => {
|
|
197
|
+
let project: Project;
|
|
198
|
+
|
|
199
|
+
beforeAll(async () => {
|
|
200
|
+
project = await generate({
|
|
201
|
+
directory: await mktemp("acme-counter"),
|
|
202
|
+
type: "custom-element",
|
|
203
|
+
name: "@acme/counter",
|
|
204
|
+
packageManager: "npm",
|
|
205
|
+
layers: ["readme"],
|
|
206
|
+
});
|
|
207
|
+
dirs.push(project.directory);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("generates README.md with the registered tag name", async () => {
|
|
211
|
+
expect(await project.read("README.md")).toMatchInlineSnapshot(`
|
|
212
|
+
"# @acme/counter
|
|
213
|
+
|
|
214
|
+
An Ember component, packaged as a custom element, created with \`ember.nvp\`.
|
|
215
|
+
The built package contains ember, so the page that uses the element needs nothing else.
|
|
216
|
+
|
|
217
|
+
## Usage
|
|
218
|
+
|
|
219
|
+
Import the \`register\` entry once. It defines the \`<counter-element>\` tag.
|
|
220
|
+
|
|
221
|
+
\`\`\`js
|
|
222
|
+
import "@acme/counter/register";
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
\`\`\`html
|
|
226
|
+
<counter-element label="Clicks" step="2"></counter-element>
|
|
227
|
+
\`\`\`
|
|
228
|
+
|
|
229
|
+
To choose your own tag name, import the class and define it yourself:
|
|
230
|
+
|
|
231
|
+
\`\`\`js
|
|
232
|
+
import { CounterElement } from "@acme/counter";
|
|
233
|
+
|
|
234
|
+
customElements.define("my-counter", CounterElement);
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
## Getting Started
|
|
238
|
+
|
|
239
|
+
### Prerequisites
|
|
240
|
+
|
|
241
|
+
- Node.js >= 24
|
|
242
|
+
- npm
|
|
243
|
+
|
|
244
|
+
### Development & Building
|
|
245
|
+
|
|
246
|
+
To build the package:
|
|
247
|
+
|
|
248
|
+
\`\`\`sh
|
|
249
|
+
npm run build
|
|
250
|
+
\`\`\`
|
|
251
|
+
"
|
|
252
|
+
`);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
195
255
|
});
|
|
@@ -9,9 +9,12 @@ import type { Project } from "ember.nvp";
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The vitest layer ships infrastructure only (config, deps, scripts).
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* These tests:
|
|
14
|
+
* - generate projects with the layer
|
|
15
|
+
* - emit specs that exercise each project's real exports
|
|
16
|
+
* - run the real `pnpm test` (vitest in headless-browser mode),
|
|
17
|
+
* and assert the specs run and pass
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
async function emit(project: Project, files: Record<string, string>) {
|
|
@@ -32,9 +35,11 @@ async function installAndTest(project: Project) {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
* Specs against the library base's real exports,
|
|
36
|
-
* the
|
|
37
|
-
*
|
|
38
|
+
* Specs against the library base's real exports,
|
|
39
|
+
* in the flavor matching the generated project.
|
|
40
|
+
*
|
|
41
|
+
* These files are emitted by this test, not shipped by the layer,
|
|
42
|
+
* so no automatic conversion applies.
|
|
38
43
|
*/
|
|
39
44
|
function libraryTests(ext: "ts" | "js") {
|
|
40
45
|
return {
|
|
@@ -82,9 +87,11 @@ describe("add", () => {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/**
|
|
85
|
-
* Specs against the app base's real modules
|
|
86
|
-
*
|
|
87
|
-
*
|
|
90
|
+
* Specs against the app base's real modules.
|
|
91
|
+
*
|
|
92
|
+
* Boots an app made from the app's own router class
|
|
93
|
+
* (pointed at a test-friendly location) and application template,
|
|
94
|
+
* then visits its application route.
|
|
88
95
|
*/
|
|
89
96
|
function appTests(ext: "ts" | "js") {
|
|
90
97
|
return {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const root = join(import.meta.dirname, "../..");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* vitest runs modules through its own loader,
|
|
9
|
+
* so it does not see import cycles the way Node does.
|
|
10
|
+
*
|
|
11
|
+
* `#layers` awaits a dynamic import of each layer at the top level.
|
|
12
|
+
* If a layer imports a module that imports `#layers` back,
|
|
13
|
+
* the top-level await never settles and Node exits with code 13.
|
|
14
|
+
*
|
|
15
|
+
* Node's loader is the only thing that catches that,
|
|
16
|
+
* so load the module in a child process.
|
|
17
|
+
*/
|
|
18
|
+
test("the layers module loads in a plain node process", async () => {
|
|
19
|
+
const result = await execa(process.execPath, ["src/layers/index.js"], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
reject: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(result.stderr).not.toContain("unsettled top-level await");
|
|
25
|
+
expect(result.exitCode).toBe(0);
|
|
26
|
+
});
|