@gjsify/rolldown-plugin-solid 0.42.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/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @gjsify/rolldown-plugin-solid
2
+
3
+ A Rolldown (and Rollup/Vite-compatible) plugin that compiles **SolidJS JSX** during gjsify builds, in `universal` generate mode — the only mode whose output contains no DOM.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Why a Babel plugin in an oxc toolchain
8
+
9
+ Solid's JSX is a *compiler*, not a runtime: `babel-plugin-jsx-dom-expressions` turns markup into straight-line calls against a renderer, and no oxc/SWC port of it exists. Rolldown's own transformer cannot stand in — pointed at a `.tsx` with no JSX configuration it defaults to the automatic React runtime and emits `import { jsx } from "react/jsx-runtime"`, which resolves to nothing under GJS.
10
+
11
+ Babel is loaded on the first matching module, so a build with no JSX in it never pays for it.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ gjsify install @gjsify/rolldown-plugin-solid
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Wire it through `package.json#gjsify`, no JS-form config file needed:
22
+
23
+ ```json
24
+ {
25
+ "gjsify": {
26
+ "bundler": {
27
+ "plugins": [{ "name": "@gjsify/rolldown-plugin-solid" }]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ Or directly in a Rolldown / Vite config:
34
+
35
+ ```typescript
36
+ import { solidPlugin } from '@gjsify/rolldown-plugin-solid';
37
+
38
+ export default {
39
+ plugins: [solidPlugin({ moduleName: '@gjsify/gtk-host/solid' })],
40
+ };
41
+ ```
42
+
43
+ ### Options
44
+
45
+ | Option | Default | Meaning |
46
+ | --- | --- | --- |
47
+ | `moduleName` | `@gjsify/gtk-host/solid` | Module the compiler imports the renderer ops from. |
48
+ | `generate` | `universal` | `babel-preset-solid` mode. `dom` emits `document.createElement`, which does not exist on GJS. |
49
+ | `include` | `/\.(m\|c)?[jt]sx$/` | Which modules to compile. A `.ts` file cannot contain JSX. |
50
+
51
+ The target module must re-export **every** member of Solid's `Renderer<NodeType>` under its contract name — `createElement`, `createTextNode`, `insertNode`, `insert`, `spread`, `setProp`, `mergeProps`, `effect`, `memo`, `createComponent`, `render`, `use`. The compiler emits those names literally, so a renderer that exports eleven of the twelve builds fine and fails with `MISSING_EXPORT` on the twelfth — only for the JSX that happens to need it.
52
+
53
+ ## TypeScript
54
+
55
+ The type side is configured separately, and both halves are required:
56
+
57
+ ```json
58
+ {
59
+ "compilerOptions": {
60
+ "jsx": "preserve",
61
+ "jsxImportSource": "@gjsify/gtk-host",
62
+ "noImplicitAny": true,
63
+ "strictFunctionTypes": true
64
+ }
65
+ }
66
+ ```
67
+
68
+ `jsx: "preserve"` is what this plugin needs (`"react"` is refused outright with `jsxImportSource`). Without `noImplicitAny` every JSX element is implicitly `any` and `tsc` exits 0 having checked nothing.
69
+
70
+ ## License
71
+
72
+ MIT
package/lib/index.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Plugin } from 'rolldown';
2
+ /** `babel-preset-solid`'s `generate` modes. Only `universal` can run on GJS. */
3
+ export type SolidGenerateMode = 'dom' | 'ssr' | 'universal';
4
+ export interface SolidPluginOptions {
5
+ /**
6
+ * Module the compiler imports the renderer ops from.
7
+ *
8
+ * Defaults to gjsify's GTK renderer. Any module works as long as it exports
9
+ * every member of Solid's `Renderer<NodeType>` under its contract name.
10
+ */
11
+ moduleName?: string;
12
+ /** Defaults to `universal`, the only mode with no DOM in its output. */
13
+ generate?: SolidGenerateMode;
14
+ /**
15
+ * Which modules to compile. Defaults to `.jsx` / `.tsx` (also `.mtsx`,
16
+ * `.ctsx`). A `.ts` file cannot contain JSX, so widening this is a mistake.
17
+ */
18
+ include?: RegExp;
19
+ }
20
+ /**
21
+ * Compile Solid JSX to renderer calls.
22
+ *
23
+ * Wire it through `package.json#gjsify` so no JS-form config file is needed:
24
+ *
25
+ * ```json
26
+ * "gjsify": { "bundler": { "plugins": [{ "name": "@gjsify/rolldown-plugin-solid" }] } }
27
+ * ```
28
+ */
29
+ export declare function solidPlugin(options?: SolidPluginOptions): Plugin;
30
+ export default solidPlugin;
package/lib/index.js ADDED
@@ -0,0 +1,145 @@
1
+ // SolidJS JSX for Rolldown — the compile step a GTK/GJS Solid app needs.
2
+ //
3
+ // WHY BABEL AT ALL, in a repo that transforms everything else with oxc: Solid's
4
+ // JSX is not a runtime, it is a compiler. `babel-plugin-jsx-dom-expressions`
5
+ // turns `<gtk-box>…</gtk-box>` into straight-line calls against a renderer, and
6
+ // no oxc/SWC port of it exists. Rolldown's own transformer therefore CANNOT do
7
+ // this job — pointed at a `.tsx` with no JSX configuration it defaults to the
8
+ // automatic React runtime and emits `import { jsx } from "react/jsx-runtime"`,
9
+ // which resolves to nothing under GJS.
10
+ //
11
+ // WHY `generate: "universal"`: the default `"dom"` mode emits
12
+ // `document.createElement` plus `template()`/`cloneNode` cloning. There is no DOM
13
+ // under GJS, so that output cannot run at all. `"universal"` emits imports of the
14
+ // renderer ops from `moduleName` instead — measured on
15
+ // `gtk-host/type-tests/jsx/positive.tsx`, five of them:
16
+ //
17
+ // import { createElement as _$createElement } from "@gjsify/gtk-host/solid";
18
+ // import { insertNode as _$insertNode } from "@gjsify/gtk-host/solid";
19
+ // import { setProp as _$setProp } from "@gjsify/gtk-host/solid";
20
+ // import { insert as _$insert } from "@gjsify/gtk-host/solid";
21
+ // import { use as _$use } from "@gjsify/gtk-host/solid";
22
+ //
23
+ // The names are LITERAL — they are the member names of Solid's
24
+ // `Renderer<NodeType>`, and the target module must re-export all of them under
25
+ // exactly those names. A renderer that exports eleven of the twelve builds fine
26
+ // and fails with MISSING_EXPORT on the twelfth, only for the JSX that happens to
27
+ // need it (that was `setProp`).
28
+ //
29
+ // Two further shapes the measurement fixed:
30
+ // - Children are inserted BEFORE properties are set (`insertNode` then
31
+ // `setProp`), so `createElement` never sees a prop. Construct-only properties
32
+ // therefore cannot be handled by an adapter; the host has to defer
33
+ // materialisation (ADR 0027 § Decision 5).
34
+ // - Handlers arrive through `setProp` under their JSX spelling (`onClicked`),
35
+ // not through a separate op.
36
+ const DEFAULT_INCLUDE = /\.(m|c)?[jt]sx$/;
37
+ const DEFAULT_MODULE_NAME = '@gjsify/gtk-host/solid';
38
+ /**
39
+ * The two compiler modules, loaded on first transform rather than at import.
40
+ *
41
+ * Same contract as `@gjsify/rolldown-plugin-deepkit`: the heavy compiler stays
42
+ * uninstantiated for every build that has no JSX in it, and a project that never
43
+ * writes JSX never pays Babel's load time.
44
+ *
45
+ * THE CACHE HOLDS THE MODULES, NOT THE PRESET CHAIN. The chain carries
46
+ * `moduleName` and `generate`, so caching it made both options inert for every
47
+ * plugin instance after the first one in a process — and the failure is silent in
48
+ * the direction that matters: the second bundle imports its renderer ops from the
49
+ * FIRST bundle's module, which builds green and dies at its first import. Only the
50
+ * imports are expensive, and they do not depend on the options.
51
+ */
52
+ let compiler = null;
53
+ function loadCompiler() {
54
+ compiler ??= (async () => {
55
+ const babel = await import('@babel/core');
56
+ // Both presets are CommonJS, so the namespace carries them on `default`.
57
+ // Their types come from `src/babel-presets.d.ts` — neither ships any.
58
+ const { default: solid } = await import('babel-preset-solid');
59
+ const { default: typescript } = await import('@babel/preset-typescript');
60
+ return { babel, solid, typescript };
61
+ })();
62
+ return compiler;
63
+ }
64
+ async function load(moduleName, generate) {
65
+ const { babel, solid, typescript } = await loadCompiler();
66
+ // Babel applies presets LAST-TO-FIRST, so `preset-typescript` listed second is
67
+ // the one that runs first: it strips the annotations and `babel-preset-solid`
68
+ // then sees plain JSX. That is the order Solid's own tooling ships, and it is
69
+ // the one to keep — but it is convention here, not a constraint. MEASURED, 14
70
+ // inputs chosen to break it (as-casts and `satisfies` inside attributes,
71
+ // `<T,>` and `<T extends unknown>` arrows, explicit type arguments on a
72
+ // component, parameter properties, `declare`, `namespace`, overloads, a spread
73
+ // of a cast, a conditional type): both orders emit BYTE-IDENTICAL output, and
74
+ // neither throws. Babel merges every preset's visitors into one traversal, and
75
+ // the two never claim the same node — TypeScript syntax is not JSX and JSX is
76
+ // not TypeScript syntax. Recorded because the reversal is the obvious
77
+ // "cleanup", and a reader who tries it deserves the measurement rather than a
78
+ // warning nothing backs.
79
+ return {
80
+ babel,
81
+ presets: [
82
+ [solid, { generate, moduleName }],
83
+ [typescript, { isTSX: true, allExtensions: true }],
84
+ ],
85
+ };
86
+ }
87
+ /**
88
+ * Compile Solid JSX to renderer calls.
89
+ *
90
+ * Wire it through `package.json#gjsify` so no JS-form config file is needed:
91
+ *
92
+ * ```json
93
+ * "gjsify": { "bundler": { "plugins": [{ "name": "@gjsify/rolldown-plugin-solid" }] } }
94
+ * ```
95
+ */
96
+ export function solidPlugin(options = {}) {
97
+ const moduleName = options.moduleName ?? DEFAULT_MODULE_NAME;
98
+ const generate = options.generate ?? 'universal';
99
+ const include = options.include ?? DEFAULT_INCLUDE;
100
+ return {
101
+ name: 'gjsify-solid',
102
+ transform: {
103
+ // Before every other JS transform: what follows must never see JSX.
104
+ order: 'pre',
105
+ async handler(code, id) {
106
+ // A `\0` prefix is rollup's mark for a module another plugin MINTED,
107
+ // whose id names no file. Compiling one is claiming someone else's
108
+ // module, and the id would be reported back in every diagnostic.
109
+ if (id.startsWith('\0'))
110
+ return null;
111
+ // Match on the PATH, not on the id. Vite appends `?used`, `?import`
112
+ // and `?t=<timestamp>` on the HMR path, and an anchored `\.tsx$`
113
+ // silently declines every one of them — the plugin becomes a no-op
114
+ // and raw JSX reaches the next transform, which is the failure this
115
+ // package exists to prevent. This package advertises Vite support.
116
+ if (!include.test(id.split('?')[0]))
117
+ return null;
118
+ const { babel, presets } = await load(moduleName, generate);
119
+ const result = await babel.transformAsync(code, {
120
+ // The consumer's own Babel config must not reach this: the
121
+ // preset chain above is the contract, and a stray `.babelrc`
122
+ // in a monorepo root would silently change the output.
123
+ babelrc: false,
124
+ configFile: false,
125
+ filename: id,
126
+ sourceFileName: id,
127
+ sourceMaps: true,
128
+ presets,
129
+ });
130
+ const transformed = result?.code;
131
+ if (typeof transformed !== 'string') {
132
+ // Babel returning no code for a file that matched the filter is
133
+ // not a "nothing to do" — it is a silent drop, and returning
134
+ // null here would ship the untransformed JSX onwards.
135
+ throw new Error(`@gjsify/rolldown-plugin-solid: Babel produced no output for ${id}. ` +
136
+ `The file matched ${String(include)} and was expected to contain Solid JSX.`);
137
+ }
138
+ // The map is REAL and handed on: Babel rewrites every line here, so
139
+ // dropping it points every later frame at the compiled output.
140
+ return { code: transformed, map: result?.map ?? null };
141
+ },
142
+ },
143
+ };
144
+ }
145
+ export default solidPlugin;
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@gjsify/rolldown-plugin-solid",
3
+ "version": "0.42.0",
4
+ "description": "SolidJS JSX compiler plugin for Rolldown / Rollup / Vite — universal generate, targeting a gjsify renderer",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "module": "lib/index.js",
8
+ "types": "lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "default": "./lib/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "lib"
17
+ ],
18
+ "scripts": {
19
+ "clear": "gjsify clear lib test.node.mjs test.gjs.mjs tsconfig.tsbuildinfo",
20
+ "check": "gjsify tsc --noEmit",
21
+ "build": "gjsify tsc -p tsconfig.build.json",
22
+ "build:test": "gjsify run build:test:node && gjsify run build:test:gjs",
23
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
24
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
25
+ "test": "gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
26
+ "test:node": "node test.node.mjs",
27
+ "test:gjs": "gjsify run test.gjs.mjs"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/gjsify/gjsify.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/gjsify/gjsify/issues"
35
+ },
36
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/rolldown-plugin-solid#readme",
37
+ "keywords": [
38
+ "rolldown",
39
+ "rollup",
40
+ "vite",
41
+ "solid",
42
+ "solid-js",
43
+ "jsx",
44
+ "gtk",
45
+ "gjs"
46
+ ],
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "@babel/core": "^7.28.4",
50
+ "@babel/preset-typescript": "^7.27.1",
51
+ "babel-preset-solid": "^1.9.9"
52
+ },
53
+ "peerDependencies": {
54
+ "rolldown": "^1.1.4"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "rolldown": {
58
+ "optional": true
59
+ }
60
+ },
61
+ "devDependencies": {
62
+ "@gjsify/cli": "^0.42.0",
63
+ "@gjsify/unit": "^0.42.0",
64
+ "@types/babel__core": "^7.20.5",
65
+ "@types/node": "^25.9.2",
66
+ "rolldown": "^1.1.4"
67
+ },
68
+ "gjsify": {
69
+ "tier": 1
70
+ }
71
+ }