ember.nvp 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.release-plan.json +15 -35
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +73 -4
  4. package/package.json +2 -1
  5. package/packages/rolldown/package.json +6 -2
  6. package/packages/rolldown/src/dts.test.ts +105 -1
  7. package/packages/rolldown/src/isolated-declarations.ts +67 -61
  8. package/packages/rolldown/src/transform.test.ts +212 -15
  9. package/packages/rolldown/src/transform.ts +106 -26
  10. package/src/cli/args.js +170 -48
  11. package/src/cli/generator.js +2 -1
  12. package/src/cli/help.js +82 -0
  13. package/src/cli/index.js +15 -0
  14. package/src/cli/questions/layer-options.js +130 -0
  15. package/src/cli/questions/validate-option.js +106 -0
  16. package/src/layers/eslint-bundled/app.test.ts +182 -0
  17. package/src/layers/eslint-bundled/index.js +172 -0
  18. package/src/layers/expect-type/files/type-tests/index.test.ts +4 -0
  19. package/src/layers/expect-type/files/type-tests/tsconfig.json +8 -0
  20. package/src/layers/expect-type/index.js +114 -0
  21. package/src/layers/github-actions/app.test.ts +1 -1
  22. package/src/layers/typescript/app.test.ts +6 -0
  23. package/src/layers/typescript/index.js +48 -148
  24. package/src/layers/typescript/typescript-6.js +102 -0
  25. package/src/layers/typescript/typescript-7.js +177 -0
  26. package/src/utils/manifest.js +13 -0
  27. package/src/utils/npm.js +27 -0
  28. package/src/utils/project.js +23 -0
  29. package/src/utils/types.ts +52 -1
  30. package/src/utils/typescript.js +147 -0
  31. package/test/ember.nvp/base-library.test.ts +11 -0
  32. package/test/ember.nvp/cli.test.ts +142 -1
  33. package/test/ember.nvp/layer-expect-type.test.ts +113 -0
  34. package/test/ember.nvp/layer-options.test.ts +248 -0
  35. package/test/ember.nvp/layer-readme.test.ts +4 -2
  36. package/test/ember.nvp/layer-typescript.test.ts +302 -0
  37. package/test/ember.nvp/layers.test.ts +2 -2
  38. package/test/ember.nvp/permutations/shared.ts +6 -6
  39. package/test/ember.nvp/validate-option.test.ts +152 -0
  40. package/test/test-helpers/permutate.test.ts +2 -7
  41. package/test/test-helpers.ts +12 -2
  42. package/vitest.config.js +7 -1
  43. package/src/layers/eslint-bundled-ember/index.js +0 -91
  44. package/src/layers/eslint-bundled-nvp/app.test.ts +0 -91
  45. package/src/layers/eslint-bundled-nvp/index.js +0 -91
  46. /package/src/layers/{eslint-bundled-ember/files → eslint-bundled/files/ember}/eslint.config.js +0 -0
  47. /package/src/layers/{eslint-bundled-nvp/files → eslint-bundled/files/nvp}/eslint.config.js +0 -0
