Skip to content

@solidjs/web@2.0.0-rc.15

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Oct 19:07
· 39 commits to main since this release
4937f0a

Patch Changes

  • 617d195: Element claims fire after an element's initial attributes are applied, and the attributes whose writes re-claim are consumer-declared (#3923).

    A claim consumer (a router's link-state layer) is told about each a[href] / form[action] once per mount — with its dynamic href, target, rel, … already applied — instead of at creation before the binding effect's first run. Compiled output claims a static element at creation, an element with bindings at the tail of its binding effect's first run (under the owner captured at creation, since a held mount lands ownerless), and spread claims an a/form it applies after the first application (so dynamic("a") anchors are covered too). Writes before the mount claim never re-claim.

    Public API:

    • registerElementClaim(handler, options?) takes an options bag: { attributes?: readonly string[] } names the attributes whose compiler-owned writes (setAttribute, setAttributeNS, setProperty, the spread's property path) re-claim an already claimed element — plain names; prop:href and xlink:href map to href. The write sites consult the union of every registered consumer's set. Default stays ["href", "action"], so existing consumers keep today's behaviour. New exported type ElementClaimOptions.
    • claimElement(node, owner?) (compiler-emitted primitive, @internal) takes the optional reactive owner the handlers run under.

    Without a registered consumer every hook folds away: the three every-page size scenarios (app: CSR, app: compiled CSR, app: compiled hydrating) are byte-identical.

  • 56c309c: Direct server-function calls no longer report thrown Response or ResponseEnvelope values as server-function/thrown failures, matching HTTP dispatch. Callers still receive the response or the envelope's value. A response that reaches a boundary or the render root is still reported by the renderer.

  • a497d4b: Remove state, noscroll, replace, and preload from core anchor JSX types. They are not standard <a> attributes; a routing integration declares them by augmenting AnchorHTMLAttributes. link stays — it is the shared explicit-links opt-in, and the SSR claim bag already copies that name. Media and webview preload attributes are unchanged. Types only.

  • 4afc662: Move dynamicComponent into solid-js. @solidjs/web keeps dynamic and re-exports dynamicComponent.

  • a14c18b: A 304 from a server function no longer stamps a body-format header onto the cache update, so a browser replaying a cached GET still decodes the stored value.

  • 372005f: The client runtime restores the native submission value for checkbox and radio inputs by removing the value attribute when value or defaultValue is nullish. Checked inputs then submit "on". Explicit empty values are preserved, and text inputs and textareas still clear to "".

  • 469733e: Fix aborting a document that renders a live server component crashing Node: the document's sc:live channel now marks itself closed when the abort cancels it, so the render's own end (and any late hole op) no longer closes or writes the cancelled stream, which escaped as an unhandled ERR_INVALID_STATE rejection.

  • 43570f8: Reapply a select's bound value when its options arrive after the value was written.

  • 1365d31: Render input defaultValue/defaultChecked and option defaultSelected supplied through SSR spreads as value, checked and selected. Within prop sources, a default takes precedence over the bound value in the initial HTML. Named attributes compiled after the spread retain precedence.

  • 06c7717: A server component that throws during its synchronous render is no longer cacheable. respond(View, { headers }) puts those headers on the frame response, and the response is a 200 whose body carries the error, so an author's cache-control: public, max-age stored the failure. When the render fails before the head is built, the frame response sends Cache-Control: no-store and drops the CDN-targeted fields that would override it at a CDN (CDN-Cache-Control, <CDN>-CDN-Cache-Control, Surrogate-Control). The status stays 200 and the body still carries the error record. A render that succeeds keeps the author's headers, and so does a returned { error } value: the author chose that value and the headers that travel with it. A failure that lands after a suspended shell has already left can no longer change the head.

    @solidjs/web/frames/server no longer exports renderServerComponent, renderToFrameStream, serverComponentResponse or createFrameSink. They were internal building blocks: a server component reaches the client by being returned from a server function (directly or wrapped in respond()), and the transform hooks (frameTransformResult, frameTransformFlightResult, frameTransformDirectResult) render and frame it. The entry is experimental, and the hooks and document-shell exports are unchanged.

  • abe12fc: Frames client: the adopt-time slot walk reads the server's tier announcement for the scan, not only the load — a page whose sc:tiers record names other tiers and not bind is never scanned for binding-slot markers (_s:*), and the bind tier is never loaded for it; an un-announced page keeps detection. collectSlots is one TreeWalker pass over comments (and elements only when markers may exist), testing a comment's data by prefix before any regex. Same slots found, in the same order; no public API change. Measured on the HN story page (652 occurrences, 17k comments): collectSlots 14.2 → 5.4 ms.

  • f64fcfe: Fix an unbounded request loop when a server component's re-asked flight fails the same way as the error it replaces. The server writes an escaped render failure as its message string, so two flights that fail alike carry equal payloads; a mount's content node told "already surfaced" from "the next flight's error" by the payload and re-asked on every flight instead of surfacing the error. It now keys a surfaced error by the response version that carried it: reset(), and a fresh mount over an errored address (e.g. after a hover preload that failed), make one request and the error reaches the nearest <Errored> again.

  • e5a4c8e: fix(web): hydrate() started mid-parse waits for the end of the shell (#3971)

    An async client entry can run while the browser is still parsing the server shell. hydrate() then gathered only the _hk nodes parsed so far, created fresh client nodes for the rest, and left the server copies parsed afterwards in the document, dead (no listeners, no bindings).

    A render that emits <HydrationScript /> now ends its shell with a small marker script, after its records, and its bootstrap declares the ledger the marker writes to. When hydrate() starts before that marker has run and the parser is still working, it parks until the marker runs (or DOMContentLoaded, if none comes), then hydrates against the complete shell. Streamed fragments arrive after the shell and are claimed as before, so streaming still hydrates before the stream ends. No API change. A host-placed bootstrap (generateHydrationScript(): hand-built documents, islands hosts such as Astro that await each island's children themselves) declares no ledger and never parks; renders without <HydrationScript /> and noScripts renders are byte-identical.

  • 6232d18: A server component's markup hole that contains an element with a dynamic attribute is no longer sent twice. The first render writes a data-lha address on such an element and the sweep's re-evaluation does not, so the unchanged hole compared as changed and its markup shipped again as a hole record on the document's live channel. The baseline now ignores those addresses, as it already ignored the lh comment markers. The markup that ships is unchanged.

  • 3173a14: A server component factory mounted more than once keeps an independent hydrated client slot at each site. The first site still uses the function id; each later site gets its own frame scope so hydration keys, slot records, and adoption do not collide.

  • 5d75ed8: A nested server region keeps its own stream version, so a same-argument refetch no longer makes the next argument's region look stale.

  • 86ac6da: Frames (document adoption): gather an adopted boundary's hydration keys once per adoption, not once per occurrence. Each adopted occurrence's claim window used to select [_hk^="<prefix>"] over the whole hydration root — 37 ms on a 652-occurrence comment thread, more than the rest of its hydration. The boundary now indexes the _hk nodes under its element once (bucketed by occurrence prefix) and serves every window — the occurrence's and a streamed <Loading>'s resume inside it — from that index; a fragment revealed into the element later extends the index for the revealed parent, so late (post-done) claims still find their nodes. The page-level gather and the streamed boundary's resume path are unchanged.

  • c2d4571: A synchronous prepareRequest hook no longer defers the server-function request by a microtask, so reads adopted during hydration stay inside the hydration trace instead of refetching from the browser (#3959). An async hook is still awaited.

  • 49bed5c: respond() is typed as its value and its envelope is a real Response; direct calls unwrap it.

    • Direct calls unwrap envelopes. A server function called in-process during SSR that returned respond(value) handed its caller the ResponseEnvelope itself, while an HTTP caller decoded value — and a server component wrapped for its headers (respond(View, { headers })) reached the render as an object, so the frames policy never branded it and the hydration serializer failed on the envelope. The direct leg now resolves with the value (a thrown envelope rejects with it) before transformDirectResult runs. Of the metadata, Set-Cookie is appended to the render's response head; other headers describe the function's own address and the status is the page's, so neither is applied to the document.
    • The envelope is a Response. ResponseEnvelope now extends Response, carrying the given response's body, status and headers, so a consumer with no Solid knowledge (a fetch-style API dispatch) answers with it as-is instead of JSON-encoding the wrapper. new ResponseEnvelope(response, value) keeps its signature; response is the envelope itself, or undefined when constructed without one. Solid's own consumers still recognize it with isResponseEnvelope().
    • The helpers are typed by what they mean to the caller. respond<T>() returns T, and redirect()/reload() are <T = never>(…): T — control flow, not values, so they vanish from the returning function's inferred type, while an annotated return or a Response-typed request handler supplies T (a literal never would also mark code after a bare call unreachable). A bare "use server" function's own signature is therefore its callers' type, with no wrapper narrowing (NarrowResponse) needed. At runtime the helpers are unchanged.
  • 43570f8: Keep every bound value of a multi-select selected when its options arrive later.

  • b749474: Pin seroval and seroval-plugins to exactly 1.6.7: 1.6.8 changed the stream representation in a patch release and grew bundles. Stream detection now requires a real stream — the JSON decoder (server functions, frames) treats a value as a stream only when seroval's isStream holds and it has a callable .on, so decoded plain data that happens to carry a __SEROVAL_STREAM__ key stays data. The frames container-trace check, the client container-trace materializer and the render stream's channel guard also recognize seroval's untagged stream class, and the frames border walk tests for an async iterator first.

  • 234cfc9: The server-component half of hydration installs from @solidjs/web/frames' installServerComponents(), not from hydrate(): sharedConfig.holdBoundary (the client hold on adopted markup counted as a pending boundary), sharedConfig.hydrateWindow (the claim window an adopted occurrence re-enters hydration through), fragment ownership by rendering (the _$HY.fa term of the reveal policy) and the ledger's published answer _$HY.fr on the solid side; the declared claim roots (sharedConfig.claimRoots) and the frame-region exclusion of the root sweep on the DOM runtime's side. A hydrating page without server components carries none of it — −686 B minified / ≈ −205…−243 B brotli on the hydrating apps (this returns the bytes the frames A0 correctness pass added to every hydrating page); a server-component page pays the two installers' glue (≈ +220 B minified).

    Public surface (@internal): enableServerComponentHydration() on solid-js (runtime) and solid-js/internal (typed), a no-op on the server entry; installServerComponentHydration() on @solidjs/web, which installs both halves — the frames client calls it where it installs its reveal hook.

  • be845cb: A GET (or HEAD) server-function response whose Cache-Control lets a shared cache store it (public or s-maxage, unless no-store or an unqualified private is also set), or whose CDN-Cache-Control a CDN may store from, no longer carries the request's trace in Server-Timing (traceparent and any provider entries), so one request's trace is not replayed to every viewer from the cache (#3985). POST responses and documents are unchanged.

  • a26ee62: HydrationScript, generateHydrationScript and createSSRResponse now take the same CSPNonce as renderToString/renderToStream (a string or a { script, style } pair) and use its script half, so event.nonce can be passed to all of them as is. scriptNonce and styleNonce, which only existed to do that projection, are now internal: they are no longer documented, and stay exported only for @solidjs/vite-plugin's generated handler until it stops importing them.

  • 22f942d: SSR: a router can mark links in server HTML (#3878). Element claims never fire during SSR, so plain anchors arrived without aria-current="page" / data-active — late on hydrated pages, never on pages that do not hydrate. Both compilers now give every candidate <a href> in SSR output one hole after its attributes, ssrLinkClaim(attrs), where a render's link handler writes the anchor's link state and "" otherwise. A static anchor's hole is one eager call over a hoisted attributes object (no allocation per render); a dynamic anchor's joins the element's attribute group and reuses the raw value its href hole evaluated. Anchors the compiler can rule out — a static non-empty target, a download, a rel naming external, an author-written aria-current, an empty href or a non-HTTP scheme — get no hole at all. The hole creates no owner and takes no hydration id. A spread anchor's link attributes are collected by ssrElement from its winning sources (an anchor's trailing link attributes stay a source instead of baked tail markup).

    New server API (@experimental): setLinkClaim(handler) sets the handler of the render the caller belongs to — found through the owner chain like getHydrationWriter(), so concurrent renders never share one — and returns false outside a render; undefined clears it. The handler, (attrs) => string, receives the anchor's link-relevant attributes (href, target, rel, download, link, aria-current, raw values as written) and returns its attribute markup. An author-written aria-current always wins. Server components inline in the document render under the page's handler; a server component's own frame stream has none unless its render sets one.

    Trust marker: the first setLinkClaim of a render writes the links hydration record once (_$HY.r.links = 1, with the shell's hydration script on the document face, a data record on a frame stream). hasServerLinkState() (client; false on the server) reads it non-destructively, so a link consumer can tell "this page's anchors carry the server's link state" from "no handler ran" and trust the HTML at hydration instead of resolving every anchor's URL at claim. Without a handler the HTML and the hydration ids are byte-for-byte what they were.

  • 39411bd: Escape every < in useHead JSON script bodies as \u003C. JSON here means importmap, speculationrules and the JSON MIME types (application/json, text/json, any +json subtype such as application/ld+json). Only </script was rewritten before, so a JSON-LD value like <!--<script> put the HTML tokenizer in the double-escaped state, and the markup after the script became script text up to a later </script> or the end of the document. The escaped form parses back to the same JSON. The client writes and compares the same text, so hydration adopts the server's element instead of replacing it; a </script> inside JSON no longer causes that replacement either. Other scripts and data blocks keep their current body.

  • 234cfc9: readShallow (the compiler's one-layer tracked read of an object-valued class/style) reads a proxy's keys with Reflect.ownKeys directly — what sourceKeys(value, SOURCE_PROXY) resolves to by definition — instead of through sourceKeys, whose runtime kind argument kept the merge/omit view walkers of @solidjs/signals in every compiled page with a dynamic class or style, reached by nothing. Identical behaviour; −1,165 B minified / ≈ −350 B brotli on the compiled server-component page (a page with an element spread keeps the walkers through spread()).

  • Updated dependencies [4afc662]

  • Updated dependencies [b460640]

  • Updated dependencies [39450e5]

  • Updated dependencies [cbd66e5]

  • Updated dependencies [b083833]

  • Updated dependencies [5413c06]

  • Updated dependencies [0c4bff7]

  • Updated dependencies [25935fe]

  • Updated dependencies [b749474]

  • Updated dependencies [234cfc9]

  • Updated dependencies [d966729]

  • Updated dependencies [a405039]

  • Updated dependencies [234cfc9]

    • solid-js@2.0.0-rc.15