ember.nvp 0.6.0 → 0.7.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 (85) hide show
  1. package/.github/workflows/ci.yml +17 -2
  2. package/.release-plan.json +88 -4
  3. package/CHANGELOG.md +57 -0
  4. package/package.json +3 -2
  5. package/packages/build-tooling-utils/README.md +37 -0
  6. package/packages/build-tooling-utils/index.ts +2 -0
  7. package/packages/build-tooling-utils/package.json +42 -0
  8. package/packages/build-tooling-utils/rolldown.config.js +18 -0
  9. package/packages/build-tooling-utils/src/extensions.ts +18 -0
  10. package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
  11. package/packages/rolldown/README.md +115 -0
  12. package/packages/rolldown/index.ts +56 -0
  13. package/packages/rolldown/package.json +59 -0
  14. package/packages/rolldown/rolldown.config.js +21 -0
  15. package/packages/rolldown/src/babel.ts +136 -0
  16. package/packages/rolldown/src/define-config.ts +32 -0
  17. package/packages/rolldown/src/externals.ts +83 -0
  18. package/packages/rolldown/src/isolated-declarations.ts +65 -0
  19. package/packages/rolldown/src/transform.ts +162 -0
  20. package/packages/vite/README.md +7 -21
  21. package/packages/vite/index.ts +1 -1
  22. package/packages/vite/package.json +4 -6
  23. package/packages/vite/src/ember.ts +129 -15
  24. package/packages/vite/src/test-html.ts +86 -0
  25. package/src/bases/minimal-app/files/app/router.ts +1 -0
  26. package/src/bases/minimal-app/files/tsconfig.json +1 -0
  27. package/src/bases/minimal-app/index.js +2 -2
  28. package/src/bases/minimal-library/files/README.md +41 -0
  29. package/src/bases/minimal-library/files/package.json +49 -0
  30. package/src/bases/minimal-library/files/tsconfig.json +10 -0
  31. package/src/bases/minimal-library/files/tsdown.config.js +6 -0
  32. package/src/bases/minimal-library/index.js +158 -2
  33. package/src/cli/index.js +21 -2
  34. package/src/cli/questions/layers.js +5 -2
  35. package/src/cli/questions/project-type.js +0 -5
  36. package/src/layers/are-the-types-wrong/index.js +74 -0
  37. package/src/layers/git/files/gitignore +14 -1
  38. package/src/layers/git/index.js +6 -5
  39. package/src/layers/github-actions/index.js +12 -8
  40. package/src/layers/index.js +1 -1
  41. package/src/layers/inspector-support/index.js +101 -0
  42. package/src/layers/prettier/files/.prettierignore +4 -0
  43. package/src/layers/publint/index.js +68 -0
  44. package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
  45. package/src/layers/qunit/index.js +107 -7
  46. package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
  47. package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
  48. package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
  49. package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
  50. package/src/layers/qunit/library-files/vite.config.mjs +6 -0
  51. package/src/layers/typescript/index.js +43 -7
  52. package/src/layers/vitest/files/vitest.config.mjs +11 -11
  53. package/src/layers/vitest/index.js +70 -32
  54. package/src/utils/babel.js +9 -6
  55. package/src/utils/fs.js +44 -3
  56. package/src/utils/inspector-support.js +196 -0
  57. package/src/utils/npm.js +21 -7
  58. package/src/utils/remove-types.js +5 -0
  59. package/src/utils/rewrite-imports.js +157 -0
  60. package/src/utils/tsdown-config.js +85 -0
  61. package/src/utils/types.ts +5 -1
  62. package/test/ember.nvp/base-library.test.ts +232 -0
  63. package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
  64. package/test/ember.nvp/inspector-support.test.ts +179 -0
  65. package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
  66. package/test/ember.nvp/layer-publish-checks.test.ts +103 -0
  67. package/test/ember.nvp/layer-qunit.test.ts +376 -15
  68. package/test/ember.nvp/layer-vitest.test.ts +250 -0
  69. package/test/ember.nvp/layers.test.ts +16 -0
  70. package/test/ember.nvp/library-src-fixtures.ts +81 -0
  71. package/test/ember.nvp/permutations/app-1.test.ts +3 -0
  72. package/test/ember.nvp/permutations/app-2.test.ts +3 -0
  73. package/test/ember.nvp/permutations/library-1.test.ts +3 -0
  74. package/test/ember.nvp/permutations/library-2.test.ts +3 -0
  75. package/test/ember.nvp/permutations/shared.ts +239 -0
  76. package/test/ember.nvp/rewrite-imports.test.ts +235 -0
  77. package/test/test-helpers/permutate.test.ts +17 -0
  78. package/test/test-helpers.ts +20 -7
  79. package/tsconfig.json +1 -0
  80. package/packages/vite/src/maybe-babel.ts +0 -219
  81. package/src/layers/qunit/files/run-tests.mjs +0 -30
  82. package/src/layers/vitest/files/tests/test-helper.ts +0 -5
  83. package/test/ember.nvp/permutations.test.ts +0 -200
  84. /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
  85. /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
