Release RC packages (rc) - #3697
Open
github-actions[bot] wants to merge 1 commit into
Open
github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
changeset-release/next
branch
14 times, most recently
from
September 29, 2026 15:23
dd06243 to
f4654b0
Compare
github-actions
Bot
force-pushed
the
changeset-release/next
branch
from
September 29, 2026 15:45
f4654b0 to
6eaa70e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to next, this PR will be updated.
nextis currently in pre mode so this branch has prereleases rather than normal releases. If you want to exit prereleases, runchangeset pre exitonnext.Releases
@solidjs/[email protected]
Patch Changes
serverComponents(SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamicclassorstylenow compiles to a whole-attribute_$ssrElementAttributehole instead of a value inside the template's quotes, so a server component'sclass={{ selected: filters.all }}can mark the class name a client attribute slot owns;ref/on*positions compile, as before, to one guarded_$ssrClaimhole per element, which now emits_s:on:*/_s:refmarkers for slot reads (the_bndbehavior-claim marker is gone). A spread element compiles its namedref/on*(<button {...rest} onClick={row.go} ref={row.el} />, wherever they sit relative to the spreads) to the same claim map —{ click: expr, ref: [a, b] }, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (<b {...a} onClick={go} {...b}>→{ 1: { click: go } }) and handed to_$ssrElementas a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client'sspread/merge()do (the last source that has the key owns it,undefinedincluded). Arefbeside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercasedon*remainder (onClick→click);on:/oncapture:are not 2.0 syntaxes and are no longer special-cased. Both compilers share theattributeSlotsserver-components fixture; plain SSR output is unchanged.<textarea>with a dynamicvalue/defaultValueno longer desyncs hydration ids for the siblings after it (Solid 2.0 - Textarea hydration bug #3691). The server folds the value into the textarea's text content, and that fold was taking the_$scopehydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like theinnerHTML/textContentredirects (innerHTMLfrom a call expression consumes an extra hydration id — the next element is left unclaimed #3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.refnow runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration ([2.0] A ref's attribute write survives hydration but is overwritten on client creation #3685).@solidjs/[email protected]
Patch Changes
serverComponents(SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamicclassorstylenow compiles to a whole-attribute_$ssrElementAttributehole instead of a value inside the template's quotes, so a server component'sclass={{ selected: filters.all }}can mark the class name a client attribute slot owns;ref/on*positions compile, as before, to one guarded_$ssrClaimhole per element, which now emits_s:on:*/_s:refmarkers for slot reads (the_bndbehavior-claim marker is gone). A spread element compiles its namedref/on*(<button {...rest} onClick={row.go} ref={row.el} />, wherever they sit relative to the spreads) to the same claim map —{ click: expr, ref: [a, b] }, duplicate refs merged, a duplicate handler last-wins as on the template path — keyed by the index of the source each attribute sits before (<b {...a} onClick={go} {...b}>→{ 1: { click: go } }) and handed to_$ssrElementas a seventh argument, a thunk the runtime reads only inside a server component's render, so plain SSR never evaluates the expressions (they used to drop with no marker and no finding); the runtime settles each handler position in source order, as the client'sspread/merge()do (the last source that has the key owns it,undefinedincluded). Arefbeside a lone spread goes through the sources loop under the option (a dynamic spread call is thunked there, as it is beside any other attribute). The event name is always the lowercasedon*remainder (onClick→click);on:/oncapture:are not 2.0 syntaxes and are no longer special-cased. Both compilers share theattributeSlotsserver-components fixture; plain SSR output is unchanged.$keyon an intrinsic element with a spread (<li $key={id} class="todo" {...attrs}>) now follows the same rule as on a template element — SSR compiles it to the_keyattribute the frame morph matches keyed elements by, and a DOM compile strips it. It previously passed through the spread path unrenamed, so server markup carried a literal$keyattribute and keyed morphs lost identity. Babel already behaved this way; the sharedkeyedElementsfixtures pin the spread case for both.<textarea>with a dynamicvalue/defaultValueno longer desyncs hydration ids for the siblings after it (Solid 2.0 - Textarea hydration bug #3691). The server folds the value into the textarea's text content, and that fold was taking the_$scopehydration-id reservation a dynamic child hole gets — but the client writes the value as a plain property effect that never allocates an id, so every component after the textarea hydrated one id off and its updates targeted detached DOM. The fold is now treated like theinnerHTML/textContentredirects (innerHTMLfrom a call expression consumes an extra hydration id — the next element is left unclaimed #3015): opaque content, no id reservation, in both the Babel plugin and the native compiler.refnow runs after its attributes and spread are applied, and still before its children. A ref's own attribute writes now survive client creation the way they already survived hydration ([2.0] A ref's attribute write survives hydration but is overwritten on client creation #3685).@solidjs/[email protected]
Patch Changes
@solidjs/[email protected]
Patch Changes
@solidjs/[email protected]
Patch Changes
classobject values rejectingundefinedat the type level.ClassValue's record form wasRecord<string, boolean>, soclass={{ active: props.active }}with an optional prop did not type-check, which 1.xclassListallowed. The runtime already treatsundefinedas off.$key?: string | numberis declared on intrinsic elements (JSX.CustomAttributes), the entity identity the frame morph matches keyed server elements by. Both compilers already handled it (SSR →_key, DOM strips it); TypeScript rejected it.@solidjs/[email protected]
Patch Changes
@solidjs/[email protected]
Patch Changes
656f0df: Fix
createOptimisticStorewrites not reverting after a failed action while the attribution engine is enabled (createOptimisticStore overlay is not reverted after a failed action while attribution is enabled (rc.10 regression) #3687). TheOPTIMISTIC_REVERTEDcheck called the node's comparator detached from the node; store slot nodes share one comparator that readsthis._host, so the check threw inside the settle before the drop notified subscribers. The comparator is now invoked as the node's method.6958367: A tracked read served an active optimistic override is lane work, not a transaction entry (Reading a visible optimistic value in Show holds an independent signal update #3698). The reader is a lane member — its pass publishes a derived override, displays at once, and is promoted or reverted with the action — and lane membership never holds a synchronous write (the Existing and newly revealed latest readers display different values #3460 ruling: "a held lane is basically a micro transition from the outside… we wouldn't hold a sync write on a transition. Lanes are the same"). So an unrelated synchronous write that re-runs such a reader publishes at once,
isPending()on it is false, and the action stays open; entry remains for reads served held or superseded truth (A29, A18 (c)).A
<Show>over an optimistic value broke this through its children: the compiler emits a memo inside thewhengetter, soShow's condition memo owns a child, and a lane pass over a memo that owns children parked the previous children as a transaction zombie (2.0.0-rc nested render effect cleanup run too early #3404) — which queued the memo as the action's pending node "for the zombies alone" and stamped it, so its next mainline recompute re-entered the hold and adopted the unrelated write (drag()stayed at the committed value whilelatest(drag)moved andisPending(drag)was true, for the action's lifetime). The same mechanism Card disappears from keyed <For> during overlapping optimistic actions with an empty <Show> #3662 fixed for effects, now for memos: a memo's lane pass parks a lane frame (CONFIG_LANE_FRAME), retired when the lane's queue applies the pass — in the pass's flush for a lane that is not held, at the release for one that is — and the memo is neither queued nor stamped for the action's commit.d21c2be: Wide store folds that delete keys (or add many) no longer mutate the committed backing in place. A backing that served as a prototype overlay is a V8 prototype object, and below V8's descriptor limit every in-place add or delete on it costs O(keys) — a 400-key keyed record churning 100 keys per commit paid 4.7 ms per step. The commit now rebuilds the backing for such folds and swaps it in like a clone-path draft (0.22 ms per step); pure rewrites keep the in-place overlay flatten, and containers past the descriptor limit stay in place where V8 already makes the ops O(1). (2.0.0-rc.10 (performance, V8 only): a keyed projection record of a few hundred keys pays O(keys) for every key a derive adds or deletes — the overlay commit mutates an object V8 has made a fast-mode prototype #3689)
f1b0776: Reading a store key an open action did not write no longer holds the reader with that action (Reading an unchanged store property holds an independent synchronous update #3688). The store's container-level hold gate entered a deriving pass into the transaction for any key of a held container; a memo reading an unchanged
store.stablebeside an independent signal was held until the action settled, and the signal'sisPendingwent true. The gate now consults the fold's written-key record (wk) forget,hasand descriptor reads: a key the fold never wrote or deleted reads the same from either backing and is served committed with no entry — the same per-key precision node reads already had.ownKeys,$TRACKanddeep()stay container-wide; optimistic families, chained backings, folds with no trap record and array-length writes keep the whole-container hold.d0e487b:
STRICT_READ_UNTRACKEDnow fires in two places it was skipped (STRICT_READ_UNTRACKED is skipped during hydration and in lazy() component bodies #3675). In@solidjs/signals,read()served a snapshot-scope reader the captured value and returned before the strict-read check, so a component body's direct read stayed silent during the hydration pass and only warned after client navigation; the check now runs first. Insolid-js,lazy()rendered the loaded component through a bareuntrack()instead ofcreateComponent, so its body had no component label and its direct reads were never checked — and, in dev/observe builds, its owner carried no component name for diagnostics. It now renders throughcreateComponent; production output is unchanged. The warning's console line also names the value that was read when it has a name — a signal'snameoption or the store key:Reactive value "count" read directly in <Child> will not update.(thenodeNamefield already carried it).ad1ecc5: Server components: attribute slots are rebuilt as one slot per data context, bound per position; the spread shape and behavior claims are gone. A slot renders one of two things — markup, placed as
<props.x />, or attribute values: a called slot's result is a plain object the server template reads by property at positions (class={row.rowClass},hidden={row.removed},checked={row.done},onInput={row.toggle},ref={row.el}). Each read marks the element (_s:class="row#0001:rowClass",_s:on:input="row#0001:toggle",_s:class="list:allDone=completed"for a class-name read) and the client binds exactly those positions: it runs the fill once per occurrence, writes the values that change per position, dispatches events to the current handler, firesrefonce, and a morph skips the positions a fill owns. One call is one occurrence, grouped by data context, consumable across many elements; a call repeated within a render (a component prop getter re-evaluated per read) is one occurrence and one record — by$keywhen given, by structural args otherwise, so$keyis optional and names an entity for state that must follow it across responses. On the document face the fill runs at t=0 so values are in the HTML before JavaScript and hydration binds the same nodes; on the stream face reads yield stand-ins and the record carries the args.The rule, in one sentence: a slot property is a JSX attribute value, whole, and nothing else. A stand-in that is stringified, used in an expression (
Symbol.toPrimitive; reasoncoerced), placed as text, or reached inside template quotes renders nothing on either face and reportsATTRIBUTE_SLOT_POSITION(dev), so a misuse shows on the first render rather than the first refetch. A stand-in passed in another slot's argument — at the top or nested in plain objects and arrays ({ nested: { x: row.done } },[row.done]; aMap,Setor class instance is not walked) — is not data the server has: the arg carriesundefinedat that path on both faces (the record, and the document face's t=0 fill reads the same), with a finding (reasonarg,data.pathwhen nested). A cyclic arg crosses the border as a cycle (three walks used to overflow the stack on it; the border walktoBorderForm— every document-face serialization — is copy-on-write with no per-node bookkeeping on ordinary data, measured at or below its previous cost, and falls back to a memoized clone once a cycle is met past depth 16, with replacements reused so a shared generator is seated once). A value reachable at two arg paths crosses as one (one seat for one generator, the identity the input had); a stand-in shared between two paths is reported once, at its first. On the runtime spread path, a stand-in at aprop:*key is a finding (reasonprop; the compiled form dropsprop:*as SSR always has). Fill output keys are the fill's, save an explicit reserved set ($-prefixed, digit-leading,length,slice,t/h/p,then,constructor/toString/valueOf/toJSON): a key that shadows anArray.prototypemethod (filter,at,map) binds as data on both faces. The frame client reports the otherwise-silent failures — an element whose markers can never bind because no fill resolves for the prop, or a called occurrence's args record never arrived (reasonorphan,data.whyfill/record, once per occurrence), and a fill that is not a function or returns a DOM node or a non-object where data was read (reasonfill-shape). Handler positions get one listener per element and event, dispatching to the current fill's handlers; a rebind that releases a key clears the attribute it wrote; an occurrence's end detaches the listeners it attached, so a kept un-keyed element carries one listener (not one per occurrence that bound it, firing twice) and a dropped occurrence's handler never runs through its disposed fill. On the document faceclaimsis armed on a render context derived from the page's for the component's subtree, so the page's elements after a server component keep the pre-slot spread walk. Namedref/on*on a spread element (<button {...rest} onClick={row.go} />) bind like every other handler position:ssrElementtakes the compiled claim map as an optional seventh argument (a thunk keyed by source index, read only under an armed render context) and reads it — and the spread's own handler keys — throughssrClaim's logic (arrays of refs, merged duplicates, handler tuples, theserver-localfinding). A handler position settles in source order, as the client'sspread/merge()do: the last source that has the key — a spread or a named attribute at its position, whatever the value,undefinedincluded — owns it, and a nullish owner binds nothing (<b {...rest} onClick={cond ? row.go : undefined}>binds nothing on both sides whencondis false); refs merge whatever their order; one marker per position (see the compiler changeset). A merged array ref (ref={[a, b]} ref={c}) binds every entry on the template path too. The lazy-asset resolution cache is created on the root render context, so alazy()under a derived context (a Loading boundary, a server-owned frame) and one outside it share one cache — one resolver call per module per request. Under theserverComponentscompiler option a dynamicclass/styleon an intrinsic element compiles tossrElementAttribute, soclass={undefined}writes nothing where template quotes wroteclass=""(nullish is "not set", as for every other attribute).@solidjs/webnow shipsskills/server-components/SKILL.md(the package'sfiles) with the rule, the fill idiom and the diagnostic's reasons.Surface added:
AttributeSlot<P, J>from@solidjs/web/frames—(props: P & { $key? }) => J,propsoptional whenPis empty; the_s:*markers,SLOT_VALUE/slotValue/isSlotValue,SLOT_MARKER,SLOT_FACE_*from@solidjs/web's server entry as the runtime surface; theATTRIBUTE_SLOT_POSITIONdiagnostic code (reasonsspread,stringified,coerced,inline,text,markup,server-local,reserved-key,arg,prop; client:orphan,fill-shape). Removed: the_bndbehavior-claim marker,CLAIM_PROP,BEHAVIOR_CLAIM_DROPPED(a server-local function at a handler position is nowATTRIBUTE_SLOT_POSITION), the framepropsoption andFrameHostOptions.delegate/FrameOptions.delegate. The client frame's binding reads slot values in the render effect's compute phase, so a fill of getters tracks each position's own reads.fe1eb68: The route the document arrived on, declared by the router with the call it already uses.
NavigationRef.initialonOBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (atdefaults to0, the document's own navigation start; a router mounted late passes its own), takes nofrom, and settles itcommittedwithwrites: 0when the frame closes, so the first"navigation"record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out offeedback().navigations. On the server, where there is no engine, the server entry'swithOriginfiles the ref on the render, and the request's"render"record carries it asRenderEvent.route({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New typeRenderRoutefrom@solidjs/web.NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) capturesOBSERVE.attribution.currentOrigin()in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.formatOriginrenders the initial declarationinitial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.0bf5758: Observe tier:
OBSERVE.include(owner)and a body viewer's"call"record.OBSERVE.include(owner)re-admits an owner subtree under an excluded one;isExcludedanswers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a laterexclude/includeon the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).OBSERVE.records.subscribe(type, listener, { bodies?: boolean })— a listener that reads the live handles an emitter pays per record to take asks for them;OBSERVE.records.observed(type, "bodies")is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptionsexported from@solidjs/signalsandsolid-js). Accepted for any type; meaningful today for"call"."call"record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets nolive.requestand the transport's ownlive.response, as before. With it,live.requestis the request as dispatched (final url andRequestInit,prepareRequestapplied), built into aRequestof the listener's own — with the body only when it is astring,URLSearchParams,FormData,Blob,ArrayBufferor a view of one, and without it for aReadableStreamor an async iterable (a streaming upload);live.responseis an unreadclone()taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object whenclone()refuses (an already-read response a configuredfetchhanded over); the request is absent when the address is relative and there is nolocationto resolve it against, and when theRequestconstructor rejects what the configuredfetchaccepted — nothing taken for the record can fail the call.CallEvent.namecarries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).[email protected]
Patch Changes
5bef430: Move
createErrorBoundary,createLoadingBoundaryandcreateRevealOrdertypes tosolid-js/internal; they remain runtime exports ofsolid-jsbut are no longer part of its public types (solid-js types: index.d.ts re-exports createErrorBoundary, createLoadingBoundary and createRevealOrder, which hydration.d.ts strips as @internal #3709)d0e487b:
STRICT_READ_UNTRACKEDnow fires in two places it was skipped (STRICT_READ_UNTRACKED is skipped during hydration and in lazy() component bodies #3675). In@solidjs/signals,read()served a snapshot-scope reader the captured value and returned before the strict-read check, so a component body's direct read stayed silent during the hydration pass and only warned after client navigation; the check now runs first. Insolid-js,lazy()rendered the loaded component through a bareuntrack()instead ofcreateComponent, so its body had no component label and its direct reads were never checked — and, in dev/observe builds, its owner carried no component name for diagnostics. It now renders throughcreateComponent; production output is unchanged. The warning's console line also names the value that was read when it has a name — a signal'snameoption or the store key:Reactive value "count" read directly in <Child> will not update.(thenodeNamefield already carried it).fe1eb68: The route the document arrived on, declared by the router with the call it already uses.
NavigationRef.initialonOBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (atdefaults to0, the document's own navigation start; a router mounted late passes its own), takes nofrom, and settles itcommittedwithwrites: 0when the frame closes, so the first"navigation"record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out offeedback().navigations. On the server, where there is no engine, the server entry'swithOriginfiles the ref on the render, and the request's"render"record carries it asRenderEvent.route({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New typeRenderRoutefrom@solidjs/web.NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) capturesOBSERVE.attribution.currentOrigin()in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.formatOriginrenders the initial declarationinitial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.0bf5758: Observe tier:
OBSERVE.include(owner)and a body viewer's"call"record.OBSERVE.include(owner)re-admits an owner subtree under an excluded one;isExcludedanswers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a laterexclude/includeon the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).OBSERVE.records.subscribe(type, listener, { bodies?: boolean })— a listener that reads the live handles an emitter pays per record to take asks for them;OBSERVE.records.observed(type, "bodies")is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptionsexported from@solidjs/signalsandsolid-js). Accepted for any type; meaningful today for"call"."call"record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets nolive.requestand the transport's ownlive.response, as before. With it,live.requestis the request as dispatched (final url andRequestInit,prepareRequestapplied), built into aRequestof the listener's own — with the body only when it is astring,URLSearchParams,FormData,Blob,ArrayBufferor a view of one, and without it for aReadableStreamor an async iterable (a streaming upload);live.responseis an unreadclone()taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object whenclone()refuses (an already-read response a configuredfetchhanded over); the request is absent when the address is relative and there is nolocationto resolve it against, and when theRequestconstructor rejects what the configuredfetchaccepted — nothing taken for the record can fail the call.CallEvent.namecarries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).Updated dependencies [656f0df]
Updated dependencies [6958367]
Updated dependencies [d21c2be]
Updated dependencies [f1b0776]
Updated dependencies [d0e487b]
Updated dependencies [ad1ecc5]
Updated dependencies [fe1eb68]
Updated dependencies [0bf5758]
@solidjs/[email protected]
Patch Changes
@solidjs/[email protected]
Patch Changes
5bef430: Move
createErrorBoundary,createLoadingBoundaryandcreateRevealOrdertypes tosolid-js/internal; they remain runtime exports ofsolid-jsbut are no longer part of its public types (solid-js types: index.d.ts re-exports createErrorBoundary, createLoadingBoundary and createRevealOrder, which hydration.d.ts strips as @internal #3709)829f981: Fix
classobject values rejectingundefinedat the type level.ClassValue's record form wasRecord<string, boolean>, soclass={{ active: props.active }}with an optional prop did not type-check, which 1.xclassListallowed. The runtime already treatsundefinedas off.ad1ecc5:
dynamic: a kept resolution's address delivery no longer trips the dev owned-scope write guard when the source is a memo that already settled the server-component call (todos = createMemo(() => getTodos()),dynamic(() => todos()),refresh(todos)in an action — the multi-flight shape; the hydrated document's first refetch takes the same path). The delivery then runs insidedynamic's own compute rather than a promise microtask; the per-site address signal is now created withownedWrite, since nothing in that compute reads it back. Before, dev builds threwREACTIVE_WRITE_IN_OWNED_SCOPEinto the nearest error boundary on the first refetch.1ad0be0: Fix SSR
classandstyleobjects rendering a leading separator when their first entry is skipped (class=" on",style=";top:1px").ssrClassNameandssrStyleadded the separator by loop index; it now depends on what was already written.ad1ecc5: Server components: attribute slots are rebuilt as one slot per data context, bound per position; the spread shape and behavior claims are gone. A slot renders one of two things — markup, placed as
<props.x />, or attribute values: a called slot's result is a plain object the server template reads by property at positions (class={row.rowClass},hidden={row.removed},checked={row.done},onInput={row.toggle},ref={row.el}). Each read marks the element (_s:class="row#0001:rowClass",_s:on:input="row#0001:toggle",_s:class="list:allDone=completed"for a class-name read) and the client binds exactly those positions: it runs the fill once per occurrence, writes the values that change per position, dispatches events to the current handler, firesrefonce, and a morph skips the positions a fill owns. One call is one occurrence, grouped by data context, consumable across many elements; a call repeated within a render (a component prop getter re-evaluated per read) is one occurrence and one record — by$keywhen given, by structural args otherwise, so$keyis optional and names an entity for state that must follow it across responses. On the document face the fill runs at t=0 so values are in the HTML before JavaScript and hydration binds the same nodes; on the stream face reads yield stand-ins and the record carries the args.The rule, in one sentence: a slot property is a JSX attribute value, whole, and nothing else. A stand-in that is stringified, used in an expression (
Symbol.toPrimitive; reasoncoerced), placed as text, or reached inside template quotes renders nothing on either face and reportsATTRIBUTE_SLOT_POSITION(dev), so a misuse shows on the first render rather than the first refetch. A stand-in passed in another slot's argument — at the top or nested in plain objects and arrays ({ nested: { x: row.done } },[row.done]; aMap,Setor class instance is not walked) — is not data the server has: the arg carriesundefinedat that path on both faces (the record, and the document face's t=0 fill reads the same), with a finding (reasonarg,data.pathwhen nested). A cyclic arg crosses the border as a cycle (three walks used to overflow the stack on it; the border walktoBorderForm— every document-face serialization — is copy-on-write with no per-node bookkeeping on ordinary data, measured at or below its previous cost, and falls back to a memoized clone once a cycle is met past depth 16, with replacements reused so a shared generator is seated once). A value reachable at two arg paths crosses as one (one seat for one generator, the identity the input had); a stand-in shared between two paths is reported once, at its first. On the runtime spread path, a stand-in at aprop:*key is a finding (reasonprop; the compiled form dropsprop:*as SSR always has). Fill output keys are the fill's, save an explicit reserved set ($-prefixed, digit-leading,length,slice,t/h/p,then,constructor/toString/valueOf/toJSON): a key that shadows anArray.prototypemethod (filter,at,map) binds as data on both faces. The frame client reports the otherwise-silent failures — an element whose markers can never bind because no fill resolves for the prop, or a called occurrence's args record never arrived (reasonorphan,data.whyfill/record, once per occurrence), and a fill that is not a function or returns a DOM node or a non-object where data was read (reasonfill-shape). Handler positions get one listener per element and event, dispatching to the current fill's handlers; a rebind that releases a key clears the attribute it wrote; an occurrence's end detaches the listeners it attached, so a kept un-keyed element carries one listener (not one per occurrence that bound it, firing twice) and a dropped occurrence's handler never runs through its disposed fill. On the document faceclaimsis armed on a render context derived from the page's for the component's subtree, so the page's elements after a server component keep the pre-slot spread walk. Namedref/on*on a spread element (<button {...rest} onClick={row.go} />) bind like every other handler position:ssrElementtakes the compiled claim map as an optional seventh argument (a thunk keyed by source index, read only under an armed render context) and reads it — and the spread's own handler keys — throughssrClaim's logic (arrays of refs, merged duplicates, handler tuples, theserver-localfinding). A handler position settles in source order, as the client'sspread/merge()do: the last source that has the key — a spread or a named attribute at its position, whatever the value,undefinedincluded — owns it, and a nullish owner binds nothing (<b {...rest} onClick={cond ? row.go : undefined}>binds nothing on both sides whencondis false); refs merge whatever their order; one marker per position (see the compiler changeset). A merged array ref (ref={[a, b]} ref={c}) binds every entry on the template path too. The lazy-asset resolution cache is created on the root render context, so alazy()under a derived context (a Loading boundary, a server-owned frame) and one outside it share one cache — one resolver call per module per request. Under theserverComponentscompiler option a dynamicclass/styleon an intrinsic element compiles tossrElementAttribute, soclass={undefined}writes nothing where template quotes wroteclass=""(nullish is "not set", as for every other attribute).@solidjs/webnow shipsskills/server-components/SKILL.md(the package'sfiles) with the rule, the fill idiom and the diagnostic's reasons.Surface added:
AttributeSlot<P, J>from@solidjs/web/frames—(props: P & { $key? }) => J,propsoptional whenPis empty; the_s:*markers,SLOT_VALUE/slotValue/isSlotValue,SLOT_MARKER,SLOT_FACE_*from@solidjs/web's server entry as the runtime surface; theATTRIBUTE_SLOT_POSITIONdiagnostic code (reasonsspread,stringified,coerced,inline,text,markup,server-local,reserved-key,arg,prop; client:orphan,fill-shape). Removed: the_bndbehavior-claim marker,CLAIM_PROP,BEHAVIOR_CLAIM_DROPPED(a server-local function at a handler position is nowATTRIBUTE_SLOT_POSITION), the framepropsoption andFrameHostOptions.delegate/FrameOptions.delegate. The client frame's binding reads slot values in the render effect's compute phase, so a fill of getters tracks each position's own reads.ad1ecc5: Frames: a refetch of a server-component call that a boundary is already showing now settles when its response has applied, not at the response header. The header is not an answer for a showing call — until the new content lands the boundary still shows the previous render — so
isPending(source)stays true through the refetch and ayield refresh(source)inside an action holds its transaction (and any optimistic write in it) until the refetched slot args are on screen, matching what a single-flight mutation already does. Cold mounts and switches to an address nothing shows keep header-time resolution.fe1eb68: The route the document arrived on, declared by the router with the call it already uses.
NavigationRef.initialonOBSERVE.attribution.withOrigin: a router wraps the work that establishes its initial match (building its context) instead of a location write, on both sides. On the client the attribution engine opens the frame at the time origin (atdefaults to0, the document's own navigation start; a router mounted late passes its own), takes nofrom, and settles itcommittedwithwrites: 0when the frame closes, so the first"navigation"record (NavigationEvent.initial: true) names the route the page loaded as — the pageload's route pattern, which every navigation but the first already had. It is a declaration, not a timing: kept out offeedback().navigations. On the server, where there is no engine, the server entry'swithOriginfiles the ref on the render, and the request's"render"record carries it asRenderEvent.route({ name, to, params }, read from the ref when the render settles) — the name a consumer gives the request (http.route) where the URL would scatter one page across as many names as it has parameters. New typeRenderRoutefrom@solidjs/web.NavigationRef.interaction: a router that awaits between the request and the write (guards or loaders resolved in its core before it publishes the location) capturesOBSERVE.attribution.currentOrigin()in the request and hands it back on the ref, and the write it publishes later joins the click as if it had been synchronous. Declared beats ambient: the key's presence is the declaration, and an interaction on the stack at write time is used only when the key is absent.formatOriginrenders the initial declarationinitial navigation to /users/:id (/users/42). Prod artifacts byte-identical; the observe tier's client artifacts byte-identical.ad1ecc5: JSX typings:
$key?: string | numberis declared on intrinsic elements (JSX.CustomAttributes), the entity identity the frame morph matches keyed server elements by. Both compilers already handled it (SSR →_key, DOM strips it); TypeScript rejected it.0bf5758: Observe tier:
OBSERVE.include(owner)and a body viewer's"call"record.OBSERVE.include(owner)re-admits an owner subtree under an excluded one;isExcludedanswers by the nearest marked ancestor, so an observer that renders the app inside its own shell (<DevToolbar><App /></DevToolbar>) can hide its chrome and still see the app. One mark per owner — a laterexclude/includeon the same owner replaces it — and a mark belongs at the owner's creation (the engine caches each node's verdict on first read).OBSERVE.records.subscribe(type, listener, { bodies?: boolean })— a listener that reads the live handles an emitter pays per record to take asks for them;OBSERVE.records.observed(type, "bodies")is that emitter's second gate, true only while such a listener of the type is installed (RecordSubscribeOptionsexported from@solidjs/signalsandsolid-js). Accepted for any type; meaningful today for"call"."call"record's bodies are taken only under that opt-in — a plain listener (an APM adapter, the performance tracks) gets nolive.requestand the transport's ownlive.response, as before. With it,live.requestis the request as dispatched (final url andRequestInit,prepareRequestapplied), built into aRequestof the listener's own — with the body only when it is astring,URLSearchParams,FormData,Blob,ArrayBufferor a view of one, and without it for aReadableStreamor an async iterable (a streaming upload);live.responseis an unreadclone()taken before the transport's decode. The clone is not taken for an event-stream response, is cancelled at settle for a deferred result (a generator's stream — the branch would buffer every chunk for nobody), and falls back to the transport's object whenclone()refuses (an already-read response a configuredfetchhanded over); the request is absent when the address is relative and there is nolocationto resolve it against, and when theRequestconstructor rejects what the configuredfetchaccepted — nothing taken for the record can fail the call.CallEvent.namecarries the reference's metadata name (the compiler's dev-only seed, or an explicit label, which survives to production).644eaf3: New
"request"record onOBSERVE.records(CallRequestEvent,CallRequestListener; augmentingHostRecordTypesthroughsolid-jsbeside"call"): one per server-function request the browser sent, delivered at the send — after argument serialization andprepareRequest, immediately before the transport'sfetchreceives the request — so a network panel can show a call that is still in flight, or one that never settles, which the"call"record (delivered at settle) cannot.{ side: "client", id, name?, at, method: "GET" | "POST", origin? }:id,name,methodandoriginas on the call'sCallEvent;atisperformance.now()at the send (not the call's dispatch —CallEvent.atis when the call was made, and the gap is what building the request cost);sideis the client's today, the server half (the request arrived, ahead of its"invocation") additive later, the way"frame"has two halves.CallLiveis one object across a call's records — documented and pinned: thelivehanded to the"request"listener is, by identity, thelivehanded to the"call"listener at settle, filled in as the call proceeds (argsfrom the start,requestat the send,responseandresult/errorat settle — on every settle, whether or not a"call"record goes out, so a"request"listener alone reads the outcome off the object it holds). An in-process consumer joins a call's request to its settle by identity — the wayoriginjoins a record to the interaction's — with no id-plus-time join and no sequence number. It is one mutable object and oneRequestshared by both records: treat it as read-only, and read the request's body throughlive.request.clone()so the other record's listener still can."call"or"request"is observed; a"request"record is built only ifobserved("request")and a"call"record only ifobserved("call")were true then (explicit now — a"request"listener alone gets no settle record). Each record is delivered to the listeners installed when it fires, so a listener subscribing mid-call can receive a"request"and never that call's"call", or a"call"without its"request".bodieson a"request"subscription governslive.requestalone: the request is reconstructed whenobserved("call", "bodies") || observed("request", "bodies"), set onlivebefore the"request"record is delivered; the response clone stays the"call"listener's opt-in.atis read before the reconstruction, so its cost is not in the send's timestamp.prepareRequestthrew) emits no"request"— nothing was handed tofetch— only its"call"settle; a call whose fetch never settles emits"request"only; a call an integration answered locally (intercept) emits neither. Afetchthat throws synchronously still emits"request"("left" means handed tofetch). A deferred/streaming result emits"request"at the send and"call"at handoff, as today. A throwing"request"listener is reported and cannot break the call; listeners run synchronously before the send, so a slow one delays the fetch — hand work off to a microtask.IS_OBSERVE. Not in the diagnostics capture'sRECORD_TYPES(capture artifacts keep settled summaries only); no performance-tracks change.ad1ecc5: SSR:
ssrElement's spread walk keeps its pre-slot shape on the hot path. Attribute-slot handling — aclass/styleobject, a stand-in at an attribute or behavior key, a slot's return among the sources — moves into helpers reached only for object values and non-literal sources; a stringclass/style, a plain attribute and a plain source cost what they did before slots (spread-static-tailbench: the two-source forms had regressed 7–28%).Updated dependencies [5bef430]
Updated dependencies [d0e487b]
Updated dependencies [fe1eb68]
Updated dependencies [0bf5758]
[email protected]
Patch Changes
[email protected]
Patch Changes