ember.nvp 1.1.0 → 1.3.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/.release-plan.json +5 -21
- package/CHANGELOG.md +59 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +96 -9
- package/packages/rolldown/package.json +1 -1
- package/packages/rolldown/src/app-reexports.test.ts +35 -2
- package/packages/rolldown/src/app-reexports.ts +14 -3
- package/packages/rolldown/src/babel.test.ts +17 -0
- package/packages/rolldown/src/babel.ts +21 -3
- package/packages/rolldown/src/dts.test.ts +238 -38
- package/packages/rolldown/src/externals.test.ts +118 -0
- package/packages/rolldown/src/externals.ts +45 -0
- package/packages/rolldown/src/isolated-declarations.test.ts +122 -0
- package/packages/rolldown/src/isolated-declarations.ts +56 -8
- package/packages/rolldown/src/transform.test.ts +88 -2
- package/packages/rolldown/src/transform.ts +87 -33
- package/src/layers/github-actions/index.js +3 -0
- package/src/layers/prettier/index.js +12 -0
- package/src/layers/readme/index.js +176 -0
- package/src/layers/typescript/index.js +11 -0
- package/src/layers/vitest/index.js +11 -0
- package/src/utils/project.js +7 -0
- package/src/utils/types.ts +5 -0
- package/test/ember.nvp/layer-readme.test.ts +195 -0
- package/test/ember.nvp/layers.test.ts +1 -0
- package/test/ember.nvp/scoped-css-library.test.ts +211 -0
|
@@ -6,13 +6,30 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
6
6
|
|
|
7
7
|
import { ember } from "../index.ts";
|
|
8
8
|
|
|
9
|
+
interface Dist {
|
|
10
|
+
/** Sorted dist-relative paths of every emitted file. */
|
|
11
|
+
files: string[];
|
|
12
|
+
/** dist-relative path -> content, for every emitted .d.ts and .css file. */
|
|
13
|
+
contents: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let restoreCwd: (() => void) | undefined;
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
restoreCwd?.();
|
|
20
|
+
restoreCwd = undefined;
|
|
21
|
+
});
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Drives `ember()` through a real tsdown build (the dts pipeline included).
|
|
11
25
|
* `files` is a map of relative path -> source, written into a temp dir that
|
|
12
26
|
* becomes the cwd for the build (the externals and isolated-declarations
|
|
13
27
|
* plugins read package.json / tsconfig.json from cwd, like the tsdown CLI).
|
|
28
|
+
*
|
|
29
|
+
* Returns the emitted file list plus the full contents of every declaration
|
|
30
|
+
* and css file (js output is covered by transform.test.ts).
|
|
14
31
|
*/
|
|
15
|
-
async function buildFixture(files: Record<string, string>, entry: string[]): Promise<
|
|
32
|
+
async function buildFixture(files: Record<string, string>, entry: string[]): Promise<Dist> {
|
|
16
33
|
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-dts-"));
|
|
17
34
|
|
|
18
35
|
const withDefaults: Record<string, string> = {
|
|
@@ -45,33 +62,40 @@ async function buildFixture(files: Record<string, string>, entry: string[]): Pro
|
|
|
45
62
|
plugins: [ember()],
|
|
46
63
|
});
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
}
|
|
65
|
+
const distDir = path.join(dir, "dist");
|
|
66
|
+
const dist: Dist = { files: [], contents: {} };
|
|
50
67
|
|
|
51
|
-
|
|
68
|
+
for (const entry of await readdir(distDir, { withFileTypes: true, recursive: true })) {
|
|
69
|
+
if (!entry.isFile()) continue;
|
|
52
70
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
restoreCwd = undefined;
|
|
56
|
-
});
|
|
71
|
+
const relative = path.relative(distDir, path.join(entry.parentPath, entry.name));
|
|
72
|
+
dist.files.push(relative);
|
|
57
73
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for (const entry of await readdir(dir, { withFileTypes: true, recursive: true })) {
|
|
61
|
-
if (entry.isFile()) {
|
|
62
|
-
out.push(path.relative(dir, path.join(entry.parentPath, entry.name)));
|
|
74
|
+
if (relative.endsWith(".d.ts") || relative.endsWith(".css")) {
|
|
75
|
+
dist.contents[relative] = await readFile(path.join(distDir, relative), "utf8");
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
|
-
|
|
78
|
+
|
|
79
|
+
dist.files.sort();
|
|
80
|
+
|
|
81
|
+
return dist;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One printable document of every captured file, for a single snapshot. */
|
|
85
|
+
function printed(dist: Dist): string {
|
|
86
|
+
return Object.entries(dist.contents)
|
|
87
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
88
|
+
.map(([file, content]) => `//=== ${file}\n${content}`)
|
|
89
|
+
.join("\n");
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
describe("declarations (full ember() via tsdown)", () => {
|
|
69
93
|
it("emits .d.ts for an entry that type-imports from a non-entry .gts", async () => {
|
|
70
|
-
// Mirrors
|
|
94
|
+
// Mirrors a design-system shape: the component is in the runtime graph via
|
|
71
95
|
// the index entry, while a second entry (a service) only *type*-imports
|
|
72
96
|
// from it. The service's declaration then references the component's
|
|
73
97
|
// (virtual) .d.ts, which only exists in the bundler's module graph.
|
|
74
|
-
const
|
|
98
|
+
const dist = await buildFixture(
|
|
75
99
|
{
|
|
76
100
|
"src/index.ts": `export { default as Popup } from './popup.gts';`,
|
|
77
101
|
"src/service.ts": [
|
|
@@ -103,17 +127,164 @@ describe("declarations (full ember() via tsdown)", () => {
|
|
|
103
127
|
["./src/index.ts", "./src/service.ts"],
|
|
104
128
|
);
|
|
105
129
|
|
|
106
|
-
|
|
130
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
131
|
+
[
|
|
132
|
+
"index.d.ts",
|
|
133
|
+
"index.js",
|
|
134
|
+
"index.js.map",
|
|
135
|
+
"popup-CjMmUEVb.d.ts",
|
|
136
|
+
"popup-CjMmUEVb.d.ts.map",
|
|
137
|
+
"service.d.ts",
|
|
138
|
+
"service.d.ts.map",
|
|
139
|
+
"service.js",
|
|
140
|
+
"service.js.map",
|
|
141
|
+
]
|
|
142
|
+
`);
|
|
107
143
|
|
|
108
|
-
expect(dist).
|
|
109
|
-
|
|
144
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
145
|
+
"//=== index.d.ts
|
|
146
|
+
import { t as Popup } from "./popup-CjMmUEVb.js";
|
|
147
|
+
export { Popup };
|
|
148
|
+
//=== popup-CjMmUEVb.d.ts
|
|
149
|
+
import Component from "@glimmer/component";
|
|
150
|
+
//#region src/popup.d.ts
|
|
151
|
+
interface PopupBlock {
|
|
152
|
+
open: () => void;
|
|
153
|
+
}
|
|
154
|
+
interface PopupSignature {
|
|
155
|
+
Element: HTMLDivElement;
|
|
156
|
+
Blocks: {
|
|
157
|
+
default: [PopupBlock];
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
declare class Popup extends Component<PopupSignature> {}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { PopupBlock as n, Popup as t };
|
|
163
|
+
//# sourceMappingURL=popup-CjMmUEVb.d.ts.map
|
|
164
|
+
//=== service.d.ts
|
|
165
|
+
import { n as PopupBlock } from "./popup-CjMmUEVb.js";
|
|
166
|
+
//#region src/service.d.ts
|
|
167
|
+
declare class TooltipService {
|
|
168
|
+
registerPopup(context: PopupBlock): void;
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { TooltipService };
|
|
172
|
+
//# sourceMappingURL=service.d.ts.map"
|
|
173
|
+
`);
|
|
174
|
+
});
|
|
110
175
|
|
|
111
|
-
|
|
176
|
+
it("emits .d.ts when a .gts module is only ever type-imported", async () => {
|
|
177
|
+
// Nothing value-imports popup.gts, so its virtual module never enters the
|
|
178
|
+
// runtime graph: the only route to its declaration is the dts resolver
|
|
179
|
+
// loading it on demand. The declaration pipeline registers modules as
|
|
180
|
+
// their (virtual) source loads, so the import must resolve to the source
|
|
181
|
+
// id — a pre-rewritten `.d.ts` id skips that registration and dangles.
|
|
182
|
+
const dist = await buildFixture(
|
|
183
|
+
{
|
|
184
|
+
"src/service.ts": [
|
|
185
|
+
`import type { PopupBlock } from './components/popup.gts';`,
|
|
186
|
+
``,
|
|
187
|
+
`export class TooltipService {`,
|
|
188
|
+
` registerPopup(context: PopupBlock): void {`,
|
|
189
|
+
` void context;`,
|
|
190
|
+
` }`,
|
|
191
|
+
`}`,
|
|
192
|
+
].join("\n"),
|
|
193
|
+
"src/components/popup.gts": [
|
|
194
|
+
`import Component from '@glimmer/component';`,
|
|
195
|
+
``,
|
|
196
|
+
`export interface PopupBlock {`,
|
|
197
|
+
` open: () => void;`,
|
|
198
|
+
`}`,
|
|
199
|
+
``,
|
|
200
|
+
`export default class Popup extends Component {`,
|
|
201
|
+
` <template>popup</template>`,
|
|
202
|
+
`}`,
|
|
203
|
+
].join("\n"),
|
|
204
|
+
},
|
|
205
|
+
["./src/service.ts"],
|
|
206
|
+
);
|
|
112
207
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
208
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
209
|
+
[
|
|
210
|
+
"service.d.ts",
|
|
211
|
+
"service.d.ts.map",
|
|
212
|
+
"service.js",
|
|
213
|
+
"service.js.map",
|
|
214
|
+
]
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
218
|
+
"//=== service.d.ts
|
|
219
|
+
import "@glimmer/component";
|
|
220
|
+
//#region src/components/popup.d.ts
|
|
221
|
+
interface PopupBlock {
|
|
222
|
+
open: () => void;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/service.d.ts
|
|
226
|
+
declare class TooltipService {
|
|
227
|
+
registerPopup(context: PopupBlock): void;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { TooltipService };
|
|
231
|
+
//# sourceMappingURL=service.d.ts.map"
|
|
232
|
+
`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("emits .d.ts for a .gts that type-imports another (otherwise unimported) .gts", async () => {
|
|
236
|
+
// A nested component type-imports a sibling from the parent directory;
|
|
237
|
+
// its declaration then carries a relative source specifier for a module
|
|
238
|
+
// that exists only in the bundler's graph (never on disk, never in the
|
|
239
|
+
// runtime graph).
|
|
240
|
+
const dist = await buildFixture(
|
|
241
|
+
{
|
|
242
|
+
"src/index.ts": `export { default as OptionList } from './components/select/option-list.gts';`,
|
|
243
|
+
"src/components/select/option-list.gts": [
|
|
244
|
+
`import Component from '@glimmer/component';`,
|
|
245
|
+
`import type { PopupBlock } from '../popup.gts';`,
|
|
246
|
+
``,
|
|
247
|
+
`export interface OptionListArgs {`,
|
|
248
|
+
` registerPopup: (context: PopupBlock) => void;`,
|
|
249
|
+
`}`,
|
|
250
|
+
``,
|
|
251
|
+
`export default class OptionList extends Component {`,
|
|
252
|
+
` <template>options</template>`,
|
|
253
|
+
`}`,
|
|
254
|
+
].join("\n"),
|
|
255
|
+
"src/components/popup.gts": [
|
|
256
|
+
`import Component from '@glimmer/component';`,
|
|
257
|
+
``,
|
|
258
|
+
`export interface PopupBlock {`,
|
|
259
|
+
` open: () => void;`,
|
|
260
|
+
`}`,
|
|
261
|
+
``,
|
|
262
|
+
`export default class Popup extends Component {`,
|
|
263
|
+
` <template>popup</template>`,
|
|
264
|
+
`}`,
|
|
265
|
+
].join("\n"),
|
|
266
|
+
},
|
|
267
|
+
["./src/index.ts"],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
271
|
+
[
|
|
272
|
+
"index.d.ts",
|
|
273
|
+
"index.d.ts.map",
|
|
274
|
+
"index.js",
|
|
275
|
+
"index.js.map",
|
|
276
|
+
]
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
280
|
+
"//=== index.d.ts
|
|
281
|
+
import Component from "@glimmer/component";
|
|
282
|
+
//#region src/components/select/option-list.d.ts
|
|
283
|
+
declare class OptionList extends Component {}
|
|
284
|
+
//#endregion
|
|
285
|
+
export { OptionList };
|
|
286
|
+
//# sourceMappingURL=index.d.ts.map"
|
|
287
|
+
`);
|
|
117
288
|
});
|
|
118
289
|
|
|
119
290
|
it("bundles co-located CSS (via @tsdown/css) without breaking declarations", async () => {
|
|
@@ -121,8 +292,8 @@ describe("declarations (full ember() via tsdown)", () => {
|
|
|
121
292
|
// handling the build dies on tsdown's css-guard — and because the failing
|
|
122
293
|
// component module never loads, every declaration that references it
|
|
123
294
|
// dangles (surfacing as misleading UNLOADABLE_DEPENDENCY errors on
|
|
124
|
-
// <component>.d.ts). @tsdown/css
|
|
125
|
-
const
|
|
295
|
+
// <component>.d.ts). @tsdown/css makes both work.
|
|
296
|
+
const dist = await buildFixture(
|
|
126
297
|
{
|
|
127
298
|
"src/index.ts": `export { default as Popup } from './popup.gts';`,
|
|
128
299
|
"src/service.ts": [
|
|
@@ -149,18 +320,47 @@ describe("declarations (full ember() via tsdown)", () => {
|
|
|
149
320
|
["./src/index.ts", "./src/service.ts"],
|
|
150
321
|
);
|
|
151
322
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
323
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
324
|
+
[
|
|
325
|
+
"index.d.ts",
|
|
326
|
+
"index.js",
|
|
327
|
+
"index.js.map",
|
|
328
|
+
"popup-CytznrQX.d.ts",
|
|
329
|
+
"popup-CytznrQX.d.ts.map",
|
|
330
|
+
"service.d.ts",
|
|
331
|
+
"service.d.ts.map",
|
|
332
|
+
"service.js",
|
|
333
|
+
"service.js.map",
|
|
334
|
+
"style.css",
|
|
335
|
+
]
|
|
336
|
+
`);
|
|
159
337
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
338
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
339
|
+
"//=== index.d.ts
|
|
340
|
+
import { t as Popup } from "./popup-CytznrQX.js";
|
|
341
|
+
export { Popup };
|
|
342
|
+
//=== popup-CytznrQX.d.ts
|
|
343
|
+
import Component from "@glimmer/component";
|
|
344
|
+
//#region src/popup.d.ts
|
|
345
|
+
interface PopupBlock {
|
|
346
|
+
open: () => void;
|
|
347
|
+
}
|
|
348
|
+
declare class Popup extends Component {}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { PopupBlock as n, Popup as t };
|
|
351
|
+
//# sourceMappingURL=popup-CytznrQX.d.ts.map
|
|
352
|
+
//=== service.d.ts
|
|
353
|
+
import { n as PopupBlock } from "./popup-CytznrQX.js";
|
|
354
|
+
//#region src/service.d.ts
|
|
355
|
+
declare function register(context: PopupBlock): void;
|
|
356
|
+
//#endregion
|
|
357
|
+
export { register };
|
|
358
|
+
//# sourceMappingURL=service.d.ts.map
|
|
359
|
+
//=== style.css
|
|
360
|
+
.popup {
|
|
361
|
+
color: red;
|
|
362
|
+
}
|
|
363
|
+
"
|
|
364
|
+
`);
|
|
165
365
|
});
|
|
166
366
|
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { rolldown } from "rolldown";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { emberExternals } from "./externals.ts";
|
|
8
|
+
|
|
9
|
+
let restoreCwd: (() => void) | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
restoreCwd?.();
|
|
13
|
+
restoreCwd = undefined;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An ember-source stub whose `ember-addon.renamed-modules` provides
|
|
18
|
+
* `@glimmer/runtime` (file-path keys, like the real manifest).
|
|
19
|
+
*/
|
|
20
|
+
const EMBER_SOURCE_STUB = {
|
|
21
|
+
name: "ember-source",
|
|
22
|
+
version: "0.0.0",
|
|
23
|
+
"ember-addon": {
|
|
24
|
+
"renamed-modules": {
|
|
25
|
+
"@glimmer/runtime/index.js": "ember-source/@glimmer/runtime/index.js",
|
|
26
|
+
"@ember/-internals/glimmer/index.js": "ember-source/@ember/-internals/glimmer/index.js",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Writes a fixture package (manifest + optional stub ember-source in its
|
|
33
|
+
* node_modules) and cds into it — the plugin reads both from the library's
|
|
34
|
+
* own context, like the tsdown CLI.
|
|
35
|
+
*/
|
|
36
|
+
async function fixture(manifest: object, { emberSource = true } = {}): Promise<string> {
|
|
37
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-externals-"));
|
|
38
|
+
await writeFile(path.join(dir, "package.json"), JSON.stringify(manifest));
|
|
39
|
+
|
|
40
|
+
if (emberSource) {
|
|
41
|
+
const stubDir = path.join(dir, "node_modules/ember-source");
|
|
42
|
+
await mkdir(stubDir, { recursive: true });
|
|
43
|
+
await writeFile(path.join(stubDir, "package.json"), JSON.stringify(EMBER_SOURCE_STUB));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const previousCwd = process.cwd();
|
|
47
|
+
process.chdir(dir);
|
|
48
|
+
restoreCwd = () => process.chdir(previousCwd);
|
|
49
|
+
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Runs the plugin's own hooks directly against the current fixture. */
|
|
54
|
+
function resolveWith(source: string): unknown {
|
|
55
|
+
const plugin = emberExternals();
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
|
57
|
+
(plugin.buildStart as any).call({ addWatchFile() {} });
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
|
59
|
+
return (plugin.resolveId as any).handler(source);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("emberExternals", () => {
|
|
63
|
+
it("externalizes declared dependencies and the ember virtual packages", async () => {
|
|
64
|
+
await fixture({ dependencies: { "some-dep": "*" } });
|
|
65
|
+
expect(resolveWith("some-dep")).toBe(false);
|
|
66
|
+
expect(resolveWith("@ember/component")).toBe(false);
|
|
67
|
+
expect(resolveWith("@glimmer/tracking")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("externalizes the modules ember-source provides via renamed-modules", async () => {
|
|
71
|
+
// Private API like @glimmer/runtime exists only inside the app's
|
|
72
|
+
// ember-source (its ember-addon.renamed-modules) — @embroider/core's
|
|
73
|
+
// emberVirtualPackages doesn't list it.
|
|
74
|
+
await fixture({});
|
|
75
|
+
expect(resolveWith("@glimmer/runtime")).toBe(false);
|
|
76
|
+
expect(resolveWith("@ember/-internals/glimmer")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not blanket-externalize @glimmer packages ember-source doesn't provide", async () => {
|
|
80
|
+
// e.g. @glimmer/component is a real package a library may want bundled;
|
|
81
|
+
// only declared deps / virtual packages / renamed-modules are external.
|
|
82
|
+
await fixture({});
|
|
83
|
+
expect(resolveWith("@glimmer/not-provided")).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("leaves undeclared packages alone, with or without ember-source", async () => {
|
|
87
|
+
await fixture({});
|
|
88
|
+
expect(resolveWith("left-pad")).toBeUndefined();
|
|
89
|
+
|
|
90
|
+
await fixture({}, { emberSource: false });
|
|
91
|
+
expect(resolveWith("left-pad")).toBeUndefined();
|
|
92
|
+
// No ember-source in the graph -> no renamed-modules to consult.
|
|
93
|
+
expect(resolveWith("@glimmer/runtime")).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps a renamed-modules import external through a real build", async () => {
|
|
97
|
+
const dir = await fixture({ name: "build-fixture" });
|
|
98
|
+
await writeFile(
|
|
99
|
+
path.join(dir, "index.ts"),
|
|
100
|
+
`import { curry } from '@glimmer/runtime';\nexport const curried: unknown = curry;\n`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const warnings: string[] = [];
|
|
104
|
+
const build = await rolldown({
|
|
105
|
+
input: path.join(dir, "index.ts"),
|
|
106
|
+
plugins: [emberExternals()],
|
|
107
|
+
onwarn(warning) {
|
|
108
|
+
warnings.push(warning.code ?? String(warning));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const { output } = await build.generate({ format: "es" });
|
|
112
|
+
|
|
113
|
+
expect(warnings.filter((code) => code === "UNRESOLVED_IMPORT")).toEqual([]);
|
|
114
|
+
|
|
115
|
+
const code = output.filter((chunk) => "code" in chunk).map((chunk) => chunk.code)[0] ?? "";
|
|
116
|
+
expect(code).toContain(`from "@glimmer/runtime"`);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
|
|
3
5
|
import type { Plugin } from "rolldown";
|
|
4
6
|
|
|
@@ -33,6 +35,38 @@ function resolvableDependencies(): Set<string> {
|
|
|
33
35
|
return deps;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The module names ember-source provides by renaming them into itself — the
|
|
40
|
+
* keys of its `ember-addon.renamed-modules`, normalized from file paths to
|
|
41
|
+
* import specifiers (`@glimmer/runtime/index.js` → `@glimmer/runtime`).
|
|
42
|
+
*
|
|
43
|
+
* This is the authoritative list of ember's provided modules, including
|
|
44
|
+
* private API that @embroider/core's emberVirtualPackages doesn't cover
|
|
45
|
+
* (e.g. `@glimmer/runtime`). It deliberately does NOT include real packages
|
|
46
|
+
* like `@glimmer/component`, which a library may want bundled.
|
|
47
|
+
*
|
|
48
|
+
* Resolved from the library's own dependency graph; empty when ember-source
|
|
49
|
+
* isn't resolvable there.
|
|
50
|
+
*/
|
|
51
|
+
function emberSourceRenamedModules(): Set<string> {
|
|
52
|
+
const provided = new Set<string>();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const require = createRequire(path.resolve("package.json"));
|
|
56
|
+
const manifest = require("ember-source/package.json") as {
|
|
57
|
+
"ember-addon"?: { "renamed-modules"?: Record<string, string> };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const key of Object.keys(manifest["ember-addon"]?.["renamed-modules"] ?? {})) {
|
|
61
|
+
provided.add(key.replace(/\.js$/, "").replace(/\/index$/, ""));
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
// The library doesn't have ember-source in its graph; nothing to provide.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return provided;
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
/**
|
|
37
71
|
* Keeps the library's declared dependencies and the ember virtual packages
|
|
38
72
|
* (things like `@ember/component`, `@glimmer/tracking`, the template compiler,
|
|
@@ -41,6 +75,7 @@ function resolvableDependencies(): Set<string> {
|
|
|
41
75
|
*/
|
|
42
76
|
export function emberExternals(): Plugin {
|
|
43
77
|
let deps: Set<string>;
|
|
78
|
+
let renamedModules: Set<string>;
|
|
44
79
|
|
|
45
80
|
return {
|
|
46
81
|
name: "ember:externals",
|
|
@@ -48,6 +83,7 @@ export function emberExternals(): Plugin {
|
|
|
48
83
|
buildStart() {
|
|
49
84
|
this.addWatchFile("package.json");
|
|
50
85
|
deps = resolvableDependencies();
|
|
86
|
+
renamedModules = emberSourceRenamedModules();
|
|
51
87
|
},
|
|
52
88
|
|
|
53
89
|
resolveId: {
|
|
@@ -76,6 +112,15 @@ export function emberExternals(): Plugin {
|
|
|
76
112
|
return false;
|
|
77
113
|
}
|
|
78
114
|
|
|
115
|
+
// Modules ember-source provides by renaming them into itself
|
|
116
|
+
// (`ember-addon.renamed-modules`) — private API like
|
|
117
|
+
// `@glimmer/runtime` that only exists inside the app's ember-source.
|
|
118
|
+
// Matched on the full specifier: the renamed modules are module
|
|
119
|
+
// paths, not packages.
|
|
120
|
+
if (renamedModules.has(source)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
79
124
|
return undefined;
|
|
80
125
|
},
|
|
81
126
|
},
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { build } from "tsdown";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { ember } from "../index.ts";
|
|
8
|
+
|
|
9
|
+
let restoreCwd: (() => void) | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
restoreCwd?.();
|
|
13
|
+
restoreCwd = undefined;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs a real tsdown build of a single trivial entry in a temp dir, so the
|
|
18
|
+
* isolated-declarations guard reads the tsconfig files written by the test the
|
|
19
|
+
* same way it does under the tsdown CLI. Returns the build's rejection, or
|
|
20
|
+
* `undefined` when it succeeded.
|
|
21
|
+
*/
|
|
22
|
+
async function buildFixture(
|
|
23
|
+
files: Record<string, string>,
|
|
24
|
+
tsconfig?: string | boolean,
|
|
25
|
+
): Promise<Error | undefined> {
|
|
26
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-isolated-declarations-"));
|
|
27
|
+
|
|
28
|
+
const withDefaults: Record<string, string> = {
|
|
29
|
+
"package.json": JSON.stringify({ name: "fixture", version: "0.0.0", type: "module" }),
|
|
30
|
+
"src/index.ts": `export const greeting: string = 'hello';`,
|
|
31
|
+
...files,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
for (const [relative, source] of Object.entries(withDefaults)) {
|
|
35
|
+
const full = path.join(dir, relative);
|
|
36
|
+
await mkdir(path.dirname(full), { recursive: true });
|
|
37
|
+
await writeFile(full, source, "utf8");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const previousCwd = process.cwd();
|
|
41
|
+
process.chdir(dir);
|
|
42
|
+
restoreCwd = () => process.chdir(previousCwd);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await build({
|
|
46
|
+
entry: ["./src/index.ts"],
|
|
47
|
+
config: false,
|
|
48
|
+
logLevel: "silent",
|
|
49
|
+
tsconfig,
|
|
50
|
+
plugins: [ember()],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return error as Error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const withFlag = JSON.stringify({
|
|
60
|
+
compilerOptions: { isolatedDeclarations: true, declaration: true },
|
|
61
|
+
});
|
|
62
|
+
const withoutFlag = JSON.stringify({ compilerOptions: { declaration: true } });
|
|
63
|
+
|
|
64
|
+
describe("isolated declarations guard", () => {
|
|
65
|
+
it("errors when the cwd tsconfig.json does not set the flag", async () => {
|
|
66
|
+
const error = await buildFixture({ "tsconfig.json": withoutFlag });
|
|
67
|
+
|
|
68
|
+
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("passes when the cwd tsconfig.json sets the flag", async () => {
|
|
72
|
+
expect(await buildFixture({ "tsconfig.json": withFlag })).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("checks the tsconfig the `tsconfig` option points at, not tsconfig.json", async () => {
|
|
76
|
+
// The shape a library with in-package dev code wants: a permissive
|
|
77
|
+
// tsconfig.json for editors and `tsc --noEmit` over everything, and a
|
|
78
|
+
// publish-only tsconfig -- the one the build uses -- carrying the flag.
|
|
79
|
+
const error = await buildFixture(
|
|
80
|
+
{ "tsconfig.json": withoutFlag, "tsconfig.publish.json": withFlag },
|
|
81
|
+
"./tsconfig.publish.json",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(error).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("errors when the tsconfig the `tsconfig` option points at lacks the flag", async () => {
|
|
88
|
+
const error = await buildFixture(
|
|
89
|
+
{ "tsconfig.json": withFlag, "tsconfig.publish.json": withoutFlag },
|
|
90
|
+
"./tsconfig.publish.json",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(error?.message).toContain("tsconfig.publish.json");
|
|
94
|
+
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("resolves a directory `tsconfig` option to the tsconfig.json inside it", async () => {
|
|
98
|
+
const error = await buildFixture(
|
|
99
|
+
{ "tsconfig.json": withFlag, "config/tsconfig.json": withoutFlag },
|
|
100
|
+
"./config",
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(error?.message).toContain(`must set "compilerOptions.isolatedDeclarations": true`);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("skips the check when there is no tsconfig at all", async () => {
|
|
107
|
+
expect(await buildFixture({})).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("skips the check when tsconfig is disabled", async () => {
|
|
111
|
+
expect(await buildFixture({ "tsconfig.json": withoutFlag }, false)).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("inherits the flag through an extends chain", async () => {
|
|
115
|
+
const error = await buildFixture({
|
|
116
|
+
"tsconfig.base.json": withFlag,
|
|
117
|
+
"tsconfig.json": JSON.stringify({ extends: "./tsconfig.base.json" }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(error).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|