@@ -0,0 +1,56 @@
1
+ import type { Plugin } from "rolldown";
2
+
3
+ import { emberBabel, type BabelOptions } from "./src/babel.ts";
4
+ import { emberExternals } from "./src/externals.ts";
5
+ import { emberIsolatedDeclarations } from "./src/isolated-declarations.ts";
6
+ import { emberTransform } from "./src/transform.ts";
7
+
8
+ export { defineConfig } from "./src/define-config.ts";
9
+
10
+ interface Config {
11
+ /**
12
+ * Options for the babel step; see `BabelOptions`.
13
+ */
14
+ babel?: BabelOptions;
15
+ }
16
+
17
+ /**
18
+ * A batteries-included plugin for building Ember v2 libraries (addons) with
19
+ * rolldown (or tsdown, which is built on rolldown).
20
+ *
21
+ * It bundles everything needed to compile `.gts`/`.gjs` and template-tag
22
+ * (`<template>`) source into publishable output:
23
+ *
24
+ * - `emberIsolatedDeclarations()` — errors when a tsconfig.json is present
25
+ * without `isolatedDeclarations: true` (required: it is the only
26
+ * declaration pipeline that can see compiled template-tag modules).
27
+ * - `emberExternals()` — keeps your dependencies, peerDependencies, and the
28
+ * ember virtual packages external, so consuming apps resolve them.
29
+ * - `emberTransform()` — preprocesses `<template>` via content-tag and maps
30
+ * `.gts`/`.gjs` to `.ts`/`.js` so rolldown can understand them.
31
+ * - `emberBabel()` — runs babel (template compilation, decorators, type
32
+ * stripping) with `babelHelpers: "bundled"`, but only on the files that
33
+ * actually need it (via `maybeBabel`); everything else stays on the fast
34
+ * native transform. The library's own `babel.config.js` is used when it
35
+ * exists; no config file is required.
36
+ *
37
+ * Usage in `tsdown.config.js` / `rolldown.config.js`:
38
+ *
39
+ * ```js
40
+ * import { ember } from "@nullvoxpopuli/ember-rolldown";
41
+ *
42
+ * export default defineConfig({
43
+ * entry: ["./src/index.ts"],
44
+ * dts: true,
45
+ * plugins: [ember()],
46
+ * });
47
+ * ```
48
+ */
49
+ export function ember(config: Config = {}): Plugin[] {
50
+ return [
51
+ emberIsolatedDeclarations(),
52
+ emberExternals(),
53
+ emberTransform(),
54
+ emberBabel(config.babel),
55
+ ];
56
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@nullvoxpopuli/ember-rolldown",
3
+ "version": "1.1.0",
4
+ "description": "Meta plugin for building ember v2 libraries/addons with rolldown & tsdown.",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "publishConfig": {
8
+ "main": "dist/index.js"
9
+ },
10
+ "keywords": [
11
+ "rolldown",
12
+ "tsdown",
13
+ "ember",
14
+ "meta plugin",
15
+ "v2 addon",
16
+ "library"
17
+ ],
18
+ "author": "NullVoxPopuli",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/NullVoxPopuli/ember.nvp",
23
+ "directory": "packages/rolldown"
24
+ },
25
+ "scripts": {
26
+ "build": "rolldown --config",
27
+ "prepack": "pnpm build"
28
+ },
29
+ "packageManager": "pnpm@10.28.0",
30
+ "dependencies": {
31
+ "@babel/core": "^7.28.10",
32
+ "@babel/plugin-transform-typescript": "^7.28.10",
33
+ "@embroider/core": "^4.4.2",
34
+ "@nullvoxpopuli/ember-build-tooling-utils": "workspace:*",
35
+ "babel-plugin-ember-template-compilation": "^4.0.0",
36
+ "content-tag": "^4.2.0",
37
+ "decorator-transforms": "^2.3.2"
38
+ },
39
+ "engines": {
40
+ "node": ">= 24"
41
+ },
42
+ "peerDependencies": {
43
+ "tsdown": ">= 0.22.0",
44
+ "typescript": ">= 5.5"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "tsdown": {
48
+ "optional": true
49
+ },
50
+ "typescript": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "rolldown": "^1.1.2",
56
+ "tsdown": "^0.22.7",
57
+ "typescript": "^6.0.3"
58
+ }
59
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from "rolldown";
2
+
3
+ import manifest from "./package.json" with { type: "json" };
4
+
5
+ const deps = [
6
+ ...Object.keys(manifest.dependencies || {}),
7
+ ...Object.keys(manifest.peerDependencies || {}),
8
+ ];
9
+
10
+ export default defineConfig({
11
+ input: "index.ts",
12
+ output: {
13
+ format: "esm",
14
+ file: "dist/index.js",
15
+ codeSplitting: false,
16
+ },
17
+ external(id) {
18
+ if (id.startsWith("node:")) return true;
19
+ if (deps.some((dep) => id.startsWith(dep))) return true;
20
+ },
21
+ });
@@ -0,0 +1,136 @@
1
+ import { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";
2
+ import transformTypeScript from "@babel/plugin-transform-typescript";
3
+ import templateCompilation from "babel-plugin-ember-template-compilation";
4
+ import decoratorTransforms from "decorator-transforms";
5
+ import { loadPartialConfigSync } from "@babel/core";
6
+ import type { PluginItem } from "@babel/core";
7
+ import type { Plugin } from "rolldown";
8
+
9
+ export interface BabelOptions {
10
+ babelHelpers?: "bundled" | "runtime" | "inline" | "external";
11
+ /**
12
+ * The babel config file to use.
13
+ *
14
+ * - `undefined`: auto-detect the project's babel config (via babel's own
15
+ * config resolution).
16
+ * - a string: use that config file.
17
+ * - `false`: ignore config files entirely.
18
+ *
19
+ * Without a config file, templates, decorators, and TypeScript are still
20
+ * handled (see `defaultPlugins`).
21
+ */
22
+ configFile?: string | false;
23
+ /**
24
+ * Extra babel plugins to run, appended after the config's plugins.
25
+ */
26
+ plugins?: PluginItem[];
27
+ /**
28
+ * Opt additional files into babel (see maybeBabel's `filter`). Use this to
29
+ * cover addons that ship code needing babel (e.g. `ember-concurrency`).
30
+ */
31
+ filter?: {
32
+ include: {
33
+ imports: string[];
34
+ code: (string | RegExp)[];
35
+ };
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Resolve the project's root babel config the way babel itself does
41
+ * (`babel.config.{js,cjs,mjs,cts,json}`, honoring `rootMode` semantics).
42
+ *
43
+ * The sync API evaluates the config file; on node 24+ that works for native
44
+ * ESM configs too (require(esm)).
45
+ */
46
+ function detectConfigFile(): string | undefined {
47
+ const partial = loadPartialConfigSync({ cwd: process.cwd() });
48
+
49
+ return partial?.config ?? undefined;
50
+ }
51
+
52
+ /**
53
+ * Everything a library without its own babel config needs:
54
+ *
55
+ * - TypeScript stripping
56
+ * - template compilation to `precompileTemplate` (`targetFormat: "hbs"`).
57
+ * Published libraries must NOT ship wire format: the wire format is
58
+ * private between the template compiler and the glimmer runtime of the
59
+ * same version, so baking it in ties the published artifact to the
60
+ * consuming app's exact ember-source. The consuming app performs final
61
+ * compilation.
62
+ * - decorator-transforms, with its runtime left as a bare specifier so the
63
+ * consuming app resolves it (the library keeps `decorator-transforms` as
64
+ * a real dependency).
65
+ */
66
+ function defaultPlugins(): PluginItem[] {
67
+ return [
68
+ [
69
+ transformTypeScript,
70
+ {
71
+ allExtensions: true,
72
+ onlyRemoveTypeImports: true,
73
+ allowDeclareFields: true,
74
+ },
75
+ ],
76
+ [templateCompilation, { targetFormat: "hbs" }],
77
+ [
78
+ decoratorTransforms,
79
+ {
80
+ // Emit `import ... from "decorator-transforms/runtime-esm"` as a bare
81
+ // specifier so it stays external and the consuming app resolves it.
82
+ runtime: { import: "decorator-transforms/runtime-esm" },
83
+ },
84
+ ],
85
+ ];
86
+ }
87
+
88
+ /**
89
+ * We still need babel (not just oxc/rolldown's native transforms) for two
90
+ * reasons:
91
+ *
92
+ * - template compilation: `babel-plugin-ember-template-compilation` turns the
93
+ * content-tag output into `precompileTemplate` calls; oxc has no equivalent.
94
+ * - decorators: Ember uses the legacy/stage-1 decorator signature together
95
+ * with `decorator-transforms`, which rewrites decorated class fields
96
+ * (`@tracked x`) into getter/setters backed by a runtime. oxc's native
97
+ * "legacy decorators" emit tsc-style `__decorate` output, which does not
98
+ * match those semantics.
99
+ *
100
+ * But we don't want to send *every* file through babel and give back the speed
101
+ * rolldown/oxc buys us -- so we run babel via `maybeBabel`, which filters down
102
+ * to only the files that actually need it (template-tag, decorators, template
103
+ * imports). Everything else stays on the native transform.
104
+ *
105
+ * The library's own babel config is used when it exists; without one,
106
+ * `defaultPlugins` covers templates, decorators, and TypeScript, so no
107
+ * config file is required.
108
+ *
109
+ * Libraries default to `babelHelpers: "bundled"` so the emitted output is
110
+ * self-contained.
111
+ */
112
+ export function emberBabel(options: BabelOptions = {}): Plugin {
113
+ const configFile = options.configFile === undefined ? detectConfigFile() : options.configFile;
114
+
115
+ const shared = {
116
+ babelHelpers: options.babelHelpers ?? "bundled",
117
+ filter: options.filter,
118
+ };
119
+
120
+ if (configFile) {
121
+ return maybeBabel({
122
+ ...shared,
123
+ configFile,
124
+ plugins: options.plugins ?? [],
125
+ }) as unknown as Plugin;
126
+ }
127
+
128
+ return maybeBabel({
129
+ ...shared,
130
+ // No config file (or the caller opted out): don't let babel go looking
131
+ // for one -- we provide the whole plugin list inline.
132
+ configFile: false,
133
+ babelrc: false,
134
+ plugins: defaultPlugins().concat(options.plugins ?? []),
135
+ }) as unknown as Plugin;
136
+ }
@@ -0,0 +1,32 @@
1
+ import type { UserConfig } from "tsdown";
2
+
3
+ /**
4
+ * tsdown's config, set up for an Ember v2 library: builds your entry to
5
+ * `dist/*.js` and `dist/*.d.ts` with sourcemaps, cleaning `dist/` between
6
+ * builds and leaving the ember virtual packages to the consuming app.
7
+ *
8
+ * You choose the `entry` and the `plugins` (e.g. `ember()`); any tsdown
9
+ * option you pass wins.
10
+ *
11
+ * ```js
12
+ * import { defineConfig, ember } from "@nullvoxpopuli/ember-rolldown";
13
+ *
14
+ * export default defineConfig({
15
+ * entry: ["./src/index.ts"],
16
+ * plugins: [ember()],
17
+ * });
18
+ * ```
19
+ */
20
+ export function defineConfig(config: UserConfig): UserConfig {
21
+ return {
22
+ sourcemap: true,
23
+ clean: true,
24
+ dts: true,
25
+ // tsdown's extensions are .mjs/.d.mts otherwise, even in type-module
26
+ // packages; exports maps conventionally point at .js/.d.ts
27
+ outExtensions: () => ({ js: ".js", dts: ".d.ts" }),
28
+ // provided by the consuming app
29
+ neverBundle: ["node:*", "@ember/*", "@glimmer/*"],
30
+ ...config,
31
+ };
32
+ }
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import type { Plugin } from "rolldown";
4
+
5
+ // @embroider/core is cjs, so we default-import and destructure.
6
+ import pkg from "@embroider/core";
7
+ const { emberVirtualPackages, emberVirtualPeerDeps, packageName, templateCompilationModules } = pkg;
8
+
9
+ const compilationModules = new Set(templateCompilationModules.map((m) => m.module));
10
+
11
+ /**
12
+ * @param {string} path
13
+ */
14
+ function readJsonSync(path: string) {
15
+ return JSON.parse(readFileSync(path, { encoding: "utf8" }));
16
+ }
17
+
18
+ /**
19
+ * Everything the library declares as a `dependency` or `peerDependency` is
20
+ * resolvable by the consuming app, so we never bundle it.
21
+ */
22
+ function resolvableDependencies(): Set<string> {
23
+ const deps = new Set<string>();
24
+ const manifest = readJsonSync("package.json");
25
+
26
+ for (const name of Object.keys(manifest.dependencies ?? {})) {
27
+ deps.add(name);
28
+ }
29
+ for (const name of Object.keys(manifest.peerDependencies ?? {})) {
30
+ deps.add(name);
31
+ }
32
+
33
+ return deps;
34
+ }
35
+
36
+ /**
37
+ * Keeps the library's declared dependencies and the ember virtual packages
38
+ * (things like `@ember/component`, `@glimmer/tracking`, the template compiler,
39
+ * …) external, so the app that consumes the library resolves them instead of
40
+ * the library bundling copies of them.
41
+ */
42
+ export function emberExternals(): Plugin {
43
+ let deps: Set<string>;
44
+
45
+ return {
46
+ name: "ember:externals",
47
+
48
+ buildStart() {
49
+ this.addWatchFile("package.json");
50
+ deps = resolvableDependencies();
51
+ },
52
+
53
+ resolveId: {
54
+ order: "pre",
55
+ handler(source) {
56
+ // Anything with a protocol (`node:`, virtual modules, …) is not ours
57
+ // to externalize.
58
+ if (source.includes(":")) {
59
+ return null;
60
+ }
61
+
62
+ const pkgName = packageName(source);
63
+
64
+ if (!pkgName) {
65
+ // No package name means a relative import, which we don't deal with.
66
+ return null;
67
+ }
68
+
69
+ if (
70
+ deps.has(pkgName) ||
71
+ emberVirtualPeerDeps.has(pkgName) ||
72
+ emberVirtualPackages.has(pkgName) ||
73
+ compilationModules.has(pkgName)
74
+ ) {
75
+ // `false` tells rolldown to treat the id as external.
76
+ return false;
77
+ }
78
+
79
+ return undefined;
80
+ },
81
+ },
82
+ };
83
+ }
@@ -0,0 +1,65 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ import type { Plugin } from "rolldown";
5
+
6
+ /**
7
+ * Errors the build when a `tsconfig.json` is present without
8
+ * `isolatedDeclarations: true`.
9
+ *
10
+ * This is required: declarations for `.gts`/`.gjs` (template tag) modules can
11
+ * only be emitted by the isolated-declarations pipeline, which reads compiled
12
+ * modules from the bundler's module graph. The `tsc`-based pipeline reads
13
+ * source files from disk, where the compiled modules don't exist, and fails
14
+ * with "Source file not found".
15
+ *
16
+ * Failing fast here -- rather than only when declaration emit happens to run
17
+ * -- keeps the project honest: editors and `tsc`/`ember-tsc` then check the
18
+ * same rules the build enforces (exported values need explicit type
19
+ * annotations, e.g. `export const X: TOC<Sig> = <template>...`).
20
+ *
21
+ * Projects without a `tsconfig.json` (JavaScript libraries) or without the
22
+ * `typescript` package have no declarations to emit, so there is nothing to
23
+ * check.
24
+ */
25
+ export function emberIsolatedDeclarations(): Plugin {
26
+ return {
27
+ name: "ember:isolated-declarations",
28
+
29
+ async buildStart() {
30
+ const tsconfigPath = resolve("tsconfig.json");
31
+
32
+ if (!existsSync(tsconfigPath)) return;
33
+
34
+ let ts: typeof import("typescript");
35
+
36
+ try {
37
+ ts = (await import("typescript")).default;
38
+ } catch {
39
+ return;
40
+ }
41
+
42
+ const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
43
+
44
+ if (error) {
45
+ // Unreadable/unparsable tsconfig: tsc itself will report this with a
46
+ // better message than we can.
47
+ return;
48
+ }
49
+
50
+ // Resolves `extends` chains, so the flag may come from a base config.
51
+ const parsed = ts.parseJsonConfigFileContent(config, ts.sys, dirname(tsconfigPath));
52
+
53
+ if (parsed.options.isolatedDeclarations !== true) {
54
+ this.error(
55
+ `${tsconfigPath} must set "compilerOptions.isolatedDeclarations": true.\n\n` +
56
+ `Declarations for .gts/.gjs (template tag) modules can only be emitted by the ` +
57
+ `isolated-declarations pipeline, which reads compiled modules from the bundler's ` +
58
+ `module graph -- the tsc-based pipeline reads from disk and cannot see them.\n\n` +
59
+ `Isolated declarations require exported values to have explicit type annotations, ` +
60
+ `e.g. \`export const X: TOC<Sig> = <template>...\`.`,
61
+ );
62
+ }
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,162 @@
1
+ import { Preprocessor } from "content-tag";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, writeFile, readdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { Plugin } from "rolldown";
6
+
7
+ const processor = new Preprocessor();
8
+
9
+ /**
10
+ * `.d.ts` files emitted for `.gts` source reference the original `.gts`
11
+ * specifiers. Consumers can't resolve those, so we rewrite them back to
12
+ * extension-less specifiers.
13
+ */
14
+ function fixDeclarationImports(content: string): string {
15
+ return content
16
+ .replace(/from\s+'([^']+)\.gts'/g, "from '$1'")
17
+ .replace(/from\s+"([^"]+)\.gts"/g, 'from "$1"')
18
+ .replace(/import\("([^"]+)\.gts"\)/g, 'import("$1")')
19
+ .replace(/import\('([^']+)\.gts'\)/g, "import('$1')");
20
+ }
21
+
22
+ async function fixDtsExtensionsInDir(dir: string): Promise<void> {
23
+ const entries = await readdir(dir, { withFileTypes: true });
24
+
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+
28
+ if (entry.isDirectory()) {
29
+ await fixDtsExtensionsInDir(fullPath);
30
+ } else if (entry.name.endsWith(".d.ts")) {
31
+ const content = await readFile(fullPath, { encoding: "utf8" });
32
+ const fixed = fixDeclarationImports(content);
33
+
34
+ if (fixed !== content) {
35
+ await writeFile(fullPath, fixed);
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Maps `.gts` -> `.ts` and `.gjs` -> `.js` (so rolldown can identify them as
43
+ * ts/js) and preprocesses `<template>` via content-tag.
44
+ */
45
+ export function emberTransform(): Plugin {
46
+ const virtualFiles = new Set<string>();
47
+
48
+ return {
49
+ name: "ember:transform",
50
+
51
+ resolveId: {
52
+ order: "pre",
53
+ handler(id, importer) {
54
+ if (!importer) return null;
55
+
56
+ const fileName = path.join(path.dirname(importer), id);
57
+
58
+ if (id.endsWith(".gts") && !importer.endsWith(".d.ts")) {
59
+ virtualFiles.add(fileName);
60
+ return {
61
+ id: fileName.replace(/\.gts$/, ".ts"),
62
+ meta: { fileName },
63
+ };
64
+ }
65
+
66
+ if (id.endsWith(".gjs") && !importer.endsWith(".d.ts")) {
67
+ virtualFiles.add(fileName);
68
+ return {
69
+ id: fileName.replace(/\.gjs$/, ".js"),
70
+ meta: { fileName },
71
+ };
72
+ }
73
+
74
+ if (id.endsWith(".d.ts") && importer.endsWith(".d.ts")) {
75
+ const gtsFile = fileName.replace(".d.ts", ".gts");
76
+ if (virtualFiles.has(gtsFile)) {
77
+ return fileName;
78
+ }
79
+ }
80
+
81
+ if (id.endsWith(".ts") && !importer.endsWith(".d.ts")) {
82
+ const gtsFileName = fileName.replace(/\.ts$/, ".gts");
83
+ if (existsSync(gtsFileName) && !existsSync(fileName)) {
84
+ virtualFiles.add(gtsFileName);
85
+ return { id: fileName, meta: { fileName: gtsFileName } };
86
+ }
87
+ }
88
+
89
+ if (id.endsWith(".js") && !importer.endsWith(".d.ts")) {
90
+ const gjsFileName = fileName.replace(/\.js$/, ".gjs");
91
+ if (existsSync(gjsFileName) && !existsSync(fileName)) {
92
+ virtualFiles.add(gjsFileName);
93
+ return { id: fileName, meta: { fileName: gjsFileName } };
94
+ }
95
+ }
96
+
97
+ return null;
98
+ },
99
+ },
100
+
101
+ load: {
102
+ order: "pre",
103
+ filter: {
104
+ id: /\.(ts|js)$/,
105
+ },
106
+ async handler(id) {
107
+ const meta = this.getModuleInfo(id)?.meta ?? {};
108
+ const fileName = meta?.fileName;
109
+
110
+ if (fileName) {
111
+ this.addWatchFile(fileName);
112
+ const source = await readFile(fileName, { encoding: "utf8" });
113
+
114
+ if (source.includes("<template>")) {
115
+ const { code, map } = processor.process(source, {
116
+ filename: fileName,
117
+ });
118
+ return { code, map };
119
+ }
120
+
121
+ return source;
122
+ }
123
+
124
+ return null;
125
+ },
126
+ },
127
+
128
+ transform: {
129
+ order: "pre",
130
+ filter: {
131
+ code: /\.gts/,
132
+ id: /\.(js|ts)$/,
133
+ },
134
+ handler(input, id) {
135
+ if (input.includes(".gts") && id.endsWith(".d.ts")) {
136
+ return input.replace(
137
+ /(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
138
+ (_m, q, p) => `${q}${p.replace(/\.gts$/, ".d.ts")}${q}`,
139
+ );
140
+ }
141
+
142
+ if (input.includes(".gts") && !id.endsWith(".d.ts")) {
143
+ return input.replace(
144
+ /(['"`])((?:\.\.?\/|\/|@|[A-Za-z0-9_\-])[^'"]*?\.gts)\1/g,
145
+ (_m, q, p) => `${q}${p.replace(/\.gts$/, ".ts")}${q}`,
146
+ );
147
+ }
148
+
149
+ return input;
150
+ },
151
+ },
152
+
153
+ writeBundle: {
154
+ async handler(options) {
155
+ const outDir = (options as { dir?: string }).dir;
156
+ if (outDir) {
157
+ await fixDtsExtensionsInDir(path.resolve(outDir));
158
+ }
159
+ },
160
+ },
161
+ };
162
+ }
@@ -8,7 +8,9 @@ A speed-optimized default meta-config for ember projects.
8
8
  npm add @nullvoxpopuli/ember-vite
9
9
  ```
10
10
 
11
- Requires node 24+
11
+ Requires node 24+, and — since these packages ship TypeScript source — a
12
+ modern TypeScript when type-checking: 6+ with `lib` covering `es2025` (e.g.
13
+ `esnext`).
12
14
 
13
15
  ## Usage
14
16
 
@@ -43,34 +45,18 @@ The `ember()` plugin takes the following options:
43
45
 
44
46
  ```ts
45
47
  ember({
46
- // optional production configs
47
48
  production: {
48
- // optional configuration of codeSplitting.groups
49
- // in the vite config.
50
- //
51
- // Docs here:
52
- // https://rolldown.rs/reference/OutputOptions.codeSplitting#groups
53
49
  codeSplittingGroups: [
54
- /* ... */
50
+ /* https://rolldown.rs/reference/OutputOptions.codeSplitting#groups */
55
51
  ],
56
52
  },
57
- // optional babel config
58
53
  babel: {
59
- // defaults to true.
60
- // when babel runs, do so in parallel across many cores.
61
54
  parallel: false,
62
- // optional way to configure when babel is activateed.
63
- // by default, all transforming is oxc, except when babel is needed
64
- // (for things not currently implemented in oxc)
65
55
  include: {
66
- // if a file imports from anything in this array, use babel instead of
67
- // oxc for that file.
68
- //
69
- // For example: ['ember-concurrency', 'ember-intl/format-message'],
70
- whenImporting: [
71
- /* ... */
72
- ],
56
+ whenImporting: ["ember-concurrency", "ember-intl/format-message"],
73
57
  },
74
58
  },
75
59
  });
76
60
  ```
61
+
62
+ Each option is documented on its type.
@@ -1,2 +1,2 @@
1
1
  export { ember } from "./src/ember.ts";
2
- export { maybeBabel } from "./src/maybe-babel.ts";
2
+ export { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";