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.
@@ -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<string> {
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
- return dir;
49
- }
65
+ const distDir = path.join(dir, "dist");
66
+ const dist: Dist = { files: [], contents: {} };
50
67
 
51
- let restoreCwd: (() => void) | undefined;
68
+ for (const entry of await readdir(distDir, { withFileTypes: true, recursive: true })) {
69
+ if (!entry.isFile()) continue;
52
70
 
53
- afterEach(() => {
54
- restoreCwd?.();
55
- restoreCwd = undefined;
56
- });
71
+ const relative = path.relative(distDir, path.join(entry.parentPath, entry.name));
72
+ dist.files.push(relative);
57
73
 
58
- async function listFiles(dir: string): Promise<string[]> {
59
- const out: string[] = [];
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
- return out.sort();
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 the luna-core shape: the component is in the runtime graph via
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 dir = await buildFixture(
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
- const dist = await listFiles(path.join(dir, "dist"));
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).toContain("index.d.ts");
109
- expect(dist).toContain("service.d.ts");
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
- const serviceDts = await readFile(path.join(dir, "dist/service.d.ts"), "utf8");
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
- // The PopupBlock type must be reachable: either inlined into the bundled
114
- // declaration or imported via a specifier that exists in dist.
115
- expect(serviceDts).toContain("PopupBlock");
116
- expect(serviceDts).not.toContain(".gts");
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 (this package's peer) makes both work.
125
- const dir = await buildFixture(
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
- const dist = await listFiles(path.join(dir, "dist"));
153
-
154
- const cssFiles = dist.filter((file) => file.endsWith(".css"));
155
- expect(cssFiles).toHaveLength(1);
156
-
157
- const css = await readFile(path.join(dir, "dist", cssFiles[0]!), "utf8");
158
- expect(css).toContain(".popup");
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
- // The cascade regression: the css-importing component's declaration must
161
- // still be generated and reachable from the service's declaration.
162
- const serviceDts = await readFile(path.join(dir, "dist/service.d.ts"), "utf8");
163
- expect(serviceDts).toContain("PopupBlock");
164
- expect(serviceDts).not.toContain(".gts");
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
+ });