ember.nvp 0.6.0 → 1.0.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 +17 -2
- package/.release-plan.json +29 -5
- package/CHANGELOG.md +73 -0
- package/package.json +3 -2
- package/packages/build-tooling-utils/README.md +37 -0
- package/packages/build-tooling-utils/index.ts +2 -0
- package/packages/build-tooling-utils/package.json +42 -0
- package/packages/build-tooling-utils/rolldown.config.js +18 -0
- package/packages/build-tooling-utils/src/extensions.ts +18 -0
- package/packages/build-tooling-utils/src/maybe-babel.ts +146 -0
- package/packages/rolldown/README.md +120 -0
- package/packages/rolldown/index.ts +60 -0
- package/packages/rolldown/package.json +59 -0
- package/packages/rolldown/rolldown.config.js +21 -0
- package/packages/rolldown/src/babel.ts +136 -0
- package/packages/rolldown/src/config.ts +50 -0
- package/packages/rolldown/src/externals.ts +83 -0
- package/packages/rolldown/src/isolated-declarations.ts +65 -0
- package/packages/rolldown/src/transform.ts +162 -0
- package/packages/vite/README.md +7 -21
- package/packages/vite/index.ts +1 -1
- package/packages/vite/package.json +4 -6
- package/packages/vite/src/ember.ts +129 -15
- package/packages/vite/src/test-html.ts +86 -0
- package/src/bases/minimal-app/files/app/router.ts +1 -0
- package/src/bases/minimal-app/files/tsconfig.json +1 -0
- package/src/bases/minimal-app/index.js +2 -2
- package/src/bases/minimal-library/files/README.md +41 -0
- package/src/bases/minimal-library/files/package.json +49 -0
- package/src/bases/minimal-library/files/tsconfig.json +10 -0
- package/src/bases/minimal-library/files/tsdown.config.js +7 -0
- package/src/bases/minimal-library/index.js +158 -2
- package/src/cli/index.js +21 -2
- package/src/cli/questions/layers.js +5 -2
- package/src/cli/questions/project-type.js +0 -5
- package/src/layers/are-the-types-wrong/index.js +74 -0
- package/src/layers/git/files/gitignore +14 -1
- package/src/layers/git/index.js +6 -5
- package/src/layers/github-actions/index.js +12 -8
- package/src/layers/index.js +1 -1
- package/src/layers/inspector-support/index.js +101 -0
- package/src/layers/prettier/files/.prettierignore +4 -0
- package/src/layers/publint/index.js +68 -0
- package/src/layers/qunit/files/tests/unit/macros-test.ts +2 -2
- package/src/layers/qunit/index.js +107 -7
- package/src/layers/qunit/library-files/config/test/babel.config.js +41 -0
- package/src/layers/qunit/library-files/config/test/testem.cjs +26 -0
- package/src/layers/qunit/library-files/tests/test-helper.ts +36 -0
- package/src/layers/qunit/library-files/tests/unit/.gitkeep +0 -0
- package/src/layers/qunit/library-files/vite.config.mjs +6 -0
- package/src/layers/typescript/index.js +43 -7
- package/src/layers/vitest/files/vitest.config.mjs +11 -11
- package/src/layers/vitest/index.js +70 -32
- package/src/utils/babel.js +9 -6
- package/src/utils/fs.js +44 -3
- package/src/utils/inspector-support.js +196 -0
- package/src/utils/npm.js +21 -7
- package/src/utils/remove-types.js +5 -0
- package/src/utils/rewrite-imports.js +157 -0
- package/src/utils/tsdown-config.js +85 -0
- package/src/utils/types.ts +5 -1
- package/test/ember.nvp/base-library.test.ts +236 -0
- package/test/ember.nvp/generator-replace-or-update.test.ts +2 -3
- package/test/ember.nvp/inspector-support.test.ts +179 -0
- package/test/ember.nvp/layer-inspector-support.test.ts +69 -0
- package/test/ember.nvp/layer-publish-checks.test.ts +109 -0
- package/test/ember.nvp/layer-qunit.test.ts +381 -16
- package/test/ember.nvp/layer-vitest.test.ts +250 -0
- package/test/ember.nvp/layers.test.ts +16 -0
- package/test/ember.nvp/library-src-fixtures.ts +81 -0
- package/test/ember.nvp/permutations/app-1.test.ts +3 -0
- package/test/ember.nvp/permutations/app-2.test.ts +3 -0
- package/test/ember.nvp/permutations/library-1.test.ts +3 -0
- package/test/ember.nvp/permutations/library-2.test.ts +3 -0
- package/test/ember.nvp/permutations/shared.ts +239 -0
- package/test/ember.nvp/rewrite-imports.test.ts +235 -0
- package/test/test-helpers/permutate.test.ts +17 -0
- package/test/test-helpers.ts +46 -8
- package/tsconfig.json +1 -0
- package/packages/vite/src/maybe-babel.ts +0 -219
- package/src/layers/qunit/files/run-tests.mjs +0 -30
- package/src/layers/vitest/files/tests/test-helper.ts +0 -5
- package/test/ember.nvp/permutations.test.ts +0 -200
- /package/src/{layers/vitest/files/tests/integration/.gitkeep → bases/minimal-library/files/src/index.ts} +0 -0
- /package/src/layers/{vitest/files/tests/unit → qunit/library-files/tests/rendering}/.gitkeep +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nullvoxpopuli/ember-rolldown",
|
|
3
|
+
"version": "2.0.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,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 ??= 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,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
|
+
}
|
package/packages/vite/README.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/packages/vite/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ember } from "./src/ember.ts";
|
|
2
|
-
export { maybeBabel } from "
|
|
2
|
+
export { maybeBabel } from "@nullvoxpopuli/ember-build-tooling-utils";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nullvoxpopuli/ember-vite",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Meta plugin for ember projects focusing on speed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -27,18 +27,16 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@babel/core": "^7.28.10",
|
|
29
29
|
"@embroider/core": "^4.4.2",
|
|
30
|
+
"@embroider/macros": "^1.20.3",
|
|
30
31
|
"@embroider/vite": "^1.5.0",
|
|
31
|
-
"@
|
|
32
|
-
"oxc-parser": "^0.132.0",
|
|
33
|
-
"oxc-transform": "^0.132.0",
|
|
34
|
-
"zimmerframe": "^1.1.4"
|
|
32
|
+
"@nullvoxpopuli/ember-build-tooling-utils": "workspace:*"
|
|
35
33
|
},
|
|
36
34
|
"engines": {
|
|
37
35
|
"node": ">= 24"
|
|
38
36
|
},
|
|
39
37
|
"devDependencies": {
|
|
40
38
|
"rolldown": "^1.1.2",
|
|
41
|
-
"typescript": "^
|
|
39
|
+
"typescript": "^6.0.3",
|
|
42
40
|
"vite": "^8.0.14"
|
|
43
41
|
}
|
|
44
42
|
}
|