@@ -2,64 +2,44 @@
2
2
  "solution": {
3
3
  "ember.nvp": {
4
4
  "impact": "minor",
5
- "oldVersion": "1.7.1",
6
- "newVersion": "1.8.0",
5
+ "oldVersion": "1.9.0",
6
+ "newVersion": "1.10.0",
7
7
  "tagName": "latest",
8
8
  "constraints": [
9
9
  {
10
10
  "impact": "minor",
11
11
  "reason": "Appears in changelog section :rocket: Enhancement"
12
+ },
13
+ {
14
+ "impact": "patch",
15
+ "reason": "Appears in changelog section :bug: Bug Fix"
12
16
  }
13
17
  ],
14
18
  "pkgJSONPath": "./package.json"
15
19
  },
16
20
  "@nullvoxpopuli/ember-build-tooling-utils": {
17
- "impact": "minor",
18
- "oldVersion": "1.1.1",
19
- "newVersion": "1.2.0",
20
- "tagName": "latest",
21
- "constraints": [
22
- {
23
- "impact": "minor",
24
- "reason": "Appears in changelog section :rocket: Enhancement"
25
- }
26
- ],
27
- "pkgJSONPath": "./packages/build-tooling-utils/package.json"
21
+ "oldVersion": "1.2.0"
28
22
  },
29
23
  "@nullvoxpopuli/ember-rolldown": {
30
24
  "impact": "minor",
31
- "oldVersion": "2.8.1",
32
- "newVersion": "2.9.0",
25
+ "oldVersion": "2.9.0",
26
+ "newVersion": "2.10.0",
33
27
  "tagName": "latest",
34
28
  "constraints": [
35
- {
36
- "impact": "patch",
37
- "reason": "Has dependency `workspace:*` on @nullvoxpopuli/ember-build-tooling-utils"
38
- },
39
29
  {
40
30
  "impact": "minor",
41
31
  "reason": "Appears in changelog section :rocket: Enhancement"
32
+ },
33
+ {
34
+ "impact": "patch",
35
+ "reason": "Appears in changelog section :bug: Bug Fix"
42
36
  }
43
37
  ],
44
38
  "pkgJSONPath": "./packages/rolldown/package.json"
45
39
  },
46
40
  "@nullvoxpopuli/ember-vite": {
47
- "impact": "minor",
48
- "oldVersion": "1.1.1",
49
- "newVersion": "1.2.0",
50
- "tagName": "latest",
51
- "constraints": [
52
- {
53
- "impact": "patch",
54
- "reason": "Has dependency `workspace:*` on @nullvoxpopuli/ember-build-tooling-utils"
55
- },
56
- {
57
- "impact": "minor",
58
- "reason": "Appears in changelog section :rocket: Enhancement"
59
- }
60
- ],
61
- "pkgJSONPath": "./packages/vite/package.json"
41
+ "oldVersion": "1.2.0"
62
42
  }
63
43
  },
