Repository navigation
@solidjs/web@2.0.0-rc.14
Pre-releasePatch Changes
-
ecb68a1: Binding slots: the fill runs once per occurrence, untracked, under the occurrence's owner — as a component body and a template-slot fill do. State created in the fill lives as long as the occurrence; a top-level read is a one-time read (dev:
STRICT_READ_UNTRACKED, naming the fill); getters are the reactive form. Handlers and refs are read once when an element binds and go throughassign, so events delegate, tuples bind and interactions wrap as in client JSX. On the server an array at a handler position is a dev finding (reasontuple) instead of being flattened; onlyrefmerges arrays. Template-slot fills are untracked on every render path and carry the same labelled warning.Breaking:
AttributeSlotis renamedBindingSlot, with no alias, and its return is constrained (SlotOutput<J>/SlotError<M>, both exported) so an array, DOM node, function, async value or$-prefixed key is a type error on both sides. The diagnostic codeATTRIBUTE_SLOT_POSITIONis renamedBINDING_SLOT_POSITION. The fill-shape finding also names async values. -
d4b10b5: Binding slots: a slot property placed as a child (
<strong>{list.remaining}</strong> items left) is a text position. The server emits a comment pair around it,<!--_s:t=<occurrence>:<key>-->…<!--/_s:t-->, with the escaped t=0 value inside on the document face and nothing on the stream face; the client writes the text between the markers from the occurrence's render effect, and a refetch's morph keeps the client's text. Strings and numbers render; nullish and booleans render empty, as a client insert renders them. Any other value is a new client dev finding (BINDING_SLOT_POSITION, reasontext-shape) and clears. The content of<textarea>,<title>,<style>and<script>is not a text position (a comment is literal text there); bindvalue=or a style property instead.Breaking: a slot property as a child used to render nothing and raise the
textfinding; it now renders and binds, and thetextreason is retired. -
2eb6e00: Cancelling a server function or frames stream partway through a chunk now ends it cleanly instead of failing as a malformed stream.
- Chunk headers must be exactly
;0xplus 8 hex digits plus;, and payloads must be valid UTF-8. - A stream that fails to decode is cancelled instead of being left unread.
- The reader frees the memory from a large chunk once it is read.
- Chunk headers must be exactly
-
721eb06: Performance tracks no longer run the attribution cost checks by default (#3739).
enablePerformanceTracks()— which@solidjs/vite-plugincalls in everyvite devsession — now takes a records-only engine hold, so the six cost checks (hotRuns,hotTime,wideDeps,unstableMemos,fanOut,wastedRecompute) are opt-in again. To turn them back on, passenablePerformanceTracks({ attribution: { checks: true } })(orperformanceTracks: { attribution: { checks: true } }in the vite plugin), or take your ownattribution.enable()hold. In dev builds, theinsertchild-resolution effect (which must track each row's resolved child, e.g. every<Show>row of a<For>) is also exempt fromWIDE_SCOPE_DEPS;HUGE_FAN_INstill covers it. -
365f37d: Fix a hydration key miss on a
dynamic()/<Dynamic>element whose prop getter mints a hydration id (a conditional expression over a signal compiles to a condition memo) and whose children include an element (#3741). The client's runtimespreadinsertedchildrenbefore running its attribute effect, while the server'sssrElementread attributes first, so the memo and the child element took each other's ids and the child was rebuilt instead of claimed.spreadnow applies attributes before inserting children — the order a compiled element already uses — andssrElementreads achildrenprop after every attribute regardless of key order. Observable on the client: arefon adynamic()element now runs before its children are inserted, as it does on a compiled element. -
98d35b9: Fix computed keys in compiled SSR
styleobjects (builds withoutserverComponents). A computed key's name got no;, sostyle={{ color: "red", [k]: v }}renderedcolor:redtop:1pxand the browser dropped both declarations. Hydration keeps the server's inline style, so they stayed dropped. A nullish first value also no longer leaves a leading;. Both compilers now pass a multi-entry object to a newssrStyleProperties(name, value, …)helper, which writes the separator only between the entries it writes; a single entry keepsssrStyleProperty. -
b57eb2e: Streamed fragments gated on their stylesheets now reveal under a nonce-based CSP, and no longer stay on the fallback when a sheet loads before its fragment's template is parsed (#3747). The gated links carry
data-dfcinstead of inlineonload/onerrorhandlers, which a strictscript-srcblocks; a capture-phase listener installed by the stream's fragment helpers, from a script that carries the nonce, counts them down. The content template is now written ahead of its links: a cached sheet could fireloadbefore the parser reached the template, and$dfrdropped the reveal. -
3c1d512: frames: A1b + A4 (S-ref) — a slot record's
{$ref}s settle at the host's write, through the response's own data table (FrameHostOptions.resolve(ref, frameId, version, current)); an undelivered key is a pending read the response'sdatachunk settles and itscomplete/errorrejects (L1 — a value that never comes is an error, not a silence); a fresh mount waits for the record to settle, a mounted occurrence's prop pends and holds its value. Codec data tables are per response (keyed by frame id and version). Deleted:ServerComponentHandlerOptions.onStream,STAGED_DATAand the staged tables,FrameHost.resolve,FrameHostOptions.isContainer/FrameHost.isContainer,Frame.rebase, the frame's record dedupe (#refArgsUnchanged,#slotResolvedRefs) — a re-sent record's equality is the fill's per-prop memo's — and the frame's error/root value latches (applied state keyed by record identity).FrameHost.preview/Frame.previewlose theirresolveparameter and stay: the compute-half preview is what stages a refetch's args with the transaction that read it. -
3c1d512: frames: a reveal applies its range (frames-rulings 2.3, A3). A revealed segment's content is applied against the store as it is revealed — its fills mount and the segments whose placeholders it carries reveal inside it, in the same flush and before a reconstructed boundary commits the content — so the frame's flush no longer retries over its segments, and a segment nested in content a pending fill holds no longer waits for a chunk that never comes. The readiness/retry model's second ledger (
#revealed/#fallbackShown) is gone: a reveal's applied state is the content record it applied, a fallback's the gate it materialized, by identity, in the one applied map; whether a segment is shown is the DOM's to say.Frame.isRevealed(segment)is removed. -
3c1d512: frames: A4 (S-record) — the document face declares a slot record at its marker:
_$HY.r["sc:slot:<fid>:<occurrence>"]is a pending promise written with the occurrence's markup and settled with the args by the record's data script (the shape a fragment's<key>_frtakes), so the adopting client awaits a record that trails its range's reveal through the value's own.theninstead of polling the registry (C2 (a2) flips). Output shape: ≈ +32–38 B per document slot record (the resolver helpers are shared with the page's fragment declarations); a sync render (renderToString) writes the settled value as before. Deleted: the #2968setTimeoutre-drain poll andFrameOptions.recordsPending/FrameOptions.drainRecords. -
3c1d512: Frames A5′ — a deferred fragment's placeholder inside a server component's element is the frame's content by rendering, not by adoption (frames-rulings 3.3, ruled 2026-10-06).
solid-js: the document fragment ledger'sfragmentPolicylets a post-done swap proceed when the fragment is owned — itspl-*template is in the document and the integration's ownership predicate_$HY.fa(placeholder)says so — beside the existing claimant case; no hold, no replay for owned fragments._$HY.fr.claim/_$HY.fr.releaseare removed from the published ledger (_$HY.fris{ pending, subscribe });_$HY.fa(placeholder): booleanis the new integration hook.fragmentPendingnow reads a revealed fragment from its swap record (_$HY.v) before its_frstamp: the producer emits the swap script and then the_frsettle in the same batch, so a_$HY.fr.pending()read inside the reveal notification saw the revealing fragment as still pending — a page's last reveal never read as exhaustion and a waiter released on exhaustion waited forever.@solidjs/web(frames client): installs_$HY.faonce (pl.closest("[data-fid]"), minus elements of a boundary disposed in place — C14); deletesclaimRegionFragments, the per-adoption claim set, the cascade's claim half and the release loop (the dev-only rejection report over the region'spl-*templates stays, 0 prod bytes);documentBoundarypends on the intercept's one arrival answer (awaitBoundary) andboundaryWaitersis deleted (G9). A post-done swap into server-component markup no client has adopted yet now lands at once; the adoption that follows finds it in place and reads its declared records synchronously. -
3c1d512: frames: a frame's error is an errored async value (frames-rulings 3.3, A7).
FrameHost.landing(address)rejects with the error record at the response's:errorwrite, so the mount's content node throws to the nearest client<Errored>— the covering<Loading>no longer releases over an empty<solid-frame>. An error after the landing (a later yield failing, a cut-off stream, a refetch's response erroring) errors the node the same way; with no client<Errored>the core halts, as for any uncaught async error. The<Errored>'sresetre-asks: an errored landing is not a landing for a fresh consumer, and the re-read opens a new flight for the same address — the handler records the call behind every address it handles or the document answered (callFor,@internal), and the client re-invokes it through the server-function registry's RPC seam, whose client half now carriescreateServerReference(id). A response for an address whose mounts show an error writes through instead of being staged.frame.errorstill records the error. -
9d89df7: frames: the address is an async source (S-flush) — a mount's covering
<Loading>pends on the bound address's first flush throughFrameHost.landing(address)(content or error), per bound address, so a switch is a new question and the superseded address's late writes release nothing (contract C17 a, c); the store is one response's — a version bump or rebind replaces it wholesale, root included (C6 a1, b2; C7 c); a called occurrence (prop#n) found without its record waits for it instead of being evaluated argless (C18); the document face's reveal cascade syncs the adopting frame — a reveal is an apply (C2 b, C4 d). -
7233451: frames: the assets tier (frames savings pass C5) — the head mirror a segment's
assetsrecord drives (the stylesheet gate, module and typed preloads, inline styles) leaves the eager frames client for the lazy chunk@solidjs/web/frames/assets, loaded through the tier mechanism. A segment whose assets record carries stylesheets or inline styles is not ready while the tier is not resident: the server's fallback stays on screen and the reveal happens at max(tier load, stylesheet load) — no segment reveals unstyled. Once the tier is resident only stylesheets gate; inline styles, modules and preloads apply at the record's arrival, and a segment with none of these never waits on the tier. New export path@solidjs/web/frames/assets(@experimental);InstallOptions.tiersloaders resolveTierModule(a module with or withoutinstall()). -
7233451: frames: the binding-slot tier (
@solidjs/web/frames/bind,@experimental) — the_s:*marker parsers, consumer discovery, the morph's owned-position arms, the per-frame consumer sets and the fill's binding leave the eager frames client as a lazy chunk;assignleaves with them. A marker met while the tier is absent holds the frame under frames-rulings 3.1 (hydration-done waits; the install's flush binds). The delegated-event replay window is closed by a bare_hkthe server stamps on each document-face element with an_s:on:*position (4 bytes each, nothing else stamped): the tier marks the element completed at the bind and drains the queue, so a click before or during the hold reaches the bound handler. Data occurrences now mount with the current record. -
3c1d512: A server
<Loading>inside a server component that fails after the first flush renders the server's outcome into its fragment instead of a blank (C12 (c), frames-rulings 3.3): the nearest server<Errored>'s fallback for the error, at the<Loading>'s position (asked through the boundary error handler's newoutcomemode; a<Loading>between passes the question up); with no server<Errored>the error escapes the component — the frame's own:erroron the stream face (an unkeyederrorchunk), a frame-addressed{ type: "error", fid, error }op on the document face'ssc:livechannel (only the owning adopted boundary applies it) — and the position keeps the boundary's own fallback._frstill rejects and the keyed error chunk still rides (the diagnostics). Outside a server component nothing changes (the blank the client twin renders fresh over).HydrationContext.registerFragment's resolver gains a third argument (escaped?: { frame?: string }) and the context an internalframeId. -
3c1d512: frames: one server sweep lands as one frame (C13). The sink collects the
hole/attrre-emissions one sweep produces and emits them as one{ type: "ops", ops: [...] }chunk on the stream face (one wire line) and onesc:liveop of the same shape on the document face; a sweep that changes one binding emits that member alone, as before. The client maps the unit to one record map and applies it as one write — one hole pass, oneframe:applied(the hole pass now announces once per flush, not once per hole). The document op log flattens a unit into its members (last value per hole). Additive wire (FrameChunkgains theopsmember; RFC addendum inframe-streams-rfc.md). -
3c1d512: An adopted frame occurrence claims its server markup by re-entering hydration the way a streamed
<Loading>resume does (frames-rulings 3.1 / 3.2, the savings plan's A2 — S-hold's window form).solid-js:hydrateWindow(id, fn, scope?)is factored out of a streamed boundary's resume and reached assharedConfig.hydrateWindow(@internal): the keys underidgathered into the registry (the capturedscopepair when anotherhydrate()root replaced the live one, #2917), hydrating on for the synchronous window, the current owner the claim owner (a render the window forces elsewhere is a client render, #3504), the owner the window's snapshot and live scope when none is open — so a write during a late claim is held and replays once the claim is over, and a late claim no longer re-marks the root's scope.sharedConfig.claimRoots(@internal) is typed: the claimant declares a range that may be detached around its window.holdBoundarystays the registration; the resume path is unchanged in behaviour.@solidjs/web(frames):claimRenderis the window — onecreateOwner({ id: prefix })and the call — instead of a registry of its own gathered by walking the range, a hydrating flag flipped throughsharedConfig's setter (which reset hydration-done and re-ran its completion from outside the runtime), and a hand-over of keys from the root registry:gatherClaimsandhasPendingFragmentare deleted (the window gathers by the producer prefix and always engages).adoptBoundarycaptures the registry/gather pair it adopts under so a claim made long after — under the frame's hold, at a fragment's reveal — gathers against the root that holds the frame.@solidjs/web:gatherHydratable's prefix-scoped gather selects its keys natively ([_hk^="…"]) instead of sweeping every_hkand filtering in JS — it now runs once per adopted occurrence, not only per late resume.
-
9d89df7: frames: a
datachunk is under the store's version guard like every other chunk — a superseded response's late data lands nowhere, never in the table that is now the current response's (contract C5 a, b, e). -
7233451: frames: drop the three dead
#slotNodeswrites for data occurrences — the only reader (the zombie check) skips data occurrences, so the consumer-element arrays were built and never read -
9d89df7: frames (dev): a server
<Loading>fragment inside an adopted server component that rejects on the server is reported — the client shows what the server rendered for that outcome and invents no error state of its own (contract C12 c, client half). -
9d89df7: Hydration-done counts the frames client's holds (frames-rulings 3.1, ruled): an adopted occurrence the frame has not claimed yet — waiting for its args record or a
{$ref}'s data — registers as a pending boundary through the same registration a streamed<Loading>resume takes (sharedConfig.holdBoundary, internal), soonHydrationEndandisHydrationInProgress()mean the same thing with or without server components (contract C3 a). -
7233451: frames: the live wire moves into a lazy tier (
@solidjs/web/frames/wire, the sixth chunk). The connection alive()loop's response runs under — the open-frame count its end is judged by (a death with frames open vs a completion), the join of a second loop onto an address already live, the supersession cancel,connection.ended/cancel— the have-list ledger per mount with its encoder, and the resume shape (Last-Event-ID+X-Frame-Have) load with the chunk. The eager client keeps the arm that awaits the tier's residency before a live response's body is read, so a live connection without the chunk cannot happen; the chunk's import starts at thelive()call, before its first fetch (preload-at-call, through the new optionalresponseHandler.onLive()hook the sf client fires), and a non-live call never loads it. One accepted degraded case: content applied before the chunk was resident kept no ledger, so that first connect sends no have-list (and no ordinal) and receives a full snapshot.Public surface:
Frame.have?()is removed from theFrameinterface (the ledger is the tier'shaveOf(frame),@internal);encodeHaveListmoves fromframe-transportinto the tier (@internal;decodeHaveListand theFRAME_HAVE_*constants stay); the@solidjs/web/frames/wireexport path is added;ServerFunctionsClientConfig.responseHandlergains the optionalonLive();ServerComponentHandlergainsonLive().Pins:
consistency/tier-wire-preload.specandserver-functions-live-loop.spec— the latter driveslive()'s reconnect loop from the consumer's side (first-connect failure, the fail-fast 4xx set and 408 / 425 / 429 /Retry-After, the cursor asLast-Event-ID, theonlinewake,return()and a caller's signal at every phase,live(GET(fn)), the handler's local answers), closing the loop's branch-coverage gap.Size: frames eager −965 min / −305 br (10.89 KB); pages −349 / −335 br eager; the
wire.jschunk 1,922 / 934 (lazy; the live page loads it at its firstlive()call). Caps ratcheted to measured + 10 B. -
3c1d512: frames: a plain (non-
live) server component response ends at a streaming bound. A response whose content reads a standing source — a generator memo, a projection — used to stay open until the source settled, which for a source that never returns was never, with none oflive's reconnect semantics. The producer now ends it aftermaxYieldsemitting sweeps past the first flush (default 64) ormaxDurationMsafter the first flush (default 30 000 ms) — and when the request'ssignalaborts after the first flush — emitting{ type: "complete", bound: "yields" | "time" }before the body closes, and tears the render down quietly (sources returned, no abandonment finding). Both are new options onFrameStreamOptions(renderServerComponent,renderToFrameStream,serverComponentResponse); aliveresponse is never bounded. The client stores:boundbeside:completeand, in dev, warns once per cut-off naminglive()as the declared way past the bound.createFrameSinkgains an optional fourthhooksargument (onYield) and itsend(bound?)takes the bound. Additive wire; RFC addendum inframe-streams-rfc.md. -
1b9ceb6: Frames client: an address switch re-arms the shell gate and rebinds the frame in the pass that sees the new address (the follow's compute), not in an effect's run. Under the hold model a switch delivered while the previous switch's gate still pends lands the binding in the frame that gate holds, and an effect's run is stashed with that frame — behind the very gate the rebind would release, so a second switch mid-flight never bound to the live call. The gate signal takes
ownedWrite(the re-arm and a warm rebind's release are written from the pass). Both the call-driven mount and the document-adoption face. -
203ab1a: Server-component content for a call a boundary is showing now lands with the transition that read it, instead of morphing in when the response arrives. A refetch, or a single-flight region, is staged: the call resolves to a binding naming the staged version, and the mount commits it when that binding reaches it. The slot args go first, under the transition, so a fill deriving optimistic intent over a server arg never reads the old arg. The markup lands at the commit. Single-flight regions show when the integration's cache takes the mutation's slice, so any cache that subscribes to flight data drives it, not just Solid Router.
An address switch's new content lands at the same commit: the frame re-binds in the run of the effect that follows the address, while the switch gate re-arms in its pass and settles on the new address's first content through a frameless registration on the host — so a second switch mid-flight still binds to the live call, and the morph no longer lands beside siblings the transaction still holds.
Behaviour change: a response for a showing call that no reader mounts is never shown. Previously it morphed every mount of the address on arrival.
-
7233451: frames: C4 — the regions tier. Nested server-content regions (
{$frame}slot args resolved to<solid-frame>region elements with frames bound over them) leave the eager frames client for the lazy chunk@solidjs/web/frames/regions(a new@experimentalexport path), loaded through the tier mechanism: the server announcesregionswhere it mints one; a record naming a region met before the tier is resident waits — a fresh mount in the held set (the frame's hold registered under frames-rulings 3.1 on the adopt path, the server interior on screen), a mounted occurrence's new record pending in the store until the install's flush.InstallOptions.tiersnow types a tier's module as the new exportedTierModule(its exports are the tier's appliers;install()stays optional). Frames eager −1,078 B minified / −237 B brotli; the page scenarios −1,083 B minified; the non-SC scenarios unchanged. -
7233451: Frames client, residue pass 1 — the D list (dead / incidental code off the eager client; frames residue pass §2 rows 2b, 2c, 2e, 2f, 2g).
documentAddressis gone: an adopted mount's address comes with its binding (every serialized reference resolves to the call's binding), and a mount with none — the per-function placeholder rendered directly — binds the function id.slotsForkeeps one dispose map: a live range binding'sinsertlives under the fill's own owner where there is one, else under an owner registered the same way, so a re-call disposes whichever path the previous invocation took. The zombie heuristic and the#slotNodesmap it read are gone — the morph relocates ranges among siblings and recreates nothing (DR-5), so "mounted" is the frame's set.#revealSegmenthas one path: a frame created withoutFrameOptions.revealreveals through a default seam that inserts the content at once (no boundary) — the content is applied as it is revealed, as on the boundary path — and a placeholder missing its closing comment is not revealed (dev error;#showFallbackalready treated it so).handler.showingno longer brands the bare per-function component: the reference a reader holds at t=0 is the bootstrap's binding, branded there. The capture arm (contentHTML+ the last-unmount capture) stays:lifecycle-matrix/remountpins it. -
7233451:
FRAME_HAVE_HEADERandFRAME_HAVE_BUDGETleave the@solidjs/web/framesclient entry's export list (frames residue pass §2 row 5a). They stay exported from@solidjs/web/frames/server, where the header is read; the client sends the have-list itself through the handler'sresumeand nothing consumed the names off the client entry. Removed public surface,@experimental. -
7233451: The lean re-ask (frames residue pass §3.3). The server-function client hands its response handler the call it dispatched as a thunk —
ctx.retryonresponseHandler.handle,info.retryonresponseHandler.intercept— the same reference, arguments, declared shape and per-call options, so aGET-declared read stays a GET by construction. The frames client's re-ask for an errored boundary'sreset(frames-rulings 3.3) is that thunk:callFor(address).retry(). Gone with it: the frames client'sreaskinstaller and its reads of the RPC slot and the metadata brand, andcreateServerReference(id)on the client half of the server-function RPC slot (getServerFunctionRPC(),@internal), which nothing reads any more.callFor(@internal) now returns the recorded call with itsretry. TheresponseHandlertype inServerFunctionsClientConfigdocuments the existinginterceptseam alongsidehandle, both carryingretry. -
7233451: Frames client: every slot fill is placed by
insert— the fill's output has the core's lifecycle (created under the fill's owner, claimed in place under hydration throughinsertExpression's claim pass, disposed with the owner) instead of a frame-side copy of it. The Solid binding's static path (normalizeSlotContent,isReactiveContent, the in-placesettletest) and the frame runtime's own range writer (#replaceRange) are gone; a static fill is oneinsertof a non-function value (no effect created), a reactive one binds the range as before.createFrame/createFrameElement: the frame element IS the range — the never-used comment-marker range mode of the runtime is removed.Slot(FrameOptions.slots,@experimental) changes shape: a callback owns its range — it places or binds its output beforectx.range.endoverctx.existing— and its return value is no longer read (before: returned nodes were placed by the frame;undefinedclaimed). A slot range missing its end marker (FRAME_MARKER_CORRUPTED) is left as the server rendered it instead of being filled at the parent's end. -
7233451: frames: residue 4 — a pending
{$ref}read is owned by the decode table. A slot record's{$ref}to a key its response has not delivered yet is answered by that response'screateJSONDataTablewith a promise the table owns (markeds = 0, carryingc, the callbacks run when it settles), settled by the key'sdatachunk through the table's ownapplyand rejected byclose(error)when the response ends (L1 — a value that never comes is an error where it is read, not a silence); a version bump drops the superseded table and its reads. The frames host keeps only therecord.pendingcount (a fresh mount waits for its record, the covering<Loading>pends on the landing alone) and one fan-out helper;pendingRef/settleWait/ the per-store waits are gone.FrameHostOptions.closeData(frameId, version, error?)(new,@experimental) tells the integration a response ended; the shared host closes that response's table. The transport awaitsprepareDatabefore aslotchunk whose args carry a{$ref}, as it does before adatachunk, soresolvealways has a table. Frames eager client −505 min / −174 br (34,272 / 11,367 → 33,767 / 11,193); the lazy decode chunk +420 / +161. -
3c7631a: ABANDONED_FLIGHTS no longer fires for a frames slot arg that every re-shipped record re-reads while it is still pending (#3852). Dev builds mark the per-key slot-arg memo with an internal option that the check skips; the memo's flights are still recorded and still counted in
feedback(). -
7233451: Frames: the server-announced tier mechanism (frames savings pass §2, Phase B — the seam alone; no tier is cut yet, every capability stays eager).
The server learns at render time which frames-client capabilities a response or a document needs — it mints each feature — and announces their names so the client can start the tier's import in parallel with the content instead of at first use: on a frame stream, the
X-Frame-Tiersresponse header (the tiers the sync render pass minted) and, for a tier first needed after the head left, an in-bandtiersmember on the next chunk out (FrameChunk.tiers, additive); on a document, the_$HY.r["sc:tiers"]hydration record (cumulative, re-written at each new mint) plus a<link rel="modulepreload">per tier whose chunk URL the integration gave (frameTransformDirectResult(value, { id, args, tierUrls })— the server does not know the client's chunks; absent, the record alone carries the name). The sink'sneeds(tier)runs at the mint sites: a binding-slot position read (bind), a nested server-content region (regions), an assets chunk (assets), a traced container in a slot arg (trace), aliveresponse (wire). A syncrenderToStringdocument announces nothing (no shared render slot); the client detects.The client (
@solidjs/web/frames):installServerComponents(host?, { tiers })takes the tier → loader map (() => import(...), the module exportinginstall()), reads the document's record and starts each named load;applyFrameResponsereads the header before the body andchunk.tiersat each chunk, and adatachunk awaits the tiers it names before it decodes (its node tree needs them). A fresh mount whose tier is not resident — a data occurrence'sbind, a region-carrying record'sregions— waits like a recordless called occurrence: on the adopt path that wait is one more reason in the frame's registered hold (frames-rulings 3.1 — the event-replay window stays open, hydration-done waits), on the stream path the record stays pending in the store; the readiness check itself starts the load when nothing announced it (detection is the fallback), and the install flushes every live frame so the held occurrence mounts with the record it was held on. A name with no loader is resident (eager), which is every name today.Measured before written (edited dist copies,
scripts/size): frames eager client +473 B min / +145 B br (≤ +150 budget; the plan's ≈ +100 estimate, ×2.5 pre-estimate ≈ +250); pages +478 min; every non-server-component scenario 0 B. Frames server dist +793 B min;@solidjs/web's own server entry 0 B. -
7233451: frames: the trace tier's update-site wait. A MOUNTED occurrence's new slot record whose args carry a
{ $tr }container-trace marker while the trace tier is absent (a live slot op minting the page's first trace after the shell, a refetch adding a projection arg) now stays pending in the store: the live binding keeps the args it shows, the tier load starts, and the install's flush applies the record as an args change with the marker materialized. Before,#syncSlotsguarded only the fresh mount (needsTrace) and the regions tier at the update site (needsRegions), so such a record was pushed into the live binding raw — the marker read as the value. Mirrors the regions tier's update-site wait. -
7233451: frames: the container-trace materializer is the frames client's traces tier —
@solidjs/web/frames/trace, loaded through the server-announced tier mechanism (prepareTier("trace")), so the store engine leaves every server-component page that never meets a trace (page base −6.6 KB brotli, page live −6.7 KB; frames eager −83 B).solid-js/internal/container-traceis a newsolid-jsentry carryingmaterializeContainerTrace(marker, claiming?)(the store engine reached through@solidjs/signals, the hydration dispatchwithStoreHydrationand the patch protocol read back fromsolid-js); the materializer leavessolid-js's main andsolid-js/internalentries. The eager frames client keeps the trigger: the loader entry, the held-set predicate (an adopt-time record whose args carry a{ $tr }marker while the tier is absent is held under frames-rulings 3.1 — its server interior on screen, hydration-done waits — and mounts with the record it was held on, a replacement applying as an args change), and theclaiminghint (FrameHostOptions.revive(value, claiming?)), which keys the materializer's parked backlog on the claim again: a fresh mount reads the fold of its whole backlog at once. -
e44b2e4: Hydration: a
<Loading>aroundlazy()whose module is still loading when hydration starts no longer moves its server-rendered nodes once the module lands. The boundary claims those nodes on its late resume, but the enclosing insert had tracked its region as empty, so it re-inserted them, which moved connected nodes and blurred a focused input. While hydration is in progress, an insert whose region is untracked now leaves the incoming nodes in place when every one of them is already a child of its parent. Once hydration completes, inserts behave as before. Fixes #3749. -
01e80a6: The hydration bootstrap marks every promise the inline payload files under
_$HY.ras handled when it lands, so a serialized async read that rejected under an<Errored>(which already rendered its fallback) no longer raisesunhandledrejectionin the browser. A consumer reading the record still receives the rejection. -
be82cf3: Observe: one interaction frame per event, not per listener.
InteractionReftakes an optionalevent; frames with the sameeventre-enter one interaction record, which stays joinable for the event's dispatch and is recorded at the next task. The web runtime keys its frames by the DOM event, and@solidjs/webexportsdispatchAsInteraction(e, fn)so a listener outside the runtime (a router'sdocumentclick handler) joins the frame of the same event —fn()in production builds. One anchor click is now one interaction record carrying both theonClickwork and the navigation (#3754). -
4f67697: Breaking: only
onfollowed by an uppercase letter (onClick,onPointerDown) is an event handler. Lowercaseon*names (onclick,onmouseover) are plain attributes everywhere:- Both compilers compile
onclick={expr}like any other attribute (setAttribute, reactive whenexpris dynamic) instead of binding a delegated or native event, and SSR renders it as an escaped attribute instead of dropping it. A leftover 1.xon:click={fn}is likewise a plain namespaced attribute (it previously compiled toaddEventListener(":click", fn)). @solidjs/webspread/assignset lowercaseon*keys as attributes, the server spread walk renders them, anduseHeadapplies lowercaseon*attributes (camelCase handler names stay skipped).ssrAttributeescapes a function value instead of interpolating its source raw.@solidjs/htmland@solidjs/helements wrap a function passed to a lowercaseon*in a getter like any other attribute; onlyonXxxandrefare exempt.@solidjs/htmlcomponents follow@solidjs/h's rule: every zero-argument function prop,onXxxhandlers andrefincluded, is a getter, so a component handler must declare its event argument (onClick=${e => …}).- New dev-only check
LOWERCASE_EVENT_ATTRIBUTE(added to theDiagnosticCodeunion): warns once per attribute name when a function is set on a lowercaseon*(oron:) attribute, naming the camelCase handler to use.
- Both compilers compile
-
41fdf96:
reportRequestFailure(error, event)(@solidjs/web, server) reports a failure that fails a request outside any render or server function the runtime reports, such as a middleware throw that a framework's request handler catches. The ambient server error hook hears it askind: "request",handling: "failed", with the request event, once per error object, and its return is ignored. With no hook registered the failure goes toconsole.error, as a render that fails before its shell does.ServerErrorSite.kind(solid-js) gains"request". On the client the function is a no-op.A synchronous throw out of
renderToString, or out ofrenderToStream's first pass (and sorenderToFrameStreamandserverComponentResponse), is now reported to the server error hook askind: "render",handling: "failed"before it is rethrown, where it previously reached the caller without the hook hearing it. A request handler that catches it and callsreportRequestFailureadds nothing; the hook hears it once, as the render's. -
53ef0e6: Single-flight delivery now settles slice entries the server folded while still pending before handing the envelope to the flight-data consumers. A collector may fold values that are still in flight (the router's collector returns its preloads'
querypromises as they stand), and the codec streams those after the response head; since the body decode resolves on the first chunk, the mutation call resolved, and a router action settled (busy state cleared,onSettledran) before the refreshed route data existed.deliverFlightData, shared by the plain client and the frames client, now waits for every pending entry (allSettled, so a failed read stays with the cache entry it seeds) before running consumers. -
2656284:
createSSRResponseno longer hangs when a streamed render ends before its shell flushes (#3719). A render that fails pre-shell (onErrorhearshandling: "failed") or is aborted through itssignalnow resolves with a bodyless 500, or a redirect when aLocationis already on the response stub, and the stub is committed. A render that succeeds with an empty document resolves with an empty 200. The promise still never rejects. -
9338c00:
createSSRResponseover a stream result now tears the render down when nobody will read it (#3768). Cancelling the resolved body (the client went away, a HEAD request whose body the host cancels) disposes the render throughrenderToStream's disconnect path, as cancelling itsreadabledoes: every async source still being pulled is returned,onCleanupruns, and nothing more is written. This emitsSSR_STREAM_ABANDONEDwithdata.reason: "consumer". A pre-flushLocation(a bodyless redirect) tears the render down the same way, as the JSDoc already said, but without the finding, because nobody left. Previously both only dropped writes, and the render ran on until its sources happened to end. The render's observe record settles as"abandoned"in both cases. -
7addcc6: Fix two SSR failures when the shell suspends above an
<Errored>(a lazy memo throwingNotReadyError, the router's flash-decode shape). A<Loading>below the boundary whose content settled before the shell's suspension did lost its content: the fallback shipped and nothing swapped it, because the fragment inlined into a shell that did not yet hold its placeholder. And a hole that reached a suspension through a returned accessor ({props.children}resolving to an<Errored>whose subtree is pending — alazy()route with no<Loading>between, for one) re-read its expression on every retry, re-creating every component above the boundary — state and pending sources included, so a component owning its pending source never converged. The retry now resumes the accessor that suspended. -
6f77b1b: Fix the end of a streamed render racing a suspended shell. A
<Loading>fragment settling while the shell was still suspended (a lazy memo throwingNotReadyErrorabove it) could empty the fragment registry before the shell's root holes re-pulled, and the render flushed its serializer then: everything the re-pull serialized was dropped from the hydration data, and the awaited form (renderToStream(...).then) completed with an empty document — or, with an<Errored>holding the suspended subtree, never completed. The end of the response now waits for pending root holes. -
537e945: SSR validates dynamic attribute and tag names: a spread key the HTML parser would not read back as one attribute name (whitespace, quotes,
<,>,/,=, control characters, or empty) is dropped, as the client'ssetAttributerefuses it; other names are written as is (&is no longer escaped to&, which the parser keeps literally in a name). A<Dynamic>/ssrElementtag that is not one element name throws, as the client'screateElementdoes. -
7233451: Add
dynamicComponent, the component-only sibling ofdynamic(client and server entries). Same contract, semantics and hydration shape asdynamic— the two share one implementation — but its source type excludes tag names, and it never references the element runtime:dynamicmust be able to render a tag, so onedynamicon a page retainscreateElement,spread, the prop-collection helpers and the SVG/MathML tables for everyone;dynamicComponentnever does. It is the documented way to mount a server component (dynamicComponent(() => getStory(id))); a server-component page mounted through it sheds ≈ 2.2 KB brotli.dynamicis unchanged. -
8d66ae5: Server bundles that don't serialize no longer retain the serializer's plugin set. Five module-level statements that Rollup shakes but Rolldown (Vite) and esbuild keep are now side-effect free to every bundler: the frozen
DEFAULT_WEB_PLUGINSarray is/* @__PURE__ */-annotated, serovalFeatureflags are read on use, the stub gap-fill header set is built on first use, the event-stream heartbeat'snew TextEncoder()is annotated (and no longer built at import time bydist/server.js), and the flash-cookie matcher is a regex literal. A server bundle importing onlyisServerorgetRequestEventdrops from ~11.7 KB to ~1.6 KB minified (seroval bundled; Rolldown), andrenderToStringbundles shed ~260–380 B. No behavior or API change. -
Updated dependencies [4b4c2cb]
-
Updated dependencies [21ff8f8]
-
Updated dependencies [3c1d512]
-
Updated dependencies [9a213bb]
-
Updated dependencies [b0c6e6a]
-
Updated dependencies [da84bd9]
-
Updated dependencies [3c1d512]
-
Updated dependencies [3c1d512]
-
Updated dependencies [3c1d512]
-
Updated dependencies [9d89df7]
-
Updated dependencies [7233451]
-
Updated dependencies [759a9b6]
-
Updated dependencies [e345dc7]
-
Updated dependencies [41fdf96]
-
Updated dependencies [87a3862]
-
Updated dependencies [7addcc6]
- solid-js@2.0.0-rc.14