ember.nvp 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +6 -0
- package/.release-plan.json +11 -15
- package/CHANGELOG.md +47 -0
- package/package.json +1 -1
- package/packages/rolldown/README.md +101 -1
- package/packages/rolldown/package.json +13 -4
- package/packages/rolldown/rolldown.config.js +5 -3
- package/packages/rolldown/src/app-reexports.test.ts +220 -0
- package/packages/rolldown/src/app-reexports.ts +152 -0
- package/packages/rolldown/src/babel.test.ts +17 -0
- package/packages/rolldown/src/babel.ts +21 -3
- package/packages/rolldown/src/config.ts +1 -1
- package/packages/rolldown/src/dts.test.ts +366 -0
- package/packages/rolldown/src/transform.test.ts +208 -0
- package/packages/rolldown/src/transform.ts +64 -31
- package/src/bases/minimal-extension/extension.test.ts +251 -0
- package/src/bases/minimal-extension/files/.env.development +8 -0
- package/src/bases/minimal-extension/files/README.md +36 -0
- package/src/bases/minimal-extension/files/app/app.ts +21 -0
- package/src/bases/minimal-extension/files/app/boot.ts +8 -0
- package/src/bases/minimal-extension/files/app/config.ts +22 -0
- package/src/bases/minimal-extension/files/app/router.ts +10 -0
- package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
- package/src/bases/minimal-extension/files/babel.config.js +49 -0
- package/src/bases/minimal-extension/files/index.html +17 -0
- package/src/bases/minimal-extension/files/package.json +58 -0
- package/src/bases/minimal-extension/files/public/manifest.json +9 -0
- package/src/bases/minimal-extension/files/tsconfig.json +13 -0
- package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
- package/src/bases/minimal-extension/index.js +142 -0
- package/src/cli/generator.js +4 -0
- package/src/cli/questions/project-type.js +9 -2
- package/src/layers/renovate/index.js +4 -3
- package/src/layers/typescript/index.js +6 -0
- package/src/utils/types.ts +1 -1
- package/test/ember.nvp/base-library.test.ts +39 -32
- package/test/ember.nvp/layer-publish-checks.test.ts +4 -13
- package/test/ember.nvp/layer-qunit.test.ts +13 -30
- package/test/ember.nvp/layer-vitest.test.ts +4 -18
- package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
- package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +68 -19
- package/test/ember.nvp/scoped-css-library.test.ts +211 -0
- package/test/test-helpers.ts +25 -30
- package/vitest.config.js +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { emberBabel } from "./babel.ts";
|
|
4
|
+
|
|
5
|
+
describe("emberBabel", () => {
|
|
6
|
+
it("rejects templateTransforms combined with a config file", () => {
|
|
7
|
+
expect(() =>
|
|
8
|
+
emberBabel({ configFile: "./babel.config.js", templateTransforms: ["some-transform"] }),
|
|
9
|
+
).toThrow(/pass the transforms/);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("allows templateTransforms when config files are opted out", () => {
|
|
13
|
+
expect(() =>
|
|
14
|
+
emberBabel({ configFile: false, templateTransforms: ["some-transform"] }),
|
|
15
|
+
).not.toThrow();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -4,6 +4,7 @@ import templateCompilation from "babel-plugin-ember-template-compilation";
|
|
|
4
4
|
import decoratorTransforms from "decorator-transforms";
|
|
5
5
|
import { loadPartialConfigSync } from "@babel/core";
|
|
6
6
|
import type { PluginItem } from "@babel/core";
|
|
7
|
+
import type { Transform } from "babel-plugin-ember-template-compilation";
|
|
7
8
|
import type { Plugin } from "rolldown";
|
|
8
9
|
|
|
9
10
|
export interface BabelOptions {
|
|
@@ -24,6 +25,15 @@ export interface BabelOptions {
|
|
|
24
25
|
* Extra babel plugins to run, appended after the config's plugins.
|
|
25
26
|
*/
|
|
26
27
|
plugins?: PluginItem[];
|
|
28
|
+
/**
|
|
29
|
+
* Template AST transforms (e.g. ember-scoped-css's `scopedCSS.template()`)
|
|
30
|
+
* passed to the default `babel-plugin-ember-template-compilation` entry.
|
|
31
|
+
*
|
|
32
|
+
* Only valid without a babel config file: with your own config, you list
|
|
33
|
+
* `babel-plugin-ember-template-compilation` yourself, so its `transforms`
|
|
34
|
+
* belong there.
|
|
35
|
+
*/
|
|
36
|
+
templateTransforms?: Transform[];
|
|
27
37
|
/**
|
|
28
38
|
* Opt additional files into babel (see maybeBabel's `filter`). Use this to
|
|
29
39
|
* cover addons that ship code needing babel (e.g. `ember-concurrency`).
|
|
@@ -63,7 +73,7 @@ function detectConfigFile(): string | undefined {
|
|
|
63
73
|
* consuming app resolves it (the library keeps `decorator-transforms` as
|
|
64
74
|
* a real dependency).
|
|
65
75
|
*/
|
|
66
|
-
function defaultPlugins(): PluginItem[] {
|
|
76
|
+
function defaultPlugins(templateTransforms?: Transform[]): PluginItem[] {
|
|
67
77
|
return [
|
|
68
78
|
[
|
|
69
79
|
transformTypeScript,
|
|
@@ -73,7 +83,7 @@ function defaultPlugins(): PluginItem[] {
|
|
|
73
83
|
allowDeclareFields: true,
|
|
74
84
|
},
|
|
75
85
|
],
|
|
76
|
-
[templateCompilation, { targetFormat: "hbs" }],
|
|
86
|
+
[templateCompilation, { targetFormat: "hbs", transforms: templateTransforms ?? [] }],
|
|
77
87
|
[
|
|
78
88
|
decoratorTransforms,
|
|
79
89
|
{
|
|
@@ -118,6 +128,14 @@ export function emberBabel(options: BabelOptions = {}): Plugin {
|
|
|
118
128
|
};
|
|
119
129
|
|
|
120
130
|
if (configFile) {
|
|
131
|
+
if (options.templateTransforms?.length) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`\`babel.templateTransforms\` cannot be combined with a babel config file (found ${configFile}). ` +
|
|
134
|
+
`Your config lists babel-plugin-ember-template-compilation itself, so pass the transforms ` +
|
|
135
|
+
`to its \`transforms\` option there instead.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
return maybeBabel({
|
|
122
140
|
...shared,
|
|
123
141
|
configFile,
|
|
@@ -131,6 +149,6 @@ export function emberBabel(options: BabelOptions = {}): Plugin {
|
|
|
131
149
|
// for one -- we provide the whole plugin list inline.
|
|
132
150
|
configFile: false,
|
|
133
151
|
babelrc: false,
|
|
134
|
-
plugins: defaultPlugins().concat(options.plugins ?? []),
|
|
152
|
+
plugins: defaultPlugins(options.templateTransforms).concat(options.plugins ?? []),
|
|
135
153
|
}) as unknown as Plugin;
|
|
136
154
|
}
|
|
@@ -34,7 +34,7 @@ export function emberConfig(): TsdownPlugin {
|
|
|
34
34
|
tsdownConfig(config: UserConfig) {
|
|
35
35
|
config.sourcemap ??= true;
|
|
36
36
|
config.clean ??= true;
|
|
37
|
-
config.dts ??= true;
|
|
37
|
+
config.dts ??= { sourcemap: true };
|
|
38
38
|
config.outExtensions ??= () => ({ js: ".js", dts: ".d.ts" });
|
|
39
39
|
config.report ??= false;
|
|
40
40
|
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { build } from "tsdown";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { ember } from "../index.ts";
|
|
8
|
+
|
|
9
|
+
interface Dist {
|
|
10
|
+
/** Sorted dist-relative paths of every emitted file. */
|
|
11
|
+
files: string[];
|
|
12
|
+
/** dist-relative path -> content, for every emitted .d.ts and .css file. */
|
|
13
|
+
contents: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let restoreCwd: (() => void) | undefined;
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
restoreCwd?.();
|
|
20
|
+
restoreCwd = undefined;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 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
|
+
* Returns the emitted file list plus the full contents of every declaration
|
|
30
|
+
* and css file (js output is covered by transform.test.ts).
|
|
31
|
+
*/
|
|
32
|
+
async function buildFixture(files: Record<string, string>, entry: string[]): Promise<Dist> {
|
|
33
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-dts-"));
|
|
34
|
+
|
|
35
|
+
const withDefaults: Record<string, string> = {
|
|
36
|
+
"package.json": JSON.stringify({
|
|
37
|
+
name: "fixture",
|
|
38
|
+
version: "0.0.0",
|
|
39
|
+
type: "module",
|
|
40
|
+
exports: { "./*": { types: "./dist/*.d.ts", default: "./dist/*.js" } },
|
|
41
|
+
}),
|
|
42
|
+
"tsconfig.json": JSON.stringify({
|
|
43
|
+
compilerOptions: { isolatedDeclarations: true, declaration: true },
|
|
44
|
+
}),
|
|
45
|
+
...files,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (const [relative, source] of Object.entries(withDefaults)) {
|
|
49
|
+
const full = path.join(dir, relative);
|
|
50
|
+
await mkdir(path.dirname(full), { recursive: true });
|
|
51
|
+
await writeFile(full, source, "utf8");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const previousCwd = process.cwd();
|
|
55
|
+
process.chdir(dir);
|
|
56
|
+
restoreCwd = () => process.chdir(previousCwd);
|
|
57
|
+
|
|
58
|
+
await build({
|
|
59
|
+
entry,
|
|
60
|
+
config: false,
|
|
61
|
+
logLevel: "silent",
|
|
62
|
+
plugins: [ember()],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const distDir = path.join(dir, "dist");
|
|
66
|
+
const dist: Dist = { files: [], contents: {} };
|
|
67
|
+
|
|
68
|
+
for (const entry of await readdir(distDir, { withFileTypes: true, recursive: true })) {
|
|
69
|
+
if (!entry.isFile()) continue;
|
|
70
|
+
|
|
71
|
+
const relative = path.relative(distDir, path.join(entry.parentPath, entry.name));
|
|
72
|
+
dist.files.push(relative);
|
|
73
|
+
|
|
74
|
+
if (relative.endsWith(".d.ts") || relative.endsWith(".css")) {
|
|
75
|
+
dist.contents[relative] = await readFile(path.join(distDir, relative), "utf8");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
dist.files.sort();
|
|
80
|
+
|
|
81
|
+
return dist;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One printable document of every captured file, for a single snapshot. */
|
|
85
|
+
function printed(dist: Dist): string {
|
|
86
|
+
return Object.entries(dist.contents)
|
|
87
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
88
|
+
.map(([file, content]) => `//=== ${file}\n${content}`)
|
|
89
|
+
.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("declarations (full ember() via tsdown)", () => {
|
|
93
|
+
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.
|
|
98
|
+
const dist = await buildFixture(
|
|
99
|
+
{
|
|
100
|
+
"src/index.ts": `export { default as Popup } from './popup.gts';`,
|
|
101
|
+
"src/service.ts": [
|
|
102
|
+
`import type { PopupBlock } from './popup.gts';`,
|
|
103
|
+
``,
|
|
104
|
+
`export class TooltipService {`,
|
|
105
|
+
` registerPopup(context: PopupBlock): void {`,
|
|
106
|
+
` void context;`,
|
|
107
|
+
` }`,
|
|
108
|
+
`}`,
|
|
109
|
+
].join("\n"),
|
|
110
|
+
"src/popup.gts": [
|
|
111
|
+
`import Component from '@glimmer/component';`,
|
|
112
|
+
``,
|
|
113
|
+
`export interface PopupBlock {`,
|
|
114
|
+
` open: () => void;`,
|
|
115
|
+
`}`,
|
|
116
|
+
``,
|
|
117
|
+
`export interface PopupSignature {`,
|
|
118
|
+
` Element: HTMLDivElement;`,
|
|
119
|
+
` Blocks: { default: [PopupBlock] };`,
|
|
120
|
+
`}`,
|
|
121
|
+
``,
|
|
122
|
+
`export default class Popup extends Component<PopupSignature> {`,
|
|
123
|
+
` <template>popup</template>`,
|
|
124
|
+
`}`,
|
|
125
|
+
].join("\n"),
|
|
126
|
+
},
|
|
127
|
+
["./src/index.ts", "./src/service.ts"],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
131
|
+
[
|
|
132
|
+
"index.d.ts",
|
|
133
|
+
"index.js",
|
|
134
|
+
"index.js.map",
|
|
135
|
+
"popup-CjMmUEVb.d.ts",
|
|
136
|
+
"popup-CjMmUEVb.d.ts.map",
|
|
137
|
+
"service.d.ts",
|
|
138
|
+
"service.d.ts.map",
|
|
139
|
+
"service.js",
|
|
140
|
+
"service.js.map",
|
|
141
|
+
]
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
145
|
+
"//=== index.d.ts
|
|
146
|
+
import { t as Popup } from "./popup-CjMmUEVb.js";
|
|
147
|
+
export { Popup };
|
|
148
|
+
//=== popup-CjMmUEVb.d.ts
|
|
149
|
+
import Component from "@glimmer/component";
|
|
150
|
+
//#region src/popup.d.ts
|
|
151
|
+
interface PopupBlock {
|
|
152
|
+
open: () => void;
|
|
153
|
+
}
|
|
154
|
+
interface PopupSignature {
|
|
155
|
+
Element: HTMLDivElement;
|
|
156
|
+
Blocks: {
|
|
157
|
+
default: [PopupBlock];
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
declare class Popup extends Component<PopupSignature> {}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { PopupBlock as n, Popup as t };
|
|
163
|
+
//# sourceMappingURL=popup-CjMmUEVb.d.ts.map
|
|
164
|
+
//=== service.d.ts
|
|
165
|
+
import { n as PopupBlock } from "./popup-CjMmUEVb.js";
|
|
166
|
+
//#region src/service.d.ts
|
|
167
|
+
declare class TooltipService {
|
|
168
|
+
registerPopup(context: PopupBlock): void;
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { TooltipService };
|
|
172
|
+
//# sourceMappingURL=service.d.ts.map"
|
|
173
|
+
`);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("emits .d.ts when a .gts module is only ever type-imported", async () => {
|
|
177
|
+
// Nothing value-imports popup.gts, so its virtual module never enters the
|
|
178
|
+
// runtime graph: the only route to its declaration is the dts resolver
|
|
179
|
+
// loading it on demand. The declaration pipeline registers modules as
|
|
180
|
+
// their (virtual) source loads, so the import must resolve to the source
|
|
181
|
+
// id — a pre-rewritten `.d.ts` id skips that registration and dangles.
|
|
182
|
+
const dist = await buildFixture(
|
|
183
|
+
{
|
|
184
|
+
"src/service.ts": [
|
|
185
|
+
`import type { PopupBlock } from './components/popup.gts';`,
|
|
186
|
+
``,
|
|
187
|
+
`export class TooltipService {`,
|
|
188
|
+
` registerPopup(context: PopupBlock): void {`,
|
|
189
|
+
` void context;`,
|
|
190
|
+
` }`,
|
|
191
|
+
`}`,
|
|
192
|
+
].join("\n"),
|
|
193
|
+
"src/components/popup.gts": [
|
|
194
|
+
`import Component from '@glimmer/component';`,
|
|
195
|
+
``,
|
|
196
|
+
`export interface PopupBlock {`,
|
|
197
|
+
` open: () => void;`,
|
|
198
|
+
`}`,
|
|
199
|
+
``,
|
|
200
|
+
`export default class Popup extends Component {`,
|
|
201
|
+
` <template>popup</template>`,
|
|
202
|
+
`}`,
|
|
203
|
+
].join("\n"),
|
|
204
|
+
},
|
|
205
|
+
["./src/service.ts"],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
209
|
+
[
|
|
210
|
+
"service.d.ts",
|
|
211
|
+
"service.d.ts.map",
|
|
212
|
+
"service.js",
|
|
213
|
+
"service.js.map",
|
|
214
|
+
]
|
|
215
|
+
`);
|
|
216
|
+
|
|
217
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
218
|
+
"//=== service.d.ts
|
|
219
|
+
import "@glimmer/component";
|
|
220
|
+
//#region src/components/popup.d.ts
|
|
221
|
+
interface PopupBlock {
|
|
222
|
+
open: () => void;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/service.d.ts
|
|
226
|
+
declare class TooltipService {
|
|
227
|
+
registerPopup(context: PopupBlock): void;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { TooltipService };
|
|
231
|
+
//# sourceMappingURL=service.d.ts.map"
|
|
232
|
+
`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("emits .d.ts for a .gts that type-imports another (otherwise unimported) .gts", async () => {
|
|
236
|
+
// A nested component type-imports a sibling from the parent directory;
|
|
237
|
+
// its declaration then carries a relative source specifier for a module
|
|
238
|
+
// that exists only in the bundler's graph (never on disk, never in the
|
|
239
|
+
// runtime graph).
|
|
240
|
+
const dist = await buildFixture(
|
|
241
|
+
{
|
|
242
|
+
"src/index.ts": `export { default as OptionList } from './components/select/option-list.gts';`,
|
|
243
|
+
"src/components/select/option-list.gts": [
|
|
244
|
+
`import Component from '@glimmer/component';`,
|
|
245
|
+
`import type { PopupBlock } from '../popup.gts';`,
|
|
246
|
+
``,
|
|
247
|
+
`export interface OptionListArgs {`,
|
|
248
|
+
` registerPopup: (context: PopupBlock) => void;`,
|
|
249
|
+
`}`,
|
|
250
|
+
``,
|
|
251
|
+
`export default class OptionList extends Component {`,
|
|
252
|
+
` <template>options</template>`,
|
|
253
|
+
`}`,
|
|
254
|
+
].join("\n"),
|
|
255
|
+
"src/components/popup.gts": [
|
|
256
|
+
`import Component from '@glimmer/component';`,
|
|
257
|
+
``,
|
|
258
|
+
`export interface PopupBlock {`,
|
|
259
|
+
` open: () => void;`,
|
|
260
|
+
`}`,
|
|
261
|
+
``,
|
|
262
|
+
`export default class Popup extends Component {`,
|
|
263
|
+
` <template>popup</template>`,
|
|
264
|
+
`}`,
|
|
265
|
+
].join("\n"),
|
|
266
|
+
},
|
|
267
|
+
["./src/index.ts"],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
271
|
+
[
|
|
272
|
+
"index.d.ts",
|
|
273
|
+
"index.d.ts.map",
|
|
274
|
+
"index.js",
|
|
275
|
+
"index.js.map",
|
|
276
|
+
]
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
280
|
+
"//=== index.d.ts
|
|
281
|
+
import Component from "@glimmer/component";
|
|
282
|
+
//#region src/components/select/option-list.d.ts
|
|
283
|
+
declare class OptionList extends Component {}
|
|
284
|
+
//#endregion
|
|
285
|
+
export { OptionList };
|
|
286
|
+
//# sourceMappingURL=index.d.ts.map"
|
|
287
|
+
`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
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.
|
|
296
|
+
const dist = await buildFixture(
|
|
297
|
+
{
|
|
298
|
+
"src/index.ts": `export { default as Popup } from './popup.gts';`,
|
|
299
|
+
"src/service.ts": [
|
|
300
|
+
`import type { PopupBlock } from './popup.gts';`,
|
|
301
|
+
``,
|
|
302
|
+
`export function register(context: PopupBlock): void {`,
|
|
303
|
+
` void context;`,
|
|
304
|
+
`}`,
|
|
305
|
+
].join("\n"),
|
|
306
|
+
"src/popup.css": `.popup { color: red; }`,
|
|
307
|
+
"src/popup.gts": [
|
|
308
|
+
`import './popup.css';`,
|
|
309
|
+
`import Component from '@glimmer/component';`,
|
|
310
|
+
``,
|
|
311
|
+
`export interface PopupBlock {`,
|
|
312
|
+
` open: () => void;`,
|
|
313
|
+
`}`,
|
|
314
|
+
``,
|
|
315
|
+
`export default class Popup extends Component {`,
|
|
316
|
+
` <template>popup</template>`,
|
|
317
|
+
`}`,
|
|
318
|
+
].join("\n"),
|
|
319
|
+
},
|
|
320
|
+
["./src/index.ts", "./src/service.ts"],
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(dist.files).toMatchInlineSnapshot(`
|
|
324
|
+
[
|
|
325
|
+
"index.d.ts",
|
|
326
|
+
"index.js",
|
|
327
|
+
"index.js.map",
|
|
328
|
+
"popup-CytznrQX.d.ts",
|
|
329
|
+
"popup-CytznrQX.d.ts.map",
|
|
330
|
+
"service.d.ts",
|
|
331
|
+
"service.d.ts.map",
|
|
332
|
+
"service.js",
|
|
333
|
+
"service.js.map",
|
|
334
|
+
"style.css",
|
|
335
|
+
]
|
|
336
|
+
`);
|
|
337
|
+
|
|
338
|
+
expect(printed(dist)).toMatchInlineSnapshot(`
|
|
339
|
+
"//=== index.d.ts
|
|
340
|
+
import { t as Popup } from "./popup-CytznrQX.js";
|
|
341
|
+
export { Popup };
|
|
342
|
+
//=== popup-CytznrQX.d.ts
|
|
343
|
+
import Component from "@glimmer/component";
|
|
344
|
+
//#region src/popup.d.ts
|
|
345
|
+
interface PopupBlock {
|
|
346
|
+
open: () => void;
|
|
347
|
+
}
|
|
348
|
+
declare class Popup extends Component {}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { PopupBlock as n, Popup as t };
|
|
351
|
+
//# sourceMappingURL=popup-CytznrQX.d.ts.map
|
|
352
|
+
//=== service.d.ts
|
|
353
|
+
import { n as PopupBlock } from "./popup-CytznrQX.js";
|
|
354
|
+
//#region src/service.d.ts
|
|
355
|
+
declare function register(context: PopupBlock): void;
|
|
356
|
+
//#endregion
|
|
357
|
+
export { register };
|
|
358
|
+
//# sourceMappingURL=service.d.ts.map
|
|
359
|
+
//=== style.css
|
|
360
|
+
.popup {
|
|
361
|
+
color: red;
|
|
362
|
+
}
|
|
363
|
+
"
|
|
364
|
+
`);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { rolldown } from "rolldown";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { emberTransform } from "./transform.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Drives the plugin through a real rolldown build. `files` is a map of
|
|
11
|
+
* relative path -> source; the build entry is `index.ts`. Every bare (package)
|
|
12
|
+
* specifier is marked external so the build only exercises the local
|
|
13
|
+
* `.gts`/`.gjs`/`.ts` handling and doesn't need real dependencies installed.
|
|
14
|
+
* Returns the concatenated code of every emitted chunk.
|
|
15
|
+
*/
|
|
16
|
+
async function bundle(files: Record<string, string>): Promise<string> {
|
|
17
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-build-"));
|
|
18
|
+
|
|
19
|
+
for (const [relative, source] of Object.entries(files)) {
|
|
20
|
+
const full = path.join(dir, relative);
|
|
21
|
+
await mkdir(path.dirname(full), { recursive: true });
|
|
22
|
+
await writeFile(full, source, "utf8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isLocal = (id: string) => id.startsWith(".") || id.startsWith("/") || path.isAbsolute(id);
|
|
26
|
+
|
|
27
|
+
const build = await rolldown({
|
|
28
|
+
input: path.join(dir, "index.ts"),
|
|
29
|
+
plugins: [emberTransform()],
|
|
30
|
+
external: (id) => !isLocal(id),
|
|
31
|
+
onwarn() {},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { output } = await build.generate({ format: "es" });
|
|
35
|
+
|
|
36
|
+
const code = output
|
|
37
|
+
.filter((chunk): chunk is typeof chunk & { code: string } => "code" in chunk)
|
|
38
|
+
.map((chunk) => chunk.code)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
// rolldown's `//#region <path>` comments reference the random temp dir;
|
|
42
|
+
// collapse them to the bare filename so snapshots stay deterministic.
|
|
43
|
+
return code.replace(/(\/\/#region ).*\/([^/\n]+)$/gm, "$1$2");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("emberTransform (full plugin via rolldown)", () => {
|
|
47
|
+
it("compiles a .gts module's <template> and keeps bare imports external", async () => {
|
|
48
|
+
const code = await bundle({
|
|
49
|
+
"index.ts": `export { default as Foo } from './foo.gts';`,
|
|
50
|
+
"foo.gts": [
|
|
51
|
+
`import Component from '@glimmer/component';`,
|
|
52
|
+
`export default class Foo extends Component {`,
|
|
53
|
+
` <template>Hello</template>`,
|
|
54
|
+
`}`,
|
|
55
|
+
].join("\n"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// <template> is gone, replaced by a content-tag `template(...)` call.
|
|
59
|
+
expect(code).not.toContain("<template>");
|
|
60
|
+
expect(code).toContain("template(");
|
|
61
|
+
// The bare import survived as an external dependency.
|
|
62
|
+
expect(code).toContain(`from "@glimmer/component"`);
|
|
63
|
+
// ...and it pulls in the template-compiler runtime import.
|
|
64
|
+
expect(code).toContain("@ember/template-compiler");
|
|
65
|
+
|
|
66
|
+
expect(code).toMatchInlineSnapshot(`
|
|
67
|
+
"import { template } from "@ember/template-compiler";
|
|
68
|
+
import Component from "@glimmer/component";
|
|
69
|
+
//#region foo.ts
|
|
70
|
+
var Foo = class extends Component {
|
|
71
|
+
static {
|
|
72
|
+
template(\`Hello\`, {
|
|
73
|
+
component: this,
|
|
74
|
+
eval() {
|
|
75
|
+
return eval(arguments[0]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
//#endregion
|
|
81
|
+
export { Foo };
|
|
82
|
+
"
|
|
83
|
+
`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("compiles .gjs alongside .gts and plain .ts in one graph", async () => {
|
|
87
|
+
const code = await bundle({
|
|
88
|
+
"index.ts": [
|
|
89
|
+
`export { default as Foo } from './foo.gts';`,
|
|
90
|
+
`export { greet } from './util.gjs';`,
|
|
91
|
+
`export { helper } from './plain.ts';`,
|
|
92
|
+
].join("\n"),
|
|
93
|
+
"foo.gts": `<template>Foo</template>`,
|
|
94
|
+
"util.gjs": [
|
|
95
|
+
`export function greet() { return 'hi'; }`,
|
|
96
|
+
`<template>{{greet}}</template>`,
|
|
97
|
+
].join("\n"),
|
|
98
|
+
"plain.ts": `export function helper() { return 1; }`,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(code).not.toContain("<template>");
|
|
102
|
+
expect(code).toContain("function greet()");
|
|
103
|
+
expect(code).toContain("function helper()");
|
|
104
|
+
expect(code).toContain("export {");
|
|
105
|
+
|
|
106
|
+
expect(code).toMatchInlineSnapshot(`
|
|
107
|
+
"import { template } from "@ember/template-compiler";
|
|
108
|
+
//#region foo.ts
|
|
109
|
+
var foo_default = template(\`Foo\`, { eval() {
|
|
110
|
+
return eval(arguments[0]);
|
|
111
|
+
} });
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region util.js
|
|
114
|
+
function greet() {
|
|
115
|
+
return "hi";
|
|
116
|
+
}
|
|
117
|
+
var util_default = template(\`{{greet}}\`, { eval() {
|
|
118
|
+
return eval(arguments[0]);
|
|
119
|
+
} });
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region plain.ts
|
|
122
|
+
function helper() {
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { foo_default as Foo, greet, helper };
|
|
127
|
+
"
|
|
128
|
+
`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("resolves a .gts module that imports another .gts relatively", async () => {
|
|
132
|
+
const code = await bundle({
|
|
133
|
+
"index.ts": `export { default as Page } from './page.gts';`,
|
|
134
|
+
"page.gts": [
|
|
135
|
+
`import Widget from './widget.gts';`,
|
|
136
|
+
`export default class Page {`,
|
|
137
|
+
` Widget = Widget;`,
|
|
138
|
+
` <template><Widget /></template>`,
|
|
139
|
+
`}`,
|
|
140
|
+
].join("\n"),
|
|
141
|
+
"widget.gts": `<template>widget</template>`,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Both templates compiled, and the relative .gts import resolved (bundled).
|
|
145
|
+
expect(code).not.toContain("<template>");
|
|
146
|
+
expect(code).not.toContain(".gts");
|
|
147
|
+
expect(code).toContain("template(");
|
|
148
|
+
|
|
149
|
+
expect(code).toMatchInlineSnapshot(`
|
|
150
|
+
"import { template } from "@ember/template-compiler";
|
|
151
|
+
//#region widget.ts
|
|
152
|
+
var widget_default = template(\`widget\`, { eval() {
|
|
153
|
+
return eval(arguments[0]);
|
|
154
|
+
} });
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region page.ts
|
|
157
|
+
var Page = class {
|
|
158
|
+
Widget = widget_default;
|
|
159
|
+
static {
|
|
160
|
+
template(\`<Widget />\`, {
|
|
161
|
+
component: this,
|
|
162
|
+
eval() {
|
|
163
|
+
return eval(arguments[0]);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
export { Page };
|
|
170
|
+
"
|
|
171
|
+
`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("emits sourcemaps for the specifier rewrite (no SOURCEMAP_BROKEN warnings)", async () => {
|
|
175
|
+
const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-map-"));
|
|
176
|
+
|
|
177
|
+
const files = {
|
|
178
|
+
// index.ts contains a `.gts` specifier, so it goes through the rewrite.
|
|
179
|
+
"index.ts": `export { default as Foo } from './foo.gts';`,
|
|
180
|
+
"foo.gts": `<template>Foo</template>`,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
for (const [relative, source] of Object.entries(files)) {
|
|
184
|
+
await writeFile(path.join(dir, relative), source, "utf8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const warnings: string[] = [];
|
|
188
|
+
const build = await rolldown({
|
|
189
|
+
input: path.join(dir, "index.ts"),
|
|
190
|
+
plugins: [emberTransform()],
|
|
191
|
+
external: (id) => !(id.startsWith(".") || path.isAbsolute(id)),
|
|
192
|
+
onwarn(warning) {
|
|
193
|
+
warnings.push(warning.code ?? String(warning));
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { output } = await build.generate({ format: "es", sourcemap: true });
|
|
198
|
+
|
|
199
|
+
expect(warnings.filter((code) => code === "SOURCEMAP_BROKEN")).toEqual([]);
|
|
200
|
+
|
|
201
|
+
const chunk = output.find((entry) => entry.type === "chunk" && "map" in entry);
|
|
202
|
+
expect(chunk).toBeDefined();
|
|
203
|
+
// The composed map traces through the rewrite back to the original .gts
|
|
204
|
+
// source (content-tag's map survives the specifier-rewrite pass).
|
|
205
|
+
const sources = (chunk as { map?: { sources: string[] } }).map?.sources ?? [];
|
|
206
|
+
expect(sources.some((source) => source.endsWith("foo.gts"))).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|