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.
- package/.github/workflows/ci.yml +17 -2
- package/.release-plan.json +88 -4
- package/CHANGELOG.md +57 -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 +115 -0
- package/packages/rolldown/index.ts +56 -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/define-config.ts +32 -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 +6 -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 +232 -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 +103 -0
- package/test/ember.nvp/layer-qunit.test.ts +376 -15
- 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 +20 -7
- 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,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
|
+
}
|
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";
|