partial-solidjs
v0.0.1
Published
A state-preserving renderer for prefix-growing streaming Solid/TSX.
Readme
partial-solidjs
State-preserving renderer for prefix-growing streaming Solid/TSX. Solid ships neither an HMR runtime nor a cross-render
reconciler, so this package owns both gaps: a createSignal shim that survives recompiles, and an in-place DOM reconcile
for append-only streamed frames.
Input
import { SolidRenderer } from "partial-solidjs";
const renderer = await SolidRenderer.create(document.getElementById("preview"));
renderer.pushCode('import { createSignal } from "solid-js";\nexport default function App() {\n const [count, setCount] = createSignal(0);\n return <button onClick={() => setCount(count() + 1)}>{count()}');Output
createSolidTsxCompiler()(Node,@babel/core) andcreateBrowserSolidTsxCompiler()(@babel/standalone) strip TypeScript and run the Solid JSX transform in a single pass;partial: truecompletes the prefix throughpartial-tsx'snormalizeGeneratedTsxfirst.SolidRenderermounts viasolid-js/web.pushCodeappends a streaming frame,renderreplaces the buffer, andfinishinstalls one clean reactive root.
State preservation
SolidStatePreserver re-binds createSignal through a Blob-URL shim of solid-js, so the app's signals are created by
the renderer and a recompile reattaches them instead of resetting.
- Cells are indexed by source-order
createSignal(call. A frame with the same signature reuses the previous cells; a changed signature resets them. - A cell is
touchedonce the app calls its setter. Only touched cells keep their value — an untouched cell re-seeds from the newest frame's initializer on every pass, so a truncatedcreateSignal("")converges to the final value. - Signals created after commit (lazy children) fall through to the real
createSignalso they cannot alias a preserved cell.
Corner Cases
- Streaming reconcile. Solid's effects capture their own fresh nodes, so swapping trees every token destroys hover,
focus and in-flight clicks.
patchFramereconciles instead: keep the committed nodes, patch their text/attributes, and adopt the newly streamed-in tail. Gated onisFramePatchable(committed children are a structural prefix of the staged ones) and on the frame reusing the same signal cells.finish()drops the kept root. - Visible-regression hold. A streamed frame that drops text length or structural element count versus the committed one is held back. Only a setter running inside a DOM interaction releases the hold — a timer or fetch callback cannot commit a shorter partial.
- Focus. A full-swap commit replays focus via
captureFocusWithin/restoreFocusWithin(child-index path plus text selection). A structure-shifting frame can land the caret on a different element; that positional limit is accepted. - Signature blind spots.
getSolidSignalSignaturemasks comments/strings/templates first, but JSX text is lexed like code (an apostrophe can open a phantom string and swallow a latercreateSignal() and template literals are opaque backtick-to-backtick. Tracked in #1208.
Test
bun test lib/partial-solidjs