Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
19bed41
docs(stage7): retire `predict` — Stage 7 is attribute slots (§9.2.2)
ryansolid Sep 27, 2026
fc45c90
docs(stage7): §9.2.2 — both mutation shapes are required
ryansolid Sep 27, 2026
b94cd53
docs(stage7): §9.2.2 — spread only, no compiler change
ryansolid Sep 28, 2026
6df3dd2
docs(stage7): §9.2.2 — flicker checks run; action syntax corrected
ryansolid Sep 28, 2026
1ced180
frames: a showing call's refetch settles when its response has applied
ryansolid Sep 28, 2026
d93e71e
compiler: `$key` on a spread intrinsic compiles like a template element
ryansolid Sep 28, 2026
574c9aa
web: dynamic's address delivery is an owned write
ryansolid Sep 28, 2026
8dc1b29
compilers: `serverComponents` keeps attribute-slot positions bindable
ryansolid Sep 28, 2026
f1b9553
jsx: `$key` is typed on intrinsic elements
ryansolid Sep 28, 2026
9936a69
frames: attribute slots — one per data context, bound per position
ryansolid Sep 28, 2026
70d9615
examples: todos-server, notes and chat on attribute slots
ryansolid Sep 28, 2026
206fb7b
docs(stage7): §9.2.3 — attribute slots, second form; build record
ryansolid Sep 28, 2026
e469907
frames: the orphan reporter is a module function; size ledger for att…
ryansolid Sep 28, 2026
b0388fb
test(web): the attribute-slot specs pass `test-types`
ryansolid Sep 28, 2026
921f211
web(ssr): the spread walk's hot path keeps its pre-slot shape
ryansolid Sep 28, 2026
04a12a0
frames: attribute slots — review fixes; the walk's gate is a module a…
ryansolid Sep 28, 2026
8e4300c
frames: an occurrence detaches its listeners; stand-ins scrubbed from…
ryansolid Sep 29, 2026
9f8285f
frames: bind ref/on* on spread elements; drop on:/oncapture: handling
ryansolid Sep 29, 2026
160c746
frames: spread-element claims via ssrElement; cycle-safe border walk …
ryansolid Sep 29, 2026
ed3ef72
frames: spread-element handler positions settle in source order
ryansolid Sep 29, 2026
029179c
frames: a nullish handler owns its position on a spread element, as t…
ryansolid Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/babel-plugin": patch
"@solidjs/compiler": patch
---

