ember.nvp 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +5 -1
- package/.release-plan.json +51 -9
- package/CHANGELOG.md +30 -0
- package/README.md +41 -28
- package/RELEASE.md +16 -14
- package/package.json +1 -1
- package/packages/build-tooling-utils/README.md +7 -12
- package/packages/build-tooling-utils/package.json +1 -1
- package/packages/build-tooling-utils/src/extensions.ts +4 -3
- package/packages/build-tooling-utils/src/maybe-babel.ts +18 -15
- package/packages/rolldown/README.md +138 -98
- package/packages/rolldown/index.ts +66 -28
- package/packages/rolldown/package.json +1 -1
- package/packages/rolldown/src/app-reexports.test.ts +4 -3
- package/packages/rolldown/src/app-reexports.ts +37 -27
- package/packages/rolldown/src/babel.test.ts +10 -5
- package/packages/rolldown/src/babel.ts +117 -72
- package/packages/rolldown/src/bundle.test.ts +149 -0
- package/packages/rolldown/src/bundle.ts +71 -0
- package/packages/rolldown/src/config.ts +42 -18
- package/packages/rolldown/src/dts.test.ts +42 -26
- package/packages/rolldown/src/externals.test.ts +16 -11
- package/packages/rolldown/src/externals.ts +45 -28
- package/packages/rolldown/src/isolated-declarations.ts +56 -43
- package/packages/rolldown/src/plugin-like.ts +18 -15
- package/packages/rolldown/src/published-types.test.ts +18 -13
- package/packages/rolldown/src/transform.test.ts +26 -17
- package/packages/rolldown/src/transform.ts +68 -48
- package/packages/vite/README.md +10 -11
- package/packages/vite/package.json +1 -1
- package/packages/vite/src/ember.ts +44 -36
- package/packages/vite/src/test-html.ts +13 -11
- package/src/bases/README.md +6 -3
- package/src/bases/minimal-app/app.test.ts +7 -4
- package/src/bases/minimal-app/files/app/app.ts +2 -3
- package/src/bases/minimal-app/files/app/router.ts +3 -3
- package/src/bases/minimal-app/index.js +10 -6
- package/src/bases/minimal-custom-element/files/README.md +95 -0
- package/src/bases/minimal-custom-element/files/package.json +47 -0
- package/src/bases/minimal-custom-element/files/src/components/counter.gts +27 -0
- package/src/bases/minimal-custom-element/files/src/element.ts +45 -0
- package/src/bases/minimal-custom-element/files/src/index.ts +2 -0
- package/src/bases/minimal-custom-element/files/src/register.ts +5 -0
- package/src/bases/minimal-custom-element/files/tsconfig.json +10 -0
- package/src/bases/minimal-custom-element/files/tsdown.config.js +7 -0
- package/src/bases/minimal-custom-element/index.js +212 -0
- package/src/bases/minimal-extension/files/README.md +13 -5
- package/src/bases/minimal-extension/files/app/app.ts +2 -3
- package/src/bases/minimal-extension/files/app/config.ts +5 -3
- package/src/bases/minimal-extension/index.js +16 -11
- package/src/bases/minimal-library/files/README.md +8 -7
- package/src/bases/minimal-library/index.js +21 -15
- package/src/cli/args.js +1 -1
- package/src/cli/generator.js +4 -0
- package/src/cli/index.js +23 -18
- package/src/cli/questions/project-type.js +17 -2
- package/src/cli/questions/write.js +12 -10
- package/src/layers/are-the-types-wrong/index.js +6 -6
- package/src/layers/github-actions/index.js +7 -4
- package/src/layers/inspector-support/index.js +2 -2
- package/src/layers/publint/index.js +2 -2
- package/src/layers/qunit/index.js +21 -17
- package/src/layers/readme/index.js +55 -4
- package/src/layers/renovate/index.js +5 -5
- package/src/layers/typescript/index.js +20 -13
- package/src/layers/vitest/index.js +8 -4
- package/src/utils/babel.js +3 -2
- package/src/utils/buffer.js +2 -2
- package/src/utils/cli.js +4 -4
- package/src/utils/fs.js +23 -14
- package/src/utils/inspector-support.js +22 -19
- package/src/utils/npm.js +15 -12
- package/src/utils/project-type.js +19 -0
- package/src/utils/project.js +14 -2
- package/src/utils/remove-types.js +2 -4
- package/src/utils/rewrite-imports.js +43 -31
- package/src/utils/stage.js +44 -33
- package/src/utils/tsdown-config.js +6 -5
- package/src/utils/types.ts +10 -9
- package/test/ember.nvp/base-custom-element.test.ts +440 -0
- package/test/ember.nvp/base-library.test.ts +6 -4
- package/test/ember.nvp/generator-replace-or-update.test.ts +6 -4
- package/test/ember.nvp/inspector-support.test.ts +4 -3
- package/test/ember.nvp/layer-inspector-support.test.ts +2 -3
- package/test/ember.nvp/layer-publish-checks.test.ts +2 -3
- package/test/ember.nvp/layer-qunit.test.ts +2 -2
- package/test/ember.nvp/layer-readme.test.ts +63 -3
- package/test/ember.nvp/layer-vitest.test.ts +16 -9
- package/test/ember.nvp/layers-load.test.ts +26 -0
- package/test/ember.nvp/library-src-fixtures.ts +7 -3
- package/test/ember.nvp/permutations/custom-element-1.test.ts +3 -0
- package/test/ember.nvp/permutations/custom-element-2.test.ts +3 -0
- package/test/ember.nvp/permutations/custom-element-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +22 -12
- package/test/ember.nvp/publish-configs.test.ts +44 -31
- package/test/ember.nvp/rewrite-imports.test.ts +4 -3
- package/test/ember.nvp/scoped-css-library.test.ts +6 -4
- package/test/test-helpers/permutate.test.ts +3 -3
- package/test/test-helpers.ts +12 -7
|
@@ -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
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Sorted dist-relative paths of every emitted file.
|
|
12
|
+
*/
|
|
11
13
|
files: string[];
|
|
12
|
-
/**
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
/**
|
|
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:
|
|
95
|
-
// the
|
|
96
|
-
//
|
|
97
|
-
//
|
|
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
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// their (virtual) source loads,
|
|
181
|
-
//
|
|
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
|
-
//
|
|
238
|
-
//
|
|
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.
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
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
|
-
*
|
|
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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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`
|
|
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
|
|
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
|
|
44
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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():
|
|
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
|
-
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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 =
|
|
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, …)
|
|
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`)
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
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
|
|
9
|
-
* `tsconfig` option (`string | boolean`):
|
|
8
|
+
* Resolve the tsconfig the build will use.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* - a path to a
|
|
14
|
-
* -
|
|
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,
|
|
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
|
|
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
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
//
|
|
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,
|
|
76
|
-
// explicit `false` is the only "no declarations" signal.
|
|
77
|
-
//
|
|
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`,
|
|
86
|
-
// falls back to the tsc-based declaration pipeline
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
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
|
|
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:
|
|
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
|
|
6
|
-
* member, every hook is optional
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
//
|
|
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:
|
|
184
|
-
* with `\0`, so it exists nowhere on disk)
|
|
185
|
-
* real files by absolute path,
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
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
|
-
//
|
|
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
|
});
|