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.
Files changed (99) hide show
  1. package/.github/workflows/ci.yml +5 -1
  2. package/.release-plan.json +51 -9
  3. package/CHANGELOG.md +30 -0
  4. package/README.md +41 -28
  5. package/RELEASE.md +16 -14
  6. package/package.json +1 -1
  7. package/packages/build-tooling-utils/README.md +7 -12
  8. package/packages/build-tooling-utils/package.json +1 -1
  9. package/packages/build-tooling-utils/src/extensions.ts +4 -3
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
  11. package/packages/rolldown/README.md +138 -98
  12. package/packages/rolldown/index.ts +66 -28
  13. package/packages/rolldown/package.json +1 -1
  14. package/packages/rolldown/src/app-reexports.test.ts +4 -3
  15. package/packages/rolldown/src/app-reexports.ts +37 -27
  16. package/packages/rolldown/src/babel.test.ts +10 -5
  17. package/packages/rolldown/src/babel.ts +117 -72
  18. package/packages/rolldown/src/bundle.test.ts +149 -0
  19. package/packages/rolldown/src/bundle.ts +71 -0
  20. package/packages/rolldown/src/config.ts +42 -18
  21. package/packages/rolldown/src/dts.test.ts +42 -26
  22. package/packages/rolldown/src/externals.test.ts +16 -11
  23. package/packages/rolldown/src/externals.ts +45 -28
  24. package/packages/rolldown/src/isolated-declarations.ts +56 -43
  25. package/packages/rolldown/src/plugin-like.ts +18 -15
  26. package/packages/rolldown/src/published-types.test.ts +18 -13
  27. package/packages/rolldown/src/transform.test.ts +26 -17
  28. package/packages/rolldown/src/transform.ts +68 -48
  29. package/packages/vite/README.md +10 -11
  30. package/packages/vite/package.json +1 -1
  31. package/packages/vite/src/ember.ts +44 -36
  32. package/packages/vite/src/test-html.ts +13 -11
  33. package/src/bases/README.md +6 -3
  34. package/src/bases/minimal-app/app.test.ts +7 -4
  35. package/src/bases/minimal-app/files/app/app.ts +2 -3
  36. package/src/bases/minimal-app/files/app/router.ts +3 -3
  37. package/src/bases/minimal-app/index.js +10 -6
  38. package/src/bases/minimal-custom-element/files/README.md +95 -0
  39. package/src/bases/minimal-custom-element/files/package.json +47 -0
  40. package/src/bases/minimal-custom-element/files/src/components/counter.gts +27 -0
  41. package/src/bases/minimal-custom-element/files/src/element.ts +45 -0
  42. package/src/bases/minimal-custom-element/files/src/index.ts +2 -0
  43. package/src/bases/minimal-custom-element/files/src/register.ts +5 -0
  44. package/src/bases/minimal-custom-element/files/tsconfig.json +10 -0
  45. package/src/bases/minimal-custom-element/files/tsdown.config.js +7 -0
  46. package/src/bases/minimal-custom-element/index.js +212 -0
  47. package/src/bases/minimal-extension/files/README.md +13 -5
  48. package/src/bases/minimal-extension/files/app/app.ts +2 -3
  49. package/src/bases/minimal-extension/files/app/config.ts +5 -3
  50. package/src/bases/minimal-extension/index.js +16 -11
  51. package/src/bases/minimal-library/files/README.md +8 -7
  52. package/src/bases/minimal-library/index.js +21 -15
  53. package/src/cli/args.js +1 -1
  54. package/src/cli/generator.js +4 -0
  55. package/src/cli/index.js +23 -18
  56. package/src/cli/questions/project-type.js +17 -2
  57. package/src/cli/questions/write.js +12 -10
  58. package/src/layers/are-the-types-wrong/index.js +6 -6
  59. package/src/layers/github-actions/index.js +7 -4
  60. package/src/layers/inspector-support/index.js +2 -2
  61. package/src/layers/publint/index.js +2 -2
  62. package/src/layers/qunit/index.js +21 -17
  63. package/src/layers/readme/index.js +55 -4
  64. package/src/layers/renovate/index.js +5 -5
  65. package/src/layers/typescript/index.js +20 -13
  66. package/src/layers/vitest/index.js +8 -4
  67. package/src/utils/babel.js +3 -2
  68. package/src/utils/buffer.js +2 -2
  69. package/src/utils/cli.js +4 -4
  70. package/src/utils/fs.js +23 -14
  71. package/src/utils/inspector-support.js +22 -19
  72. package/src/utils/npm.js +15 -12
  73. package/src/utils/project-type.js +19 -0
  74. package/src/utils/project.js +14 -2
  75. package/src/utils/remove-types.js +2 -4
  76. package/src/utils/rewrite-imports.js +43 -31
  77. package/src/utils/stage.js +44 -33
  78. package/src/utils/tsdown-config.js +6 -5
  79. package/src/utils/types.ts +10 -9
  80. package/test/ember.nvp/base-custom-element.test.ts +440 -0
  81. package/test/ember.nvp/base-library.test.ts +6 -4
  82. package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
  83. package/test/ember.nvp/inspector-support.test.ts +4 -3
  84. package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
  85. package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
  86. package/test/ember.nvp/layer-qunit.test.ts +2 -2
  87. package/test/ember.nvp/layer-readme.test.ts +63 -3
  88. package/test/ember.nvp/layer-vitest.test.ts +16 -9
  89. package/test/ember.nvp/layers-load.test.ts +26 -0
  90. package/test/ember.nvp/library-src-fixtures.ts +7 -3
  91. package/test/ember.nvp/permutations/custom-element-1.test.ts +3 -0
  92. package/test/ember.nvp/permutations/custom-element-2.test.ts +3 -0
  93. package/test/ember.nvp/permutations/custom-element-checks.test.ts +3 -0
  94. package/test/ember.nvp/permutations/shared.ts +22 -12
  95. package/test/ember.nvp/publish-configs.test.ts +44 -31
  96. package/test/ember.nvp/rewrite-imports.test.ts +4 -3
  97. package/test/ember.nvp/scoped-css-library.test.ts +6 -4
  98. package/test/test-helpers/permutate.test.ts +3 -3
  99. package/test/test-helpers.ts +12 -7
