ember.nvp 0.7.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.github/workflows/ci.yml +6 -0
  2. package/.release-plan.json +7 -59
  3. package/CHANGELOG.md +46 -0
  4. package/package.json +1 -1
  5. package/packages/rolldown/README.md +68 -1
  6. package/packages/rolldown/index.ts +8 -4
  7. package/packages/rolldown/package.json +13 -4
  8. package/packages/rolldown/rolldown.config.js +5 -3
  9. package/packages/rolldown/src/app-reexports.test.ts +220 -0
  10. package/packages/rolldown/src/app-reexports.ts +152 -0
  11. package/packages/rolldown/src/config.ts +50 -0
  12. package/packages/rolldown/src/dts.test.ts +166 -0
  13. package/packages/rolldown/src/transform.test.ts +173 -0
  14. package/src/bases/minimal-extension/extension.test.ts +251 -0
  15. package/src/bases/minimal-extension/files/.env.development +8 -0
  16. package/src/bases/minimal-extension/files/README.md +36 -0
  17. package/src/bases/minimal-extension/files/app/app.ts +21 -0
  18. package/src/bases/minimal-extension/files/app/boot.ts +8 -0
  19. package/src/bases/minimal-extension/files/app/config.ts +22 -0
  20. package/src/bases/minimal-extension/files/app/router.ts +10 -0
  21. package/src/bases/minimal-extension/files/app/templates/application.gts +5 -0
  22. package/src/bases/minimal-extension/files/babel.config.js +49 -0
  23. package/src/bases/minimal-extension/files/index.html +17 -0
  24. package/src/bases/minimal-extension/files/package.json +58 -0
  25. package/src/bases/minimal-extension/files/public/manifest.json +9 -0
  26. package/src/bases/minimal-extension/files/tsconfig.json +13 -0
  27. package/src/bases/minimal-extension/files/vite.config.mjs +6 -0
  28. package/src/bases/minimal-extension/index.js +142 -0
  29. package/src/bases/minimal-library/files/tsdown.config.js +2 -1
  30. package/src/cli/generator.js +4 -0
  31. package/src/cli/questions/project-type.js +9 -2
  32. package/src/layers/renovate/index.js +4 -3
  33. package/src/layers/typescript/index.js +6 -0
  34. package/src/utils/types.ts +1 -1
  35. package/test/ember.nvp/base-library.test.ts +41 -30
  36. package/test/ember.nvp/layer-publish-checks.test.ts +7 -10
  37. package/test/ember.nvp/layer-qunit.test.ts +17 -30
  38. package/test/ember.nvp/layer-vitest.test.ts +4 -18
  39. package/test/ember.nvp/permutations/app-checks.test.ts +3 -0
  40. package/test/ember.nvp/permutations/extension-1.test.ts +3 -0
  41. package/test/ember.nvp/permutations/extension-2.test.ts +3 -0
  42. package/test/ember.nvp/permutations/extension-checks.test.ts +3 -0
  43. package/test/ember.nvp/permutations/library-checks.test.ts +3 -0
  44. package/test/ember.nvp/permutations/shared.ts +68 -19
  45. package/test/test-helpers.ts +25 -5
  46. package/vitest.config.js +1 -1
  47. package/packages/rolldown/src/define-config.ts +0 -32