64
- "description": "## Release (2026-09-21)\n\n* ember.nvp 1.8.0 (minor)\n* @nullvoxpopuli/ember-build-tooling-utils 1.2.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.9.0 (minor)\n* @nullvoxpopuli/ember-vite 1.2.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`, `@nullvoxpopuli/ember-build-tooling-utils`, `@nullvoxpopuli/ember-rolldown`, `@nullvoxpopuli/ember-vite`\n * [#135](https://github.com/NullVoxPopuli/ember.nvp/pull/135) Accept @babel/core 8 in the plugin packages ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 1\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n"
44
+ "description": "## Release (2026-09-30)\n\n* ember.nvp 1.10.0 (minor)\n* @nullvoxpopuli/ember-rolldown 2.10.0 (minor)\n\n#### :rocket: Enhancement\n* `ember.nvp`, `@nullvoxpopuli/ember-rolldown`\n * [#144](https://github.com/NullVoxPopuli/ember.nvp/pull/144) Layer options, with an ESLint preset and a TypeScript version ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n * [#142](https://github.com/NullVoxPopuli/ember.nvp/pull/142) Add a TypeScript 7.1+ layer ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#140](https://github.com/NullVoxPopuli/ember.nvp/pull/140) Point .gts declaration maps at the .gts source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n\n#### :bug: Bug Fix\n* `@nullvoxpopuli/ember-rolldown`, `ember.nvp`\n * [#145](https://github.com/NullVoxPopuli/ember.nvp/pull/145) Make .gts source maps point at the real source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n * [#140](https://github.com/NullVoxPopuli/ember.nvp/pull/140) Point .gts declaration maps at the .gts source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n\n#### Committers: 1\n- @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))\n"
65
45
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## Release (2026-09-30)
4
+
5
+ * ember.nvp 1.10.0 (minor)
6
+ * @nullvoxpopuli/ember-rolldown 2.10.0 (minor)
7
+
8
+ #### :rocket: Enhancement
9
+ * `ember.nvp`, `@nullvoxpopuli/ember-rolldown`
10
+ * [#144](https://github.com/NullVoxPopuli/ember.nvp/pull/144) Layer options, with an ESLint preset and a TypeScript version ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
11
+ * [#142](https://github.com/NullVoxPopuli/ember.nvp/pull/142) Add a TypeScript 7.1+ layer ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
12
+ * `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
13
+ * [#140](https://github.com/NullVoxPopuli/ember.nvp/pull/140) Point .gts declaration maps at the .gts source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
14
+
15
+ #### :bug: Bug Fix
16
+ * `@nullvoxpopuli/ember-rolldown`, `ember.nvp`
17
+ * [#145](https://github.com/NullVoxPopuli/ember.nvp/pull/145) Make .gts source maps point at the real source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
18
+ * [#140](https://github.com/NullVoxPopuli/ember.nvp/pull/140) Point .gts declaration maps at the .gts source ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
19
+
20
+ #### Committers: 1
21
+ - @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
22
+
23
+ ## Release (2026-09-28)
24
+
25
+ * ember.nvp 1.9.0 (minor)
26
+
27
+ #### :rocket: Enhancement
28
+ * `ember.nvp`
29
+ * [#139](https://github.com/NullVoxPopuli/ember.nvp/pull/139) CLI help ([@tcjr](https://github.com/tcjr))
30
+ * [#137](https://github.com/NullVoxPopuli/ember.nvp/pull/137) Add an expect-type layer for type tests ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
31
+
32
+ #### Committers: 2
33
+ - @NullVoxPopuli's reduced-access machine account for AI usage ([@NullVoxPopuli-ai-agent](https://github.com/NullVoxPopuli-ai-agent))
34
+ - Tom Carter ([@tcjr](https://github.com/tcjr))
35
+
3
36
  ## Release (2026-09-21)
4
37
 
5
38
  * ember.nvp 1.8.0 (minor)
package/README.md CHANGED
@@ -154,6 +154,11 @@ Adds modern ESLint configuration with:
154
154
  - Flat config (ESLint 9+)
155
155
  - Prettier compatibility
156
156
 
157
+ Choose a preset:
158
+
159
+ - `ember` (default): [ember-eslint](https://github.com/NullVoxPopuli/ember-eslint), which follows the official configuration
160
+ - `nvp`: [@nullvoxpopuli/eslint-configs](https://github.com/NullVoxPopuli/eslint-configs)
161
+
157
162
  ### 🎨 Prettier (optional)
158
163
 
159
164
  Code formatting with:
@@ -172,6 +177,26 @@ Code formatting with:
172
177
 
173
178
  Super experimental vitest setup using [ember-vitest](https://github.com/NullVoxPopuli/ember-vitest)
174
179
 
180
+ ### TypeScript (optional, on by default for libraries)
181
+
182
+ Choose a version:
183
+
184
+ - `7` (default): native TypeScript 7 and [ember-content-mapper](https://github.com/NullVoxPopuli/ember-content-mapper) for `.gts` and `.gjs`.
185
+ `lint:types` runs `tsc --noEmit --runExternalCode`.
186
+ ESLint keeps TypeScript 6, because typescript-eslint needs its API.
187
+ - `6`: TypeScript 6 and Glint's `ember-tsc`.
188
+
189
+ A TypeScript 6 project moves to 7 when you choose 7.
190
+ A TypeScript 7 project stays on 7.
191
+
192
+ ### expect-type (optional)
193
+
194
+ Type tests for TypeScript libraries with [expect-type](https://github.com/mmkal/expect-type).
195
+
196
+ - Tests live in `type-tests/`, with their own tsconfig.
197
+ - `lint:type-tests` typechecks them, and `lint` runs it too.
198
+ - JavaScript projects and apps get nothing.
199
+
175
200
  ## Architecture
176
201
 
177
202
  ### How Layers Work
@@ -180,7 +205,7 @@ Super experimental vitest setup using [ember-vitest](https://github.com/NullVoxP
180
205
  2. **Selection**: the user selects which optional layers to include
181
206
  3. **Execution**: each layer's `run()` function is called in sequence:
182
207
  ```js
183
- await layer.run(project);
208
+ await layer.run(project, project.getLayerOptions(layer.name));
184
209
  ```
185
210
  4. **Layer Functions**: inside `run()`, layers use [`ember-apply`](https://ember-apply.pages.dev/) to apply codemods:
186
211
  - copy files from the `files/` directory
@@ -196,20 +221,34 @@ To add a new layer, create a new directory in `src/layers/` with:
196
221
  1. **`index.js`** - Layer definition:
197
222
 
198
223
  ```js
224
+ // src/layers/itemizer/index.js
199
225
  import { packageJson, files } from "ember-apply";
200
226
  import { join } from "node:path";
201
227
 
202
228
  export default {
203
229
  label: "My Feature",
204
- description: "What this feature does",
230
+ hint: "What this feature does",
231
+
232
+ // Optional: questions to ask when the layer is selected (see "Layer options")
233
+ options: {
234
+ maxItems: {
235
+ type: "number",
236
+ prompt: "Enter maximum item count:",
237
+ default: 10,
238
+ validate: (val) => (val > 0 ? undefined : "Must be greater than 0"),
239
+ },
240
+ },
205
241
 
206
- async run(project) {
242
+ async run(project, { maxItems }) {
207
243
  // Copy files from files/ directory
208
244
  await files.applyFolder(join(import.meta.dirname, "files"), project.directory);
209
245
 
210
246
  await packageJson.addDependencies({ "some-package": "^1.0.0" }, project.directory);
211
247
 
212
- await packageJson.addScripts({ "my-script": 'echo "Hello"' }, project.directory);
248
+ await packageJson.addScripts(
249
+ { "my-script": `echo "Max items: ${maxItems}"` },
250
+ project.directory,
251
+ );
213
252
  },
214
253
 
215
254
  // Optionally add something to the README.md file
@@ -223,3 +262,33 @@ export default {
223
262
  - files are copied to the target directory, keeping their structure
224
263
 
225
264
  The CLI discovers the layer and offers it as an option.
265
+
266
+ ### Layer options
267
+
268
+ A layer's `options` are questions that the CLI asks after the user selects the layer.
269
+ `run(project, options)` receives the answers, with defaults filled in.
270
+ `project.getLayerOptions(name)` returns the options of any layer.
271
+
272
+ Each option needs:
273
+
274
+ - `type`: `"text"`, `"number"`, `"select"`, `"confirm"`, or `"multiselect"`
275
+ - `prompt`: the question, also shown in `--help`
276
+ - `options`, for `select` and `multiselect`: the choices, as `{ value, label, hint }`
277
+
278
+ Each option can also have:
279
+
280
+ - `default`
281
+ - `validate`: the same as [clack's `validate`](https://github.com/bombshell-dev/clack/tree/main/packages/prompts#text).
282
+ A function that returns a message to reject the value, or a [Standard Schema](https://standardschema.dev).
283
+ - `detect(project)`: the value that an existing project uses now.
284
+ When updating a project, the question starts at this value instead of `default`.
285
+
286
+ Each option is also a CLI flag, `--<layer>.<option>`:
287
+
288
+ ```bash
289
+ npx ember.nvp --layers eslint-bundled --eslint-bundled.preset nvp
290
+ ```
291
+
292
+ - `confirm` options are on with `--<layer>.<option>`, and off with `--no-<layer>.<option>`.
293
+ - `multiselect` options take the flag more than once, or a comma-separated list.
294
+ - `npx ember.nvp --help` lists every option.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember.nvp",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "ember project generator, alternative to the official blueprints, but more modern -- hopefully one day to upstream back in to ember-cli / official blueprints",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,7 @@
19
19
  "exports": "./src/index.js",
20
20
  "bin": "./src/cli/index.js",
21
21
  "dependencies": {
22
+ "@clack/core": "^1.4.2",
22
23
  "@clack/prompts": "^1.6.0",
23
24
  "babel-remove-types": "^2.0.0",
24
25
  "content-tag": "^4.2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullvoxpopuli/ember-rolldown",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Meta plugin for building ember v2 libraries/addons with rolldown & tsdown.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -46,10 +46,13 @@
46
46
  "@babel/core": "^7.28.10 || ^8.0.0",
47
47
  "@babel/plugin-transform-typescript": "^7.28.10 || ^8.0.0",
48
48
  "@embroider/core": "^4.4.2",
49
+ "@jridgewell/remapping": "^2.3.5",
49
50
  "@nullvoxpopuli/ember-build-tooling-utils": "workspace:*",
50
51
  "babel-plugin-ember-template-compilation": "^4.0.0",
51
52
  "content-tag": "^4.2.0",
52
- "decorator-transforms": "^2.4.0"
53
+ "decorator-transforms": "^2.4.0",
54
+ "get-tsconfig": "^4.14.3",
55
+ "magic-string": "^1.4.2"
53
56
  },
54
57
  "engines": {
55
58
  "node": ">= 24"
@@ -67,6 +70,7 @@
67
70
  }
68
71
  },
69
72
  "devDependencies": {
73
+ "@jridgewell/trace-mapping": "^0.3.31",
70
74
  "@tsdown/css": "^0.22.13",
71
75
  "rolldown": "^1.2.0",
72
76
  "tsdown": "^0.22.9",
@@ -1,3 +1,5 @@
1
+ import { originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
2
+ import { existsSync } from "node:fs";
1
3
  import { mkdtemp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
4
  import { tmpdir } from "node:os";
3
5
  import path from "node:path";
@@ -7,6 +9,10 @@ import { afterEach, describe, expect, it } from "vitest";
7
9
  import { ember } from "../index.ts";
8
10
 
9
11
  interface Dist {
12
+ /**
13
+ * The emitted directory.
14
+ */
15
+ dir: string;
10
16
  /**
11
17
  * Sorted dist-relative paths of every emitted file.
12
18
  */
@@ -71,7 +77,7 @@ async function buildFixture(files: Record<string, string>, entry: string[]): Pro
71
77
  });
72
78
 
73
79
  const distDir = path.join(dir, "dist");
74
- const dist: Dist = { files: [], contents: {} };
80
+ const dist: Dist = { dir: distDir, files: [], contents: {} };
75
81
 
76
82
  for (const entry of await readdir(distDir, { withFileTypes: true, recursive: true })) {
77
83
  if (!entry.isFile()) continue;
@@ -99,6 +105,41 @@ function printed(dist: Dist): string {
99
105
  .join("\n");
100
106
  }
101
107
 
108
+ /**
109
+ * Follows each declaration in a `.d.ts` through its declaration map,
110
+ * the way an editor does on "go to definition".
111
+ *
112
+ * One line per declaration: the source the map names, and that source's line.
113
+ */
114
+ async function traced(dist: Dist, file: string): Promise<string> {
115
+ const map = new TraceMap(await readFile(path.join(dist.dir, `${file}.map`), "utf8"));
116
+ const result: string[] = [];
117
+
118
+ for (const [index, line] of (dist.contents[file] ?? "").split("\n").entries()) {
119
+ const name = line.match(/\b(?:interface|class|const|function)\s+(\w+)/)?.[1];
120
+ if (!name) continue;
121
+
122
+ const { source, line: sourceLine } = originalPositionFor(map, {
123
+ line: index + 1,
124
+ column: line.indexOf(name),
125
+ });
126
+
127
+ if (!source || sourceLine === null) {
128
+ result.push(`${name} -> (unmapped)`);
129
+ continue;
130
+ }
131
+
132
+ const sourcePath = path.resolve(dist.dir, source);
133
+ const text = existsSync(sourcePath)
134
+ ? (await readFile(sourcePath, "utf8")).split("\n")[sourceLine - 1]?.trim()
135
+ : "(no such file)";
136
+
137
+ result.push(`${name} -> ${source}:${sourceLine} ${text}`);
138
+ }
139
+
140
+ return result.join("\n");
141
+ }
142
+
102
143
  describe("declarations (full ember() via tsdown)", () => {
103
144
  it("emits .d.ts for an entry that type-imports from a non-entry .gts", async () => {
104
145
  // Mirrors a design-system shape:
@@ -300,6 +341,69 @@ describe("declarations (full ember() via tsdown)", () => {
300
341
  `);
301
342
  });
302
343
 
344
+ it("maps declarations back to their .gts source lines", async () => {
345
+ // Declaration maps let a consumer's "go to definition" open the library's source.
346
+ //
347
+ // A .gts module reaches the declaration pipeline as a virtual .ts id,
348
+ // holding content-tag's output (which moves lines around).
349
+ // The maps must still name the .gts file, at its own line numbers,
350
+ // including for a .gts with no <template> to compile.
351
+ const dist = await buildFixture(
352
+ {
353
+ "src/index.ts": [
354
+ `export { default as Popup, type AfterTemplate, type PopupBlock } from './popup.gts';`,
355
+ `export { double } from './helpers.gts';`,
356
+ `export { default as Plain, type PlainArgs } from './plain.ts';`,
357
+ ].join("\n"),
358
+ "src/popup.gts": [
359
+ `import Component from '@glimmer/component';`,
360
+ ``,
361
+ `export interface PopupBlock {`,
362
+ ` open: () => void;`,
363
+ `}`,
364
+ ``,
365
+ `export default class Popup extends Component<{ Blocks: { default: [PopupBlock] } }> {`,
366
+ ` <template>`,
367
+ ` <div class="popup">`,
368
+ ` {{yield (hash open=this.open)}}`,
369
+ ` </div>`,
370
+ ` </template>`,
371
+ ``,
372
+ ` open = (): void => {};`,
373
+ `}`,
374
+ ``,
375
+ `export interface AfterTemplate {`,
376
+ ` after: boolean;`,
377
+ `}`,
378
+ ].join("\n"),
379
+ "src/helpers.gts": [
380
+ `export function double(value: number): number {`,
381
+ ` return value * 2;`,
382
+ `}`,
383
+ ].join("\n"),
384
+ "src/plain.ts": [
385
+ `export interface PlainArgs {`,
386
+ ` name: string;`,
387
+ `}`,
388
+ ``,
389
+ `export default class Plain {`,
390
+ ` args?: PlainArgs;`,
391
+ `}`,
392
+ ].join("\n"),
393
+ },
394
+ ["./src/index.ts"],
395
+ );
396
+
397
+ expect(await traced(dist, "index.d.ts")).toMatchInlineSnapshot(`
398
+ "PopupBlock -> ../src/popup.gts:3 export interface PopupBlock {
399
+ Popup -> ../src/popup.gts:7 export default class Popup extends Component<{ Blocks: { default: [PopupBlock] } }> {
400
+ AfterTemplate -> ../src/popup.gts:17 export interface AfterTemplate {
401
+ double -> ../src/helpers.gts:1 export function double(value: number): number {
402
+ PlainArgs -> ../src/plain.ts:1 export interface PlainArgs {
403
+ Plain -> ../src/plain.ts:5 export default class Plain {"
404
+ `);
405
+ });
406
+
303
407
  it("bundles co-located CSS (via @tsdown/css) without breaking declarations", async () => {
304
408
  // Ember components routinely import a co-located .css file.
305
409
  //
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
- import { dirname, resolve } from "node:path";
2
+ import { resolve } from "node:path";
3
3
 
4
+ import { parseTsconfig, type TsConfigJsonResolved } from "get-tsconfig";
4
5
  import type { Plugin } from "rolldown";
5
6
  import type { UserConfig } from "tsdown";
6
7
 
@@ -61,8 +62,11 @@ function resolveTsconfigPath(option: string | boolean | undefined): string | und
61
62
  * Isolated declarations then constrain exactly the code that gets declarations emitted,
62
63
  * and nothing else.
63
64
  *
64
- * Projects with no tsconfig (JavaScript libraries) or without the `typescript` package
65
- * have no declarations to emit, so there is nothing to check.
65
+ * Projects with no tsconfig (JavaScript libraries) have no declarations to emit,
66
+ * so there is nothing to check.
67
+ *
68
+ * The tsconfig is read without the `typescript` package.
69
+ * TypeScript 7 has no JavaScript API, and a TypeScript 7 library can have no TypeScript 6.
66
70
  *
67
71
  * `tsconfig: false` is different.
68
72
  * It is an explicit opt-out.
@@ -88,64 +92,66 @@ export function emberIsolatedDeclarations(): Plugin {
88
92
  emitsDeclarations = config.dts !== false;
89
93
  },
90
94
 
91
- async buildStart() {
92
- if (tsconfigOption === false) {
93
- if (!emitsDeclarations) return;
94
-
95
- // Without a tsconfig there is no `isolatedDeclarations`,
96
- // so tsdown falls back to the tsc-based declaration pipeline.
97
- //
98
- // That pipeline reads source files from disk,
99
- // and dies with "Source file not found" on every `.gts` / `.gjs`,
100
- // since those only exist compiled in the module graph.
101
- //
102
- // Say so here instead of leaving that to be decoded.
103
- this.error(
104
- `\`tsconfig: false\` cannot be combined with declaration emit.\n\n` +
105
- `Declarations are emitted by the isolated-declarations pipeline, which needs a ` +
106
- `tsconfig setting "compilerOptions.isolatedDeclarations": true -- it is the only ` +
107
- `pipeline that can see compiled .gts/.gjs modules.\n\n` +
108
- `Either point tsdown's \`tsconfig\` option at such a config, or set \`dts: false\` ` +
109
- `if this library ships no types.`,
110
- );
111
- }
112
-
113
- const tsconfigPath = resolveTsconfigPath(tsconfigOption);
114
-
115
- if (!tsconfigPath || !existsSync(tsconfigPath)) return;
116
-
117
- let ts: typeof import("typescript");
118
-
119
- try {
120
- ts = (await import("typescript")).default;
121
- } catch {
122
- return;
123
- }
124
-
125
- const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
126
-
127
- if (error) {
128
- // Unreadable / unparsable tsconfig.
129
- // tsc itself will report this with a better message than we can.
130
- return;
131
- }
132
-
133
- // Resolves `extends` chains, so the flag may come from a base config.
134
- const parsed = ts.parseJsonConfigFileContent(config, ts.sys, dirname(tsconfigPath));
135
-
136
- if (parsed.options.isolatedDeclarations !== true) {
137
- this.error(
138
- `${tsconfigPath} must set "compilerOptions.isolatedDeclarations": true.\n\n` +
139
- `Declarations for .gts/.gjs (template tag) modules can only be emitted by the ` +
140
- `isolated-declarations pipeline, which reads compiled modules from the bundler's ` +
141
- `module graph -- the tsc-based pipeline reads from disk and cannot see them.\n\n` +
142
- `Isolated declarations require exported values to have explicit type annotations, ` +
143
- `e.g. \`export const X: TOC<Sig> = <template>...\`.\n\n` +
144
- `If this tsconfig also covers dev-only code (a demo app, in-package tests) that ` +
145
- `should not be constrained this way, point tsdown's \`tsconfig\` option at a ` +
146
- `publish-only tsconfig that sets the flag and covers just the published sources.`,
147
- );
148
- }
95
+ buildStart: {
96
+ /**
97
+ * Before rolldown-plugin-dts, whose buildStart loads the `typescript` package
98
+ * when isolatedDeclarations is off.
99
+ *
100
+ * In a TypeScript 7 library that package is TypeScript 7, which has no JavaScript API,
101
+ * so it crashes before this message can show.
102
+ */
103
+ order: "pre",
104
+ async handler() {
105
+ if (tsconfigOption === false) {
106
+ if (!emitsDeclarations) return;
107
+
108
+ // Without a tsconfig there is no `isolatedDeclarations`,
109
+ // so tsdown falls back to the tsc-based declaration pipeline.
110
+ //
111
+ // That pipeline reads source files from disk,
112
+ // and dies with "Source file not found" on every `.gts` / `.gjs`,
113
+ // since those only exist compiled in the module graph.
114
+ //
115
+ // Say so here instead of leaving that to be decoded.
116
+ this.error(
117
+ `\`tsconfig: false\` cannot be combined with declaration emit.\n\n` +
118
+ `Declarations are emitted by the isolated-declarations pipeline, which needs a ` +
119
+ `tsconfig setting "compilerOptions.isolatedDeclarations": true -- it is the only ` +
120
+ `pipeline that can see compiled .gts/.gjs modules.\n\n` +
121
+ `Either point tsdown's \`tsconfig\` option at such a config, or set \`dts: false\` ` +
122
+ `if this library ships no types.`,
123
+ );
124
+ }
125
+
126
+ const tsconfigPath = resolveTsconfigPath(tsconfigOption);
127
+
128
+ if (!tsconfigPath || !existsSync(tsconfigPath)) return;
129
+
130
+ let tsconfig: TsConfigJsonResolved;
131
+
132
+ try {
133
+ // Resolves `extends` chains, so the flag may come from a base config.
134
+ tsconfig = parseTsconfig(tsconfigPath);
135
+ } catch {
136
+ // Unreadable / unparsable tsconfig.
137
+ // tsc itself will report this with a better message than we can.
138
+ return;
139
+ }
140
+
141
+ if (tsconfig.compilerOptions?.isolatedDeclarations !== true) {
142
+ this.error(
143
+ `${tsconfigPath} must set "compilerOptions.isolatedDeclarations": true.\n\n` +
144
+ `Declarations for .gts/.gjs (template tag) modules can only be emitted by the ` +
145
+ `isolated-declarations pipeline, which reads compiled modules from the bundler's ` +
146
+ `module graph -- the tsc-based pipeline reads from disk and cannot see them.\n\n` +
147
+ `Isolated declarations require exported values to have explicit type annotations, ` +
148
+ `e.g. \`export const X: TOC<Sig> = <template>...\`.\n\n` +
149
+ `If this tsconfig also covers dev-only code (a demo app, in-package tests) that ` +
150
+ `should not be constrained this way, point tsdown's \`tsconfig\` option at a ` +
151
+ `publish-only tsconfig that sets the flag and covers just the published sources.`,
152
+ );
153
+ }
154
+ },
149
155
  },
150
156
  };
151
157
  }