`serverComponents` (SSR): keep attribute-slot positions bindable on intrinsic elements. A dynamic `class` or `style` now compiles to a whole-attribute `_$ssrElementAttribute` hole instead of a value inside the template's quotes, so a server component's `class={{ selected: filters.all }}` can mark the class name a client attribute slot owns; `ref`/`on*` positions compile, as before, to one guarded `_$ssrClaim` hole per element, which now emits `_s:on:*`/`_s:ref` markers for slot reads (the `_bnd` behavior-claim marker is gone). A spread element compiles its named `ref`/`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 `_$ssrElement` as 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's `spread`/`merge()` do (the last source that has the key owns it, `undefined` included). A `ref` beside 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 lowercased `on*` remainder (`onClick` → `click`); `on:`/`oncapture:` are not 2.0 syntaxes and are no longer special-cased. Both compilers share the `attributeSlots` server-components fixture; plain SSR output is unchanged.
5 changes: 5 additions & 0 deletions .changeset/fix-compiler-spread-element-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/compiler": patch
---

Native compiler: `$key` on 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 `_key` attribute 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 `$key` attribute and keyed morphs lost identity. Babel already behaved this way; the shared `keyedElements` fixtures pin the spread case for both.
5 changes: 5 additions & 0 deletions .changeset/fix-dynamic-memo-source-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

`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 inside `dynamic`'s own compute rather than a promise microtask; the per-site address signal is now created with `ownedWrite`, since nothing in that compute reads it back. Before, dev builds threw `REACTIVE_WRITE_IN_OWNED_SCOPE` into the nearest error boundary on the first refetch.
10 changes: 10 additions & 0 deletions .changeset/frames-attribute-slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@solidjs/web": patch
"@solidjs/signals": patch
---

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, fires `ref` once, 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 `$key` when given, by structural args otherwise, so `$key` is 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`; reason `coerced`), placed as text, or reached inside template quotes renders **nothing on either face** and reports `ATTRIBUTE_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]`; a `Map`, `Set` or class instance is not walked) — is not data the server has: the arg carries `undefined` at that path on both faces (the record, and the document face's t=0 fill reads the same), with a finding (reason `arg`, `data.path` when nested). A cyclic arg crosses the border as a cycle (three walks used to overflow the stack on it; the border walk `toBorderForm` — 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 a `prop:*` key is a finding (reason `prop`; the compiled form drops `prop:*` 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 an `Array.prototype` method (`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 (reason `orphan`, `data.why` `fill`/`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 (reason `fill-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 face `claims` is 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. Named `ref`/`on*` on a spread element (`<button {...rest} onClick={row.go} />`) bind like every other handler position: `ssrElement` takes 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 — through `ssrClaim`'s logic (arrays of refs, merged duplicates, handler tuples, the `server-local` finding). A handler position settles in source order, as the client's `spread`/`merge()` do: the last source that _has_ the key — a spread or a named attribute at its position, whatever the value, `undefined` included — owns it, and a nullish owner binds nothing (`<b {...rest} onClick={cond ? row.go : undefined}>` binds nothing on both sides when `cond` is 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 a `lazy()` 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 the `serverComponents` compiler option a dynamic `class`/`style` on an intrinsic element compiles to `ssrElementAttribute`, so `class={undefined}` writes nothing where template quotes wrote `class=""` (nullish is "not set", as for every other attribute). `@solidjs/web` now ships `skills/server-components/SKILL.md` (the package's `files`) with the rule, the fill idiom and the diagnostic's reasons.

Surface added: `AttributeSlot<P, J>` from `@solidjs/web/frames` — `(props: P & { $key? }) => J`, `props` optional when `P` is empty; the `_s:*` markers, `SLOT_VALUE`/`slotValue`/`isSlotValue`, `SLOT_MARKER`, `SLOT_FACE_*` from `@solidjs/web`'s server entry as the runtime surface; the `ATTRIBUTE_SLOT_POSITION` diagnostic code (reasons `spread`, `stringified`, `coerced`, `inline`, `text`, `markup`, `server-local`, `reserved-key`, `arg`, `prop`; client: `orphan`, `fill-shape`). Removed: the `_bnd` behavior-claim marker, `CLAIM_PROP`, `BEHAVIOR_CLAIM_DROPPED` (a server-local function at a handler position is now `ATTRIBUTE_SLOT_POSITION`), the frame `props` option and `FrameHostOptions.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.
5 changes: 5 additions & 0 deletions .changeset/frames-showing-refetch-settles-on-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

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 a `yield 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.
6 changes: 6 additions & 0 deletions .changeset/jsx-key-intrinsic-typing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@solidjs/web": patch
"@solidjs/h": patch
---

JSX typings: `$key?: string | number` is 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.
5 changes: 5 additions & 0 deletions .changeset/ssr-spread-walk-hot-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/web": patch
---

SSR: `ssrElement`'s spread walk keeps its pre-slot shape on the hot path. Attribute-slot handling — a `class`/`style` object, 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 string `class`/`style`, a plain attribute and a plain source cost what they did before slots (`spread-static-tail` bench: the two-source forms had regressed 7–28%).
2 changes: 1 addition & 1 deletion documentation/plans/stage8-connection-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,4 +810,4 @@ prev)`; corrected to `(prev, value)`. Pinned:
Cursors as a protocol (only the `Last-Event-ID` seam), WebSocket, any
subscription registry or connection-local subscription state, any new
authoring API for liveness, any server configuration for liveness, Stage 7's
predictions (independent; §9.2.1).
optimism (independent; attribute slots, §9.2.3 — `predict` retired).
Loading
Loading