@@ -0,0 +1,152 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
3
+
4
+ import type { Plugin } from "rolldown";
5
+
6
+ export interface AppReexportsOptions {
7
+ /**
8
+ * Globs, matched against each built filename (relative to the output
9
+ * directory), selecting which modules get an app re-export.
10
+ *
11
+ * Defaults to `["services/*"]` -- top-level services only. Under strict
12
+ * mode, components and helpers are imported, but services are still
13
+ * injected by name, so they are the one thing an app must always be able
14
+ * to resolve.
15
+ *
16
+ * `.d.ts` files never match.
17
+ */
18
+ include?: string[];
19
+
20
+ /**
21
+ * Globs removing files that `include` matched.
22
+ */
23
+ exclude?: string[];
24
+
25
+ /**
26
+ * Rename a re-export: receives the built filename, returns the filename the
27
+ * app sees. Defaults to the built filename unchanged.
28
+ */
29
+ mapFilename?: (filename: string) => string;
30
+
31
+ /**
32
+ * Which bindings a file's re-export module forwards: an array of names
33
+ * (default `["default"]`), or a raw export clause string such as `"*"`.
34
+ */
35
+ exports?: (filename: string) => string[] | string | undefined;
36
+ }
37
+
38
+ /**
39
+ * Merges built modules into the consuming app's namespace, so classic
40
+ * resolution (`{{a-component}}`, service/helper lookup by name) finds them --
41
+ * the same job as `@embroider/addon-dev`'s `appReexports` rollup plugin.
42
+ *
43
+ * For every built file matching `include` (minus `exclude` and `.d.ts`), it
44
+ * writes a tiny module under `<outDir>/_app_/` re-exporting from the
45
+ * library's own name, and records the full set in `package.json` under
46
+ * `ember-addon.app-js`.
47
+ *
48
+ * Unlike the embroider plugin, nothing is written unless its content actually
49
+ * differs from what is on disk: the `_app_/` modules are compared
50
+ * byte-for-byte, and `package.json` is only rewritten when the `app-js` map
51
+ * itself changed (so an equivalent map in different formatting is left
52
+ * alone). This keeps rebuilds from re-triggering file watchers.
53
+ *
54
+ * This is a separate import, because most libraries don't need it:
55
+ *
56
+ * ```js
57
+ * import { defineConfig } from "tsdown";
58
+ * import { ember } from "@nullvoxpopuli/ember-rolldown";
59
+ * import { appReexports } from "@nullvoxpopuli/ember-rolldown/app-reexports";
60
+ *
61
+ * export default defineConfig({
62
+ * entry: ["./src/index.ts", "./src/services/session.ts"],
63
+ * plugins: [ember(), appReexports()],
64
+ * });
65
+ * ```
66
+ *
67
+ * With no arguments, top-level services (`services/*`) are re-exported. A
68
+ * string argument is an include glob; an object gives full control:
69
+ *
70
+ * ```js
71
+ * appReexports(); // services/*
72
+ * appReexports("components/**"); // one include glob
73
+ * appReexports({ include: ["services/*", "helpers/*"], exclude: [...] });
74
+ * ```
75
+ */
76
+ export function appReexports(options: string | AppReexportsOptions = {}): Plugin {
77
+ const resolved = typeof options === "string" ? { include: [options] } : options;
78
+ const include = resolved.include ?? ["services/*"];
79
+
80
+ return {
81
+ name: "ember:app-reexports",
82
+
83
+ writeBundle(outputOptions, bundle) {
84
+ const manifest = readJsonSync("package.json");
85
+ const outDir = outputOptions.dir ?? dirname(outputOptions.file ?? "dist");
86
+ const appJs: Record<string, string> = {};
87
+
88
+ // Sorted so the app-js map (and therefore package.json) is stable
89
+ // across builds regardless of chunk emission order.
90
+ for (const builtFilename of Object.keys(bundle).sort()) {
91
+ if (!isIncluded(builtFilename, include, resolved.exclude)) continue;
92
+
93
+ const appFilename = resolved.mapFilename?.(builtFilename) ?? builtFilename;
94
+ const names = resolved.exports?.(builtFilename) || ["default"];
95
+ const clause = typeof names === "string" ? names : `{ ${names.join(", ")} }`;
96
+ const withoutExtension = builtFilename.slice(0, -extname(builtFilename).length);
97
+ const reexportPath = join(outDir, "_app_", appFilename);
98
+
99
+ appJs[`./${appFilename}`] = `./${toPosix(relative(process.cwd(), resolve(reexportPath)))}`;
100
+
101
+ writeFileIfChanged(
102
+ reexportPath,
103
+ `export ${clause} from "${manifest.name}/${withoutExtension}";\n`,
104
+ );
105
+ }
106
+
107
+ const existing = manifest["ember-addon"]?.["app-js"] ?? {};
108
+
109
+ if (!sameStringRecord(existing, appJs)) {
110
+ manifest["ember-addon"] = { ...manifest["ember-addon"], "app-js": appJs };
111
+ writeFileSync("package.json", JSON.stringify(manifest, null, 2) + "\n");
112
+ }
113
+ },
114
+ };
115
+ }
116
+
117
+ function readJsonSync(path: string) {
118
+ return JSON.parse(readFileSync(path, { encoding: "utf8" }));
119
+ }
120
+
121
+ function isIncluded(filename: string, include: string[], exclude?: string[]): boolean {
122
+ if (matchesGlob(filename, "**/*.d.ts")) return false;
123
+ if (!include.some((glob) => matchesGlob(filename, glob))) return false;
124
+ if (exclude?.some((glob) => matchesGlob(filename, glob))) return false;
125
+
126
+ return true;
127
+ }
128
+
129
+ function writeFileIfChanged(path: string, content: string) {
130
+ let existing: string | undefined;
131
+
132
+ try {
133
+ existing = readFileSync(path, { encoding: "utf8" });
134
+ } catch {
135
+ // Doesn't exist yet (or isn't readable) -- write it.
136
+ }
137
+
138
+ if (existing === content) return;
139
+
140
+ mkdirSync(dirname(path), { recursive: true });
141
+ writeFileSync(path, content);
142
+ }
143
+
144
+ function sameStringRecord(a: Record<string, string>, b: Record<string, string>): boolean {
145
+ const aKeys = Object.keys(a);
146
+
147
+ return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
148
+ }
149
+
150
+ function toPosix(path: string): string {
151
+ return path.split(sep).join("/");
152
+ }
@@ -0,0 +1,50 @@
1
+ import type { TsdownPlugin, UserConfig } from "tsdown";
2
+
3
+ /**
4
+ * Sensible defaults for an Ember v2 library, applied through plugin hooks so
5
+ * they travel with `ember()` — you get them whether you use this package's
6
+ * `defineConfig`, tsdown's, or a plain `tsdown.config.js` / `rolldown.config.js`.
7
+ *
8
+ * Every value is applied with `??=`, so anything you set explicitly wins.
9
+ *
10
+ * Under tsdown, the full set applies via the `tsdownConfig` hook (analogous to
11
+ * Vite's `config` hook):
12
+ *
13
+ * - `sourcemap` — emit sourcemaps alongside the output.
14
+ * - `clean` — wipe `dist/` between builds.
15
+ * - `dts` — emit `.d.ts` declarations (via isolated declarations).
16
+ * - `outExtensions` — `.js`/`.d.ts` rather than tsdown's default `.mjs`/`.d.mts`,
17
+ * since exports maps conventionally point at `.js`/`.d.ts`.
18
+ * - `report` — off; the size report is noise for a library build.
19
+ * - `deps.neverBundle` — leave node builtins and the ember virtual packages to
20
+ * the consuming app (`emberExternals()` handles the rest).
21
+ *
22
+ * A plain rolldown build has no notion of `clean`/`dts`/`outExtensions`/`report`
23
+ * (those are tsdown-level concepts), so only the options with rolldown
24
+ * equivalents are applied there, via rolldown's own `outputOptions` hook:
25
+ *
26
+ * - `output.sourcemap` — on (output option).
27
+ *
28
+ * Externals are handled by `emberExternals()` (a `resolveId` hook) in both cases.
29
+ */
30
+ export function emberConfig(): TsdownPlugin {
31
+ return {
32
+ name: "ember:config",
33
+
34
+ tsdownConfig(config: UserConfig) {
35
+ config.sourcemap ??= true;
36
+ config.clean ??= true;
37
+ config.dts ??= { sourcemap: true };
38
+ config.outExtensions ??= () => ({ js: ".js", dts: ".d.ts" });
39
+ config.report ??= false;
40
+
41
+ config.deps ??= {};
42
+ config.deps.neverBundle ??= ["node:*", "@ember/*", "@glimmer/*"];
43
+ },
44
+
45
+ outputOptions(options) {
46
+ options.sourcemap ??= true;
47
+ return options;
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,166 @@
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
+ /**
10
+ * Drives `ember()` through a real tsdown build (the dts pipeline included).
11
+ * `files` is a map of relative path -> source, written into a temp dir that
12
+ * becomes the cwd for the build (the externals and isolated-declarations
13
+ * plugins read package.json / tsconfig.json from cwd, like the tsdown CLI).
14
+ */
15
+ async function buildFixture(files: Record<string, string>, entry: string[]): Promise<string> {
16
+ const dir = await mkdtemp(path.join(tmpdir(), "ember-rolldown-dts-"));
17
+
18
+ const withDefaults: Record<string, string> = {
19
+ "package.json": JSON.stringify({
20
+ name: "fixture",
21
+ version: "0.0.0",
22
+ type: "module",
23
+ exports: { "./*": { types: "./dist/*.d.ts", default: "./dist/*.js" } },
24
+ }),
25
+ "tsconfig.json": JSON.stringify({
26
+ compilerOptions: { isolatedDeclarations: true, declaration: true },
27
+ }),
28
+ ...files,
29
+ };
30
+
31
+ for (const [relative, source] of Object.entries(withDefaults)) {
32
+ const full = path.join(dir, relative);
33
+ await mkdir(path.dirname(full), { recursive: true });
34
+ await writeFile(full, source, "utf8");
35
+ }
36
+
37
+ const previousCwd = process.cwd();
38
+ process.chdir(dir);
39
+ restoreCwd = () => process.chdir(previousCwd);
40
+
41
+ await build({
42
+ entry,
43
+ config: false,
44
+ logLevel: "silent",
45
+ plugins: [ember()],
46
+ });
47
+
48
+ return dir;
49
+ }
50
+
51
+ let restoreCwd: (() => void) | undefined;
52
+
53
+ afterEach(() => {
54
+ restoreCwd?.();
55
+ restoreCwd = undefined;
56
+ });
57
+
58
+ async function listFiles(dir: string): Promise<string[]> {
59
+ const out: string[] = [];
60
+ for (const entry of await readdir(dir, { withFileTypes: true, recursive: true })) {
61
+ if (entry.isFile()) {
62
+ out.push(path.relative(dir, path.join(entry.parentPath, entry.name)));
63
+ }
64
+ }
65
+ return out.sort();
66
+ }
67
+
68
+ describe("declarations (full ember() via tsdown)", () => {
69
+ it("emits .d.ts for an entry that type-imports from a non-entry .gts", async () => {
70
+ // Mirrors the luna-core shape: the component is in the runtime graph via
71
+ // the index entry, while a second entry (a service) only *type*-imports
72
+ // from it. The service's declaration then references the component's
73
+ // (virtual) .d.ts, which only exists in the bundler's module graph.
74
+ const dir = await buildFixture(
75
+ {
76
+ "src/index.ts": `export { default as Popup } from './popup.gts';`,
77
+ "src/service.ts": [
78
+ `import type { PopupBlock } from './popup.gts';`,
79
+ ``,
80
+ `export class TooltipService {`,
81
+ ` registerPopup(context: PopupBlock): void {`,
82
+ ` void context;`,
83
+ ` }`,
84
+ `}`,
85
+ ].join("\n"),
86
+ "src/popup.gts": [
87
+ `import Component from '@glimmer/component';`,
88
+ ``,
89
+ `export interface PopupBlock {`,
90
+ ` open: () => void;`,
91
+ `}`,
92
+ ``,
93
+ `export interface PopupSignature {`,
94
+ ` Element: HTMLDivElement;`,
95
+ ` Blocks: { default: [PopupBlock] };`,
96
+ `}`,
97
+ ``,
98
+ `export default class Popup extends Component<PopupSignature> {`,
99
+ ` <template>popup</template>`,
100
+ `}`,
101
+ ].join("\n"),
102
+ },
103
+ ["./src/index.ts", "./src/service.ts"],
104
+ );
105
+
106
+ const dist = await listFiles(path.join(dir, "dist"));
107
+
108
+ expect(dist).toContain("index.d.ts");
109
+ expect(dist).toContain("service.d.ts");
110
+
111
+ const serviceDts = await readFile(path.join(dir, "dist/service.d.ts"), "utf8");
112
+
113
+ // The PopupBlock type must be reachable: either inlined into the bundled
114
+ // declaration or imported via a specifier that exists in dist.
115
+ expect(serviceDts).toContain("PopupBlock");
116
+ expect(serviceDts).not.toContain(".gts");
117
+ });
118
+
119
+ it("bundles co-located CSS (via @tsdown/css) without breaking declarations", async () => {
120
+ // Ember components routinely import a co-located .css file. Without CSS
121
+ // handling the build dies on tsdown's css-guard — and because the failing
122
+ // component module never loads, every declaration that references it
123
+ // dangles (surfacing as misleading UNLOADABLE_DEPENDENCY errors on
124
+ // <component>.d.ts). @tsdown/css (this package's peer) makes both work.
125
+ const dir = await buildFixture(
126
+ {
127
+ "src/index.ts": `export { default as Popup } from './popup.gts';`,
128
+ "src/service.ts": [
129
+ `import type { PopupBlock } from './popup.gts';`,
130
+ ``,
131
+ `export function register(context: PopupBlock): void {`,
132
+ ` void context;`,
133
+ `}`,
134
+ ].join("\n"),
135
+ "src/popup.css": `.popup { color: red; }`,
136
+ "src/popup.gts": [
137
+ `import './popup.css';`,
138
+ `import Component from '@glimmer/component';`,
139
+ ``,
140
+ `export interface PopupBlock {`,
141
+ ` open: () => void;`,
142
+ `}`,
143
+ ``,
144
+ `export default class Popup extends Component {`,
145
+ ` <template>popup</template>`,
146
+ `}`,
147
+ ].join("\n"),
148
+ },
149
+ ["./src/index.ts", "./src/service.ts"],
150
+ );
151
+
152
+ const dist = await listFiles(path.join(dir, "dist"));
153
+
154
+ const cssFiles = dist.filter((file) => file.endsWith(".css"));
155
+ expect(cssFiles).toHaveLength(1);
156
+
157
+ const css = await readFile(path.join(dir, "dist", cssFiles[0]!), "utf8");
158
+ expect(css).toContain(".popup");
159
+
160
+ // The cascade regression: the css-importing component's declaration must
161
+ // still be generated and reachable from the service's declaration.
162
+ const serviceDts = await readFile(path.join(dir, "dist/service.d.ts"), "utf8");
163
+ expect(serviceDts).toContain("PopupBlock");
164
+ expect(serviceDts).not.toContain(".gts");
165
+ });
166
+ });
@@ -0,0 +1,173 @@
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
+ });