The TypeScript runtime
7. The TypeScript runtime
Section titled “7. The TypeScript runtime”7.1 Bootstrap (generated by the CLI)
Section titled “7.1 Bootstrap (generated by the CLI)”// main.ts: runs on Bun's main threadimport { dlopen, FFIType } from "bun:ffi";const core = dlopen(libPath(), { mt_run: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.void }, mt_exit_code: { args: [], returns: FFIType.i32 } });new Worker(new URL("./app-worker.ts", import.meta.url).href); // the appcore.symbols.mt_run(configPtr, configLen); // never returns until quitprocess.exit(core.symbols.mt_exit_code());7.2 Components, reactivity, compiled JSX
Section titled “7.2 Components, reactivity, compiled JSX”- Reactivity: Solid’s signals in plain JS (
signal,computed,effect), re-exported frommantou-ui. JavaScriptCore’s JIT makes this fast, so there’s no reason to move the reactive graph into native code. - JSX: Bun’s built-in transform is React-style (
jsx()calls), so it doesn’t fit. ABun.pluginrunsbabel-preset-solidwithgenerate: "universal"andmoduleNamepointing at our renderer. The plugin runs indev, inbuildand in the test runner. A faster Oxc-based compiler can come later. - Renderer:
solid-js/universal’screateRenderer, implemented over a JS mirror tree. Solid’s renderer API needs synchronousgetParentNode,getFirstChildandgetNextSibling. Each mirror node holds an id, a parent, its children and its handlers. Every mutation updates the mirror and appends an op.
Status: implemented as described, with three findings.
- Bun resolves
solid-jsto Solid’s server build. The package’snodeexport condition points atdist/server.js, whose effects never re-run. Bun’s runtime plugins can’t redirect bare imports (onResolvenever sees them), butonLoaddoes run for files innode_modules. So Mantou imports the client build by path (runtime/solid/solid.ts), and the plugin rewritessolid-jsimports in Solid’s ownuniversalandstorebuilds and in app.tsxfiles. Apps import Solid’s API frommantou-ui. - Text leaves. Solid uses empty text nodes as placeholders, so the mirror tree has text leaves (
TextLeaf). They send no ops until the core has text; a node inserted next to one is anchored on the next real node. - Where it runs. The Worker’s entry (
runtime/worker.ts) registers the plugin before anything that uses Solid loads, then imports the app;bun testpreloads it (bunfig.toml). Babel adds about 30 MB to the Worker and compiles at start-up: the counter’s first frame appears about 150 ms after launch on Linux. Compiling ahead of time for release builds is still to do.
7.3 Public API sketch
Section titled “7.3 Public API sketch”import { run, Window, View, Text, Button, Scroll, signal } from "mantou-ui";
function Counter() { const n = signal(0); return ( <View style={{ flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }}> <Text style={{ fontSize: 40, fontWeight: 700 }}>{n()}</Text> <Button onPress={() => n.set(n() + 1)}>Increment</Button> </View> );}
run(() => ( <Window title="Counter" width={360} height={240}> <Counter /> </Window>));Primitives (v1): Window, View, Text (with nested spans), Scroll, Image, Input/TextArea, List (virtualized), Canvas (later).
Unstyled behaviour components (v1): Button, Toggle, Checkbox, Slider, Menu, Popover, Dialog, Tooltip. They come with their behaviour, focus handling and accessibility, and the app supplies the look.
7.4 Node lifetime
Section titled “7.4 Node lifetime”- JS allocates node ids from a counter with a free list.
REMOVEonly detaches a node, because Solid memos can put removed nodes back.RELEASEfrees the node and its native resources.- Decided (Q3): a node is released when the Solid owner that created it is disposed (
onCleanupregistered increateElement). AFinalizationRegistryacts only as a safety net.- Development builds count live nodes and warn when the safety net releases a node, because that means a release path was missed.
- Inserting a node whose owner has been disposed is an error in development builds and a no-op in production.
- Status: release on owner disposal is implemented (
runtime/solid/renderer.ts); theFinalizationRegistrysafety net and the development-build checks aren’t yet. - Nodes aren’t freed through GC finalizers alone, because the timing of JavaScriptCore’s garbage collection is unpredictable, and native resources (GPU textures, text layouts) would linger.