@@ -7,9 +7,13 @@ import { afterEach, describe, expect, it } from "vitest";
7
7
  import { ember } from "../index.ts";
8
8
 
9
9
  interface Dist {
10
- /** Sorted dist-relative paths of every emitted file. */
10
+ /**
11
+ * Sorted dist-relative paths of every emitted file.
12
+ */
11
13
  files: string[];
12
- /** dist-relative path -> content, for every emitted .d.ts and .css file. */
14
+ /**
15
+ * dist-relative path -> content, for every emitted .d.ts and .css file.
16
+ */
13
17
  contents: Record<string, string>;
14
18
  }
15
19
 
@@ -22,12 +26,16 @@ afterEach(() => {
22
26
 
23
27
  /**
24
28
  * Drives `ember()` through a real tsdown build (the dts pipeline included).
25
- * `files` is a map of relative path -> source, written into a temp dir that
26
- * becomes the cwd for the build (the externals and isolated-declarations
27
- * plugins read package.json / tsconfig.json from cwd, like the tsdown CLI).
28
29
  *
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).
30
+ * `files` is a map of relative path -> source.
31
+ * It is written into a temp dir that becomes the cwd for the build.
32
+ * (the externals and isolated-declarations plugins read package.json / tsconfig.json
33
+ * from cwd, like the tsdown CLI)
34
+ *
35
+ * Returns:
36
+ * - the emitted file list
37
+ * - the full contents of every declaration and css file
38
+ * (js output is covered by transform.test.ts)
31
39
  */
32
40
  async function buildFixture(files: Record<string, string>, entry: string[]): Promise<Dist> {
33
41
  const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-dts-"));
@@ -81,7 +89,9 @@ async function buildFixture(files: Record<string, string>, entry: string[]): Pro
81
89
  return dist;
82
90
  }
83
91
 
84
- /** One printable document of every captured file, for a single snapshot. */
92
+ /**
93
+ * One printable document of every captured file, for a single snapshot.
94
+ */
85
95
  function printed(dist: Dist): string {
86
96
  return Object.entries(dist.contents)
87
97
  .sort(([a], [b]) => a.localeCompare(b))
@@ -91,10 +101,12 @@ function printed(dist: Dist): string {
91
101
 
92
102
  describe("declarations (full ember() via tsdown)", () => {
93
103
  it("emits .d.ts for an entry that type-imports from a non-entry .gts", async () => {
94
- // Mirrors a design-system shape: the component is in the runtime graph via
95
- // the index entry, while a second entry (a service) only *type*-imports
96
- // from it. The service's declaration then references the component's
97
- // (virtual) .d.ts, which only exists in the bundler's module graph.
104
+ // Mirrors a design-system shape:
105
+ // - the component is in the runtime graph via the index entry
106
+ // - a second entry (a service) only *type*-imports from it
107
+ //
108
+ // The service's declaration then references the component's (virtual) .d.ts,
109
+ // which only exists in the bundler's module graph.
98
110
  const dist = await buildFixture(
99
111
  {
100
112
  "src/index.ts": `export { default as Popup } from './popup.gts';`,
@@ -174,11 +186,12 @@ describe("declarations (full ember() via tsdown)", () => {
174
186
  });
175
187
 
176
188
  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.
189
+ // Nothing value-imports popup.gts, so its virtual module never enters the runtime graph.
190
+ // The only route to its declaration is the dts resolver loading it on demand.
191
+ //
192
+ // The declaration pipeline registers modules as their (virtual) source loads,
193
+ // so the import must resolve to the source id.
194
+ // A pre-rewritten `.d.ts` id skips that registration and dangles.
182
195
  const dist = await buildFixture(
183
196
  {
184
197
  "src/service.ts": [
@@ -233,10 +246,10 @@ describe("declarations (full ember() via tsdown)", () => {
233
246
  });
234
247
 
235
248
  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).
249
+ // A nested component type-imports a sibling from the parent directory.
250
+ //
251
+ // Its declaration then carries a relative source specifier for a module
252
+ // that exists only in the bundler's graph (never on disk, never in the runtime graph).
240
253
  const dist = await buildFixture(
241
254
  {
242
255
  "src/index.ts": `export { default as OptionList } from './components/select/option-list.gts';`,
@@ -288,11 +301,14 @@ describe("declarations (full ember() via tsdown)", () => {
288
301
  });
289
302
 
290
303
  it("bundles co-located CSS (via @tsdown/css) without breaking declarations", async () => {
291
- // Ember components routinely import a co-located .css file. Without CSS
292
- // handling the build dies on tsdown's css-guard — and because the failing
293
- // component module never loads, every declaration that references it
294
- // dangles (surfacing as misleading UNLOADABLE_DEPENDENCY errors on
295
- // <component>.d.ts). @tsdown/css makes both work.
304
+ // Ember components routinely import a co-located .css file.
305
+ //
306
+ // Without CSS handling, the build dies on tsdown's css-guard.
307
+ // And because the failing component module never loads,
308
+ // every declaration that references it dangles
309
+ // (surfacing as misleading UNLOADABLE_DEPENDENCY errors on <component>.d.ts).
310
+ //
311
+ // @tsdown/css makes both work.
296
312
  const dist = await buildFixture(
297
313
  {
298
314
  "src/index.ts": `export { default as Popup } from './popup.gts';`,
@@ -14,8 +14,9 @@ afterEach(() => {
14
14
  });
15
15
 
16
16
  /**
17
- * An ember-source stub whose `ember-addon.renamed-modules` provides
18
- * `@glimmer/runtime` (file-path keys, like the real manifest).
17
+ * An ember-source stub whose `ember-addon.renamed-modules` provides `@glimmer/runtime`.
18
+ *
19
+ * File-path keys, like the real manifest.
19
20
  */
20
21
  const EMBER_SOURCE_STUB = {
21
22
  name: "ember-source",
@@ -29,9 +30,11 @@ const EMBER_SOURCE_STUB = {
29
30
  };
30
31
 
31
32
  /**
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.
33
+ * Writes a fixture package and cds into it:
34
+ * - a manifest
35
+ * - an optional stub ember-source in its node_modules
36
+ *
37
+ * The plugin reads both from the library's own context, like the tsdown CLI.
35
38
  */
36
39
  async function fixture(manifest: object, { emberSource = true } = {}): Promise<string> {
37
40
  const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-externals-"));
@@ -50,7 +53,9 @@ async function fixture(manifest: object, { emberSource = true } = {}): Promise<s
50
53
  return dir;
51
54
  }
52
55
 
53
- /** Runs the plugin's own hooks directly against the current fixture. */
56
+ /**
57
+ * Runs the plugin's own hooks directly against the current fixture.
58
+ */
54
59
  function resolveWith(source: string): unknown {
55
60
  const plugin = emberExternals();
56
61
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
@@ -68,17 +73,17 @@ describe("emberExternals", () => {
68
73
  });
69
74
 
70
75
  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.
76
+ // Private API like @glimmer/runtime exists only inside the app's ember-source
77
+ // (its ember-addon.renamed-modules).
78
+ // @embroider/core's emberVirtualPackages doesn't list it.
74
79
  await fixture({});
75
80
  expect(resolveWith("@glimmer/runtime")).toBe(false);
76
81
  expect(resolveWith("@ember/-internals/glimmer")).toBe(false);
77
82
  });
78
83
 
79
84
  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.
85
+ // e.g. @glimmer/component is a real package a library may want bundled.
86
+ // Only declared deps / virtual packages / renamed-modules are external.
82
87
  await fixture({});
83
88
  expect(resolveWith("@glimmer/not-provided")).toBeUndefined();
84
89
  });
@@ -18,8 +18,8 @@ function readJsonSync(path: string) {
18
18
  }
19
19
 
20
20
  /**
21
- * Everything the library declares as a `dependency` or `peerDependency` is
22
- * resolvable by the consuming app, so we never bundle it.
21
+ * Everything the library declares as a `dependency` or `peerDependency`
22
+ * is resolvable by the consuming app, so we never bundle it.
23
23
  */
24
24
  function resolvableDependencies(): Set<string> {
25
25
  const deps = new Set<string>();
@@ -36,42 +36,56 @@ function resolvableDependencies(): Set<string> {
36
36
  }
37
37
 
38
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`).
39
+ * The modules ember-source provides by renaming them into itself.
42
40
  *
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.
41
+ * This is its `ember-addon.renamed-modules`, keyed by module file path:
42
+ * `@glimmer/runtime/index.js` → `ember-source/@glimmer/runtime/index.js`
47
43
  *
48
- * Resolved from the library's own dependency graph; empty when ember-source
49
- * isn't resolvable there.
44
+ * It is the authoritative list of ember's provided modules.
45
+ * It includes private API that @embroider/core's emberVirtualPackages
46
+ * doesn't cover (e.g. `@glimmer/runtime`).
47
+ *
48
+ * It deliberately does NOT include real packages like `@glimmer/component`,
49
+ * which a library may want bundled.
50
+ *
51
+ * Empty when ember-source isn't resolvable from the library.
50
52
  */
51
- function emberSourceRenamedModules(): Set<string> {
52
- const provided = new Set<string>();
53
-
53
+ export function emberSourceRenamedModules(): Record<string, string> {
54
54
  try {
55
55
  const require = createRequire(path.resolve("package.json"));
56
56
  const manifest = require("ember-source/package.json") as {
57
57
  "ember-addon"?: { "renamed-modules"?: Record<string, string> };
58
58
  };
59
59
 
60
- for (const key of Object.keys(manifest["ember-addon"]?.["renamed-modules"] ?? {})) {
61
- provided.add(key.replace(/\.js$/, "").replace(/\/index$/, ""));
62
- }
60
+ return manifest["ember-addon"]?.["renamed-modules"] ?? {};
63
61
  } catch {
64
62
  // The library doesn't have ember-source in its graph; nothing to provide.
63
+ return {};
64
+ }
65
+ }
66
+
67
+ /**
68
+ * The renamed modules as import specifiers
69
+ * `@glimmer/runtime/index.js` → `@glimmer/runtime`
70
+ */
71
+ function emberSourceProvidedModules(): Set<string> {
72
+ const provided = new Set<string>();
73
+
74
+ for (const key of Object.keys(emberSourceRenamedModules())) {
75
+ provided.add(key.replace(/\.js$/, "").replace(/\/index$/, ""));
65
76
  }
66
77
 
67
78
  return provided;
68
79
  }
69
80
 
70
81
  /**
71
- * Keeps the library's declared dependencies and the ember virtual packages
72
- * (things like `@ember/component`, `@glimmer/tracking`, the template compiler,
73
- * …) external, so the app that consumes the library resolves them instead of
74
- * the library bundling copies of them.
82
+ * Keeps external:
83
+ * - the library's declared dependencies
84
+ * - the ember virtual packages
85
+ * (things like `@ember/component`, `@glimmer/tracking`, the template compiler, …)
86
+ *
87
+ * So the app that consumes the library resolves them,
88
+ * instead of the library bundling copies of them.
75
89
  */
76
90
  export function emberExternals(): Plugin {
77
91
  let deps: Set<string>;
@@ -83,14 +97,14 @@ export function emberExternals(): Plugin {
83
97
  buildStart() {
84
98
  this.addWatchFile("package.json");
85
99
  deps = resolvableDependencies();
86
- renamedModules = emberSourceRenamedModules();
100
+ renamedModules = emberSourceProvidedModules();
87
101
  },
88
102
 
89
103
  resolveId: {
90
104
  order: "pre",
91
105
  handler(source) {
92
- // Anything with a protocol (`node:`, virtual modules, …) is not ours
93
- // to externalize.
106
+ // Anything with a protocol (`node:`, virtual modules, …)
107
+ // is not ours to externalize.
94
108
  if (source.includes(":")) {
95
109
  return null;
96
110
  }
@@ -113,10 +127,13 @@ export function emberExternals(): Plugin {
113
127
  }
114
128
 
115
129
  // 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.
130
+ // (`ember-addon.renamed-modules`).
131
+ //
132
+ // Private API like `@glimmer/runtime`,
133
+ // which only exists inside the app's ember-source.
134
+ //
135
+ // Matched on the full specifier:
136
+ // the renamed modules are module paths, not packages.
120
137
  if (renamedModules.has(source)) {
121
138
  return false;
122
139
  }
@@ -5,16 +5,16 @@ import type { Plugin } from "rolldown";
5
5
  import type { UserConfig } from "tsdown";
6
6
 
7
7
  /**
8
- * Resolve the tsconfig the build will actually use, mirroring tsdown's own
9
- * `tsconfig` option (`string | boolean`):
8
+ * Resolve the tsconfig the build will use.
10
9
  *
11
- * - `undefined` / `true`: `tsconfig.json` in the cwd.
12
- * - a path to a file: that file.
13
- * - a path to a directory: `tsconfig.json` inside it.
14
- * - `false`: no tsconfig at all.
10
+ * Mirrors tsdown's own `tsconfig` option (`string | boolean`):
11
+ * - `undefined` / `true`: `tsconfig.json` in the cwd
12
+ * - a path to a file: that file
13
+ * - a path to a directory: `tsconfig.json` inside it
14
+ * - `false`: no tsconfig at all
15
15
  *
16
- * Under plain rolldown there is no such option, so the cwd's `tsconfig.json`
17
- * is all there is.
16
+ * Under plain rolldown there is no such option,
17
+ * so the cwd's `tsconfig.json` is all there is.
18
18
  */
19
19
  function resolveTsconfigPath(option: string | boolean | undefined): string | undefined {
20
20
  if (option === false) return undefined;
@@ -33,33 +33,41 @@ function resolveTsconfigPath(option: string | boolean | undefined): string | und
33
33
  }
34
34
 
35
35
  /**
36
- * Errors the build when the tsconfig it builds with does not set
37
- * `isolatedDeclarations: true`.
36
+ * Errors the build when the tsconfig it builds with
37
+ * does not set `isolatedDeclarations: true`.
38
38
  *
39
- * This is required: declarations for `.gts`/`.gjs` (template tag) modules can
40
- * only be emitted by the isolated-declarations pipeline, which reads compiled
41
- * modules from the bundler's module graph. The `tsc`-based pipeline reads
42
- * source files from disk, where the compiled modules don't exist, and fails
43
- * with "Source file not found".
39
+ * This is required.
40
+ * Declarations for `.gts` / `.gjs` (template tag) modules can only be emitted
41
+ * by the isolated-declarations pipeline,
42
+ * which reads compiled modules from the bundler's module graph.
44
43
  *
45
- * Failing fast here -- rather than only when declaration emit happens to run
46
- * -- keeps the project honest: editors and `tsc`/`ember-tsc` then check the
47
- * same rules the build enforces (exported values need explicit type
48
- * annotations, e.g. `export const X: TOC<Sig> = <template>...`).
44
+ * The `tsc`-based pipeline reads source files from disk.
45
+ * The compiled modules don't exist there, so it fails with "Source file not found".
49
46
  *
50
- * The tsconfig checked is the one tsdown's `tsconfig` option points at, not
51
- * necessarily `tsconfig.json`. A library whose package also holds dev-only code
52
- * (a demo app, in-package tests) can therefore keep a permissive
53
- * `tsconfig.json` covering everything for editors and `tsc --noEmit`, and point
54
- * the build at a `tsconfig.publish.json` that covers only the publishable
55
- * sources -- isolated declarations then constrain exactly the code that gets
56
- * declarations emitted for it, and nothing else.
47
+ * Failing fast here, rather than only when declaration emit happens to run,
48
+ * keeps the project honest.
49
+ * Editors and `tsc` / `ember-tsc` then check the same rules the build enforces:
50
+ * exported values need explicit type annotations,
51
+ * e.g. `export const X: TOC<Sig> = <template>...`
57
52
  *
58
- * Projects with no tsconfig (JavaScript libraries) or without the `typescript`
59
- * package have no declarations to emit, so there is nothing to check.
60
- * `tsconfig: false` is different: it is an explicit opt-out, and combined with
61
- * declaration emit it produces a confusing "Source file not found" from the
62
- * tsc-based pipeline, so it errors here instead.
53
+ * The tsconfig checked is the one tsdown's `tsconfig` option points at,
54
+ * not necessarily `tsconfig.json`.
55
+ *
56
+ * So a library whose package also holds dev-only code (a demo app, in-package tests) can:
57
+ * - keep a permissive `tsconfig.json` covering everything,
58
+ * for editors and `tsc --noEmit`
59
+ * - point the build at a `tsconfig.publish.json` that covers only the publishable sources
60
+ *
61
+ * Isolated declarations then constrain exactly the code that gets declarations emitted,
62
+ * and nothing else.
63
+ *
64
+ * Projects with no tsconfig (JavaScript libraries) or without the `typescript` package
65
+ * have no declarations to emit, so there is nothing to check.
66
+ *
67
+ * `tsconfig: false` is different.
68
+ * It is an explicit opt-out.
69
+ * Combined with declaration emit, it produces a confusing "Source file not found"
70
+ * from the tsc-based pipeline, so it errors here instead.
63
71
  */
64
72
  export function emberIsolatedDeclarations(): Plugin {
65
73
  let tsconfigOption: string | boolean | undefined;
@@ -68,13 +76,15 @@ export function emberIsolatedDeclarations(): Plugin {
68
76
  return {
69
77
  name: "ember:isolated-declarations",
70
78
 
71
- // Not called under plain rolldown, which leaves `tsconfigOption` undefined
72
- // -- i.e. the cwd's tsconfig.json, the only thing it could mean there.
79
+ // Not called under plain rolldown, which leaves `tsconfigOption` undefined.
80
+ // That means the cwd's tsconfig.json, the only thing it could mean there.
73
81
  tsdownConfig(config: UserConfig) {
74
82
  tsconfigOption = config.tsconfig;
75
- // `emberConfig()` turns dts on unless the library turned it off, so an
76
- // explicit `false` is the only "no declarations" signal. Hook order
77
- // between plugins isn't guaranteed, so don't rely on it having run.
83
+ // `emberConfig()` turns dts on unless the library turned it off,
84
+ // so an explicit `false` is the only "no declarations" signal.
85
+ //
86
+ // Hook order between plugins isn't guaranteed,
87
+ // so don't rely on it having run.
78
88
  emitsDeclarations = config.dts !== false;
79
89
  },
80
90
 
@@ -82,11 +92,14 @@ export function emberIsolatedDeclarations(): Plugin {
82
92
  if (tsconfigOption === false) {
83
93
  if (!emitsDeclarations) return;
84
94
 
85
- // Without a tsconfig there is no `isolatedDeclarations`, so tsdown
86
- // falls back to the tsc-based declaration pipeline -- which reads
87
- // source files from disk and dies with "Source file not found" on
88
- // every `.gts`/`.gjs`, since those only exist compiled in the module
89
- // graph. Say so here instead of leaving that to be decoded.
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.
90
103
  this.error(
91
104
  `\`tsconfig: false\` cannot be combined with declaration emit.\n\n` +
92
105
  `Declarations are emitted by the isolated-declarations pipeline, which needs a ` +
@@ -112,8 +125,8 @@ export function emberIsolatedDeclarations(): Plugin {
112
125
  const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
113
126
 
114
127
  if (error) {
115
- // Unreadable/unparsable tsconfig: tsc itself will report this with a
116
- // better message than we can.
128
+ // Unreadable / unparsable tsconfig.
129
+ // tsc itself will report this with a better message than we can.
117
130
  return;
118
131
  }
119
132
 
@@ -1,22 +1,25 @@
1
1
  /**
2
- * What this package hands back: an object that rolldown and tsdown accept in
3
- * `plugins`.
2
+ * What this package hands back:
3
+ * an object that rolldown and tsdown accept in `plugins`.
4
4
  *
5
- * Structurally this *is* rolldown's `Plugin` -- `name` is its only required
6
- * member, every hook is optional -- so the two are assignable in both
7
- * directions: a consumer who imports rolldown's own type can still write
8
- * `const plugins: Plugin[] = ember()`, and tsdown's `plugins` option lists
9
- * `{ name: string }` among the shapes it takes.
5
+ * Structurally this *is* rolldown's `Plugin`.
6
+ * `name` is its only required member, every hook is optional.
10
7
  *
11
- * Declaring it here rather than re-exporting rolldown's type is what keeps
12
- * `rolldown` out of the published declarations. Otherwise it has to become a
13
- * runtime `dependency` purely so consumers can resolve a type: leave it a
14
- * devDependency and the declaration bundler inlines a ~200kB private copy of
15
- * rolldown's types instead, nominally distinct from the consumer's own.
8
+ * So the two are assignable in both directions:
9
+ * - a consumer who imports rolldown's own type can still write
10
+ * `const plugins: Plugin[] = ember()`
11
+ * - tsdown's `plugins` option lists `{ name: string }` among the shapes it takes
16
12
  *
17
- * The precise hook signatures still apply inside this package -- each plugin
18
- * function is typed with rolldown's real `Plugin` (a devDependency), and this
19
- * type only widens what crosses the package boundary.
13
+ * Declaring it here, rather than re-exporting rolldown's type,
14
+ * keeps `rolldown` out of the published declarations.
15
+ *
16
+ * Otherwise it has to become a runtime `dependency`, purely so consumers can resolve a type.
17
+ * Leave it a devDependency, and the declaration bundler inlines a ~200kB private copy
18
+ * of rolldown's types instead, nominally distinct from the consumer's own.
19
+ *
20
+ * The precise hook signatures still apply inside this package.
21
+ * Each plugin function is typed with rolldown's real `Plugin` (a devDependency).
22
+ * This type only widens what crosses the package boundary.
20
23
  */
21
24
  export interface RolldownPluginLike {
22
25
  name: string;
@@ -5,10 +5,13 @@ import { fileURLToPath } from "node:url";
5
5
  import { beforeAll, describe, expect, it } from "vitest";
6
6
 
7
7
  /**
8
- * This package's own published shape: consumers write their build config in
9
- * TypeScript, so `import { ember } from "@nullvoxpopuli/ember-rolldown"` has to
10
- * carry types. Without them it is an implicit `any` (TS7016), and a config file
11
- * silently stops being type-checked.
8
+ * This package's own published shape.
9
+ *
10
+ * Consumers write their build config in TypeScript,
11
+ * so `import { ember } from "@nullvoxpopuli/ember-rolldown"` has to carry types.
12
+ *
13
+ * Without them it is an implicit `any` (TS7016),
14
+ * and a config file silently stops being type-checked.
12
15
  */
13
16
  const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
17
 
@@ -43,8 +46,8 @@ async function declarations() {
43
46
 
44
47
  describe("published types", () => {
45
48
  beforeAll(async () => {
46
- // `clean` is on, but don't let these assertions depend on that: stale
47
- // artifacts from an earlier build satisfy every one of them on their own
49
+ // `clean` is on, but don't let these assertions depend on that.
50
+ // Stale artifacts from an earlier build satisfy every one of them on their own.
48
51
  await rm(path.join(packageDir, "dist"), { recursive: true, force: true });
49
52
  await execa("pnpm", ["build"], { cwd: packageDir });
50
53
  }, 120_000);
@@ -66,14 +69,16 @@ describe("published types", () => {
66
69
 
67
70
  /**
68
71
  * A declaration may only reach for packages a consumer is guaranteed to have.
69
- * Reference a devDependency and there are two ways to lose, both quiet:
70
- * the specifier survives and consumers can't resolve it (`skipLibCheck` hides
71
- * that, degrading the type to an error type), or the declaration bundler
72
- * inlines the whole type surface — a ~200kB copy of rolldown's types, in the
73
- * case that prompted this test, nominally distinct from the consumer's own.
74
72
  *
75
- * `ember()` returns plugins, so `RolldownPluginLike` exists to keep rolldown
76
- * on the devDependency side of that line.
73
+ * Reference a devDependency, and there are two ways to lose, both quiet:
74
+ * - the specifier survives and consumers can't resolve it
75
+ * (`skipLibCheck` hides that, degrading the type to an error type)
76
+ * - the declaration bundler inlines the whole type surface
77
+ * (a ~200kB copy of rolldown's types, in the case that prompted this test,
78
+ * nominally distinct from the consumer's own)
79
+ *
80
+ * `ember()` returns plugins,
81
+ * so `RolldownPluginLike` exists to keep rolldown on the devDependency side of that line.
77
82
  */
78
83
  it("declarations only reference packages consumers have", async () => {
79
84
  const manifest = await readManifest();
@@ -8,10 +8,15 @@ import { describe, expect, it } from "vitest";
8
8
  import { emberTransform } from "./transform.ts";
9
9
 
10
10
  /**
11
- * Drives the plugin through a real rolldown build. `files` is a map of
12
- * relative path -> source; the build entry is `index.ts`. Every bare (package)
13
- * specifier is marked external so the build only exercises the local
14
- * `.gts`/`.gjs`/`.ts` handling and doesn't need real dependencies installed.
11
+ * Drives the plugin through a real rolldown build.
12
+ *
13
+ * `files` is a map of relative path -> source.
14
+ * The build entry is `index.ts`.
15
+ *
16
+ * Every bare (package) specifier is marked external,
17
+ * so the build only exercises the local `.gts` / `.gjs` / `.ts` handling
18
+ * and doesn't need real dependencies installed.
19
+ *
15
20
  * Returns the concatenated code of every emitted chunk.
16
21
  */
17
22
  async function bundle(
@@ -45,8 +50,8 @@ async function bundle(
45
50
  .map((chunk) => chunk.code)
46
51
  .join("\n");
47
52
 
48
- // rolldown's `//#region <path>` comments reference the random temp dir;
49
- // collapse them to the bare filename so snapshots stay deterministic.
53
+ // rolldown's `//#region <path>` comments reference the random temp dir.
54
+ // Collapse them to the bare filename so snapshots stay deterministic.
50
55
  return code.replace(/(\/\/#region ).*\/([^/\n]+)$/gm, "$1$2");
51
56
  }
52
57
 
@@ -180,11 +185,13 @@ describe("emberTransform (full plugin via rolldown)", () => {
180
185
 
181
186
  it("resolves an absolute .gts specifier imported from a plugin's virtual module", async () => {
182
187
  /**
183
- * The shape a code-generating plugin emits: a virtual module (id prefixed
184
- * with `\0`, so it exists nowhere on disk) whose generated source imports
185
- * real files by absolute path, because it has no directory to be relative
186
- * to. `path.dirname` of such an id is `"\0."`, so the specifier must not be
187
- * resolved against the importer's directory.
188
+ * The shape a code-generating plugin emits:
189
+ * a virtual module (id prefixed with `\0`, so it exists nowhere on disk)
190
+ * whose generated source imports real files by absolute path,
191
+ * because it has no directory to be relative to.
192
+ *
193
+ * `path.dirname` of such an id is `"\0."`,
194
+ * so the specifier must not be resolved against the importer's directory.
188
195
  */
189
196
  const registry = (dir: string): Plugin[] => {
190
197
  const source = "./registry?virtual=views";
@@ -225,10 +232,12 @@ describe("emberTransform (full plugin via rolldown)", () => {
225
232
  it("compiles a .gts module used directly as an entry (no importer)", async () => {
226
233
  const code = await bundle(
227
234
  {
228
- // Both an entry .gts and a .ts entry importing it: the shared module
229
- // must resolve to ONE id (the entry resolution realpaths, matching
230
- // rolldown's own resolver) so it lands in the entry chunk and the
231
- // index chunk imports it, rather than duplicating the code.
235
+ // Both an entry .gts and a .ts entry importing it.
236
+ //
237
+ // The shared module must resolve to ONE id
238
+ // (the entry resolution realpaths, matching rolldown's own resolver),
239
+ // so it lands in the entry chunk and the index chunk imports it,
240
+ // rather than duplicating the code.
232
241
  "index.ts": `export { default as Foo } from './foo.gts';`,
233
242
  "foo.gts": [
234
243
  `import Component from '@glimmer/component';`,
@@ -299,8 +308,8 @@ describe("emberTransform (full plugin via rolldown)", () => {
299
308
 
300
309
  const chunk = output.find((entry) => entry.type === "chunk" && "map" in entry);
301
310
  expect(chunk).toBeDefined();
302
- // The composed map traces through the rewrite back to the original .gts
303
- // source (content-tag's map survives the specifier-rewrite pass).
311
+ // The composed map traces through the rewrite back to the original .gts source.
312
+ // (content-tag's map survives the specifier-rewrite pass)
304
313
  const sources = (chunk as { map?: { sources: string[] } }).map?.sources ?? [];
305
314
  expect(sources.some((source) => source.endsWith("foo.gts"))).toBe(true);
306
315
  });