Repository navigation
compile: Solid "universal" JSX transform for OpenTUI at compile time (moduleName @opentui/solid) + solid-js condition swap #10099
Copy link
Copy link
Closed
Labels
enhancementNew capability or improvementNew capability or improvementparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
Description
Parent: #10107. Related: #4644 (SolidJS bridge), existing crates/perry-hir/src/solid_jsx.rs.
Problem
OpenCode's TUI is 107 Solid .tsx files (packages/tui/src 99, packages/opencode/src 8) with tsconfig "jsx": "preserve", "jsxImportSource": "@opentui/solid" (packages/tui/tsconfig.json:5-6, packages/opencode/tsconfig.json:5-6). Nothing ships them as JSX:
- The official binary transforms them inside
Bun.buildwithcreateSolidTransformPlugin()from@opentui/solid/bun-plugin(packages/opencode/script/build.ts:22,174). bun run src/index.tstransforms them at load time:packages/opencode/src/plugin/tui/runtime.ts:44callsensureRuntimePluginSupport({ additional: keymapRuntimeModules })→node_modules/@opentui/solid/scripts/runtime-plugin-support-configure.js→ensureSolidTransformPlugin()→Bun.plugin(createSolidTransformPlugin())(scripts/solid-plugin.js).
The transform (node_modules/@opentui/solid/scripts/solid-transform.js) is@babel/core.transformAsync(code, { presets: [[babel-preset-solid, { moduleName: "@opentui/solid", generate: "universal" }], [@babel/preset-typescript]] }), i.e. babel-plugin-jsx-dom-expressions in universal mode: elements becomecreateElement(tag)+setProp(el, name, value, prev)insideeffect()for dynamic props, children viainsert(parent, accessor), components viacreateComponent(Comp, props)with getters for dynamic props,mergeProps/spread,use:directives,ref, and text viacreateTextNode— all imported from@opentui/solid.
The same plugin also rewrites solid-js resolution:onLoadswapssolid-js/dist/server.js→dist/solid.jsandsolid-js/store/dist/server.js→store.js, because solid-js'snodeexport condition selects the SSR build (non-reactive) while the TUI needs the client build (node_modules/solid-js/package.jsonexports:node→./dist/server.js,browser/default →./dist/solid.js).
Perry today:perry-hir/src/solid_jsx.rsalready implements a "Solid universal JSX expansion" (createComponent/createElement/spread/mergeProps/memo/use), but it is gated byperry.jsx: "solid"in the package's package.json (crates/perry/src/commands/compile/host_config.rs:92) and hard-wires the runtime module name"perry-solid"(collect_modules.rs:668). Without it, perry lowers TSX through its React-stylejsx()/jsxs()path, which produces the wrong runtime calls for OpenTUI.
Scope
- Parametrize
lower_solid_jsxon the runtime module name and select it fromjsxImportSourcein the nearesttsconfig.json(auto) orperry.jsx: { runtime: "@opentui/solid" }(explicit). OpenCode's packages carry noperrykey, so auto-detection (or a compile flag) is required. - Make the expansion match babel-plugin-jsx-dom-expressions universal output semantically: static vs dynamic prop detection (call/member expressions become effects), children accessors,
ref,on*handlers passed as props,use:*directives, spread ordering, fragments → arrays,<For>/<Show>/<Switch>are plain components, text nodes viacreateTextNode,insertmarkers. Use the real transform as the oracle: runtransformSolidSource(code, { filename, moduleName: "@opentui/solid" })under bun over all 107 files and compare behaviour (not text). - Resolve
solid-jsandsolid-js/storeto the client builds (dist/solid.js,dist/store.js) for this graph, mirroring the plugin's onLoad swap (a resolver condition rule or an explicit alias). Verify@opentui/soliditself resolves to the intended condition (index.bun.jsunder bun,index.jsotherwise) and that@opentui/solid/jsx-runtimeis not pulled in by the fallback React path. - Once compiled ahead of time, the runtime plugin path must become inert (runtime(bun-compat): provide
plugin/Bun.pluginas an inert registrar so OpenTUI's runtime-plugin support installs #10100) so@babel/*,babel-preset-solid,browserslistdrop out of the live set.
Repro / oracle
- Minimal fixture: an
.tsxfile importingrenderfrom@opentui/solidrendering<box><text>{count()}</text><For each={items()}>{(i) => <text>{i}</text>}</For></box>with acreateSignalupdate; compare frames from@opentui/coretest renderer under bun vs the perry binary. - Full:
packages/opencode/src/cli/tui/worker.ts+packages/tui/src/**render the first TUI frame under the native binary.
Acceptance
- All 107 OpenCode
.tsxfiles compile with the universal transform; nojsx()/jsxs()React-runtime calls remain in the emitted HIR for those files. - Fixture frames identical to bun for static props, dynamic props, children accessors, components with getters,
ref,use:directive, spread. -
solid-jsresolves todist/solid.js(reactive) in the OpenCode graph; a test assertscreateSignalupdates re-render. - TUI first frame renders from the native binary (with runtime(bun-compat): provide
plugin/Bun.pluginas an inert registrar so OpenTUI's runtime-plugin support installs #10100, 03, 05).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew capability or improvementNew capability or improvementparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystemCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
Activity
proggeramlug commentedon Sep 12, 2026
Fixed by #10135, landed on
mainvia merge train #10139 at8b282192fa.The close keyword does not fire through a train branch, so closing manually.