@solidjs/web@2.0.0-rc.7
Pre-releasePatch Changes
-
7c14e23: Attribution: write provenance — who performed a change.
Every root
ChangeRecordnow carriesorigin: the imperative frame that made the write.interaction(a user event — type, described target such asbutton#next "Next →", and dispatch time),effect(the callback's name),action(the generator's name),async(the landing's node), orexternal(timers, sockets, promise callbacks — including writes after anawaitrather than ayieldinside an action, the documented transaction escape). Frames nested under an interaction carry it: an action a click started (every step, including post-yieldresumptions), an effect whose run a click's write caused, an async flight a click's write launched. Why-chains print the origin after the write;RerunEvent.interactionandHoldEvent.interactionexpose the interaction a run or hold traces back to, andSILENT_HOLDnow opens with what the user did and measures the wait from the event, not from the first parked flush.@solidjs/webdeclares the interaction around its two dispatch sites — delegated events (onClick,onInput,onKeyDown, pointer events: every INP-relevant type) and runtime-attached direct handlers (spreads, non-literal handler expressions) — via the newDEV.attribution.withInteraction(ref, fn), which custom renderers and test harnesses can call themselves. New core dev hookseffectRunStart/effectRunEnd(replacingeffectRun) andactionStepStart/actionStepEnd; all sites fold out of prod, verified byte-identical against the size scenarios. -
1226381: Bind
GET()grants to the function identity they were declared about (#3237). The grant — GET/HEAD dispatch plus the CSRF origin-gate exemption — was keyed by id alone, soregister -> register -> GET(oldReference)handed the NEW function cross-site GET execution on the strength of a declaration the old one signed. The grant now records the declared function, and a singledeclaresRead(id)check governs both dispatch and the 405Allowadvertisement; a stale or unverifiable declaration fails closed (GET refused, POST + origin gate required). A declaration orwithMeta({ method })write that would change an existing grant's binding throws in dev and fails closed in prod, never silently rebinds. -
7009adf: Bound the request body cap by the bytes actually received: a conforming Content-Length under the limit no longer skips the counting read, so an under-declared body cannot stream past
bodySizeLimitinto the decoder, and the abort/teardown coupling for abandoned uploads now covers declared-length POSTs too (#3236). -
ef2b02c: Internal cleanup with no behavior change: inline four single-use helpers (
hasContext/isUndefined,markCovered,shallowWithSymbols), delete two dead ones (isNextProxy,ownEnumerableKeysPlain), and collapsespread()'s nullish-source handling into one accessor closure. A few dozen bytes off the app scenarios. -
a14c138: Dedupe the requested single-flight source ids to a first-seen-order set at entry, so a repeated id runs its collector once and echoes once in the response header instead of multiplying work by the caller-controlled list length (#3251)
-
bd22ac8: Rename the three legacy client dev artifacts to the
<entry>.dev.{js,cjs}convention every other dev build already uses:solid-js/dist/dev.*→dist/solid.dev.*,@solidjs/web/dist/dev.*→dist/web.dev.*,@solidjs/universal/dist/dev.*→dist/universal.dev.*. With server dev builds now shipping asdist/server.dev.*, a baredev.jsno longer says which entry it is the dev build of. Theexportsmaps are updated; only code deep-importingdist/dev.jsdirectly (bypassingexports) is affected. -
6c8c956: Diagnostics console addressability: compiled JSX binding effects (attribute, class, style, property, spread, insert) are tagged in dev with the element they write, and a console diagnostic about such an effect prints that element as a second argument — hover highlights it on the page, click jumps to it in the Elements panel. Why-chains (
DEV.attribution.enable()logging) print as collapsed console groups, one headline per run with the causes inside. The once-per-code footer now pairs the installed skill path with the file's stable GitHub URL, anchored to the code's section. -
c08e974: Document the per-handler
wrapInvocationoption as entry-only (#3240). Ruled: entry-only semantics are kept — the option wraps exactly the invocation the request addressed, and nested direct server-function calls made by the dispatched body are not re-wrapped by it; hop-by-hop policy belongs to the configured (ambient) hook, which wraps every direct call. TSDoc only, no runtime behavior change; the boundary is now pinned by a regression test. -
fbe5bef: Encrypt the no-JS flash cookie (#3239). The flash carries the submitted form input — whatever the user typed — so its payload is now AES-GCM encrypted under a key derived (domain-separated) from the deployment secret:
configureServerFunctionsServer({ secret }), falling back to theglobalThis.__SOLID_SECRET__value the Solid bundler plugin injects into server builds. With no secret configured the outcome is withheld rather than sent in the clear (the post still redirects; dev builds warn once). Decryption failure — a tampered cookie, a rotated secret — reads as "no flash". The cookie now also carriesSameSite=LaxandMax-Age=60, andencodeFlashCookie/decodeFlashCookieare async. -
292bdc5: Enforce provideEvent's exactly-once contract on direct SSR calls too: the invocation count #3172 added to HTTP dispatch now guards both legs through a shared
provideEventOnceseam, so a hook that double-invokes or skips the callback fails loudly during a render instead of silently double-committing a mutation or answeringundefined, while synchronous direct calls keep returning their value synchronously (#3246). -
285a717: Defuse the promise the decoder's abort sweep is about to reject (#3267). A
PromiseConstructornode whose ref id collides with an already-assigned id (or is malformed) throws mid-registration, leaving a{p, s, f}deferred in the decoder's refs whose promiseownDecodedPromisesnever claimed — the deferred is not itself a Promise. The end-of-stream sweep then rejected that promise with no owner, and under Node's default policy one unauthenticated POST with a crafted argument body ended the process after the request was already refused 400. The sweep now takes ownership of.pbefore rejecting it, covering every promise it touches regardless of how the entry reached refs. -
8f11ea7: Guard failure channels under an Error carrier's non-enumerable own data slots (#3268). The #3235 guard walked Error carriers with
Object.keys, but seroval encodes an Error's own properties throughgetOwnPropertyNames— so a rejected promise or erroring stream parked on a non-enumerable slot (causeis non-enumerable by spec since ES2022, and the ordinary place a wrapped driver error carries its context) was encoded without ever being walked: its failure reason rode the wire verbatim on a committed 200, and the rejection had no owner. The guard now descends an Error's own string-keyed data slots, enumerable or not. Hidden accessors remain the codec's read (4799541's pinned ruling): the walk still does not invoke what the author hid. -
d0ca3a4: The no-JS flash cookie now records the UNBOUND function base as the submission's
url— the request's pathname (<endpoint>/<id>), never the query. A.with()-bound form's action url carries its bound arguments in?args=…, and integrations match submissions against the action's unbound base (the router'ss.url === fn.base): a flash url wearing the binding stored, decoded, and then matched nothing on the post-redirect render. The seed now matches the scripted submission shape exactly — the base asurl, bound arguments prepended toinput(which the argument parser's?argsprepend already provided). -
b6a90f9: Fix
deferStreambeing a silent no-op inside a code-splitlazy()component (#3299). A module load is code, not data: the shell's "no new async discovered during the sync render" rule cannot be evaluated for a segment whose code has not run, so the shell now waits for the chunk even under a<Loading>(the boundary still owns the data the loaded code discovers — plain async streams behind the fallback as before, and adeferStreamread inside the chunk holds the shell exactly like one in an eagerly imported component). Only the first render that reaches an un-preloaded chunk pays; a lazy mounted by a post-shell fragment streams as before.Also closes a gap in the flush loop where a shell blocker registered while a boundary resumed during the drain — after the awaited set had settled but before the flush attempt snapshotted it — was never re-awaited.
dynamic()keeps streaming its source by default (a source is data of unknown cost) and gains adeferStreamoption to opt into holding the shell on it, with the same meaning ascreateMemo's. -
6164667: Fix a lone reactive spread crashing when its source is
nullorundefined(#3297).<input {...props()} />compiles tospread(el, props)with the accessor passed through, so an absent optional props object threw insidespread()(andssrElementon the server) and halted the app's updates. A nullish source is now an empty spread: attributes applied by the previous value are removed and reactivity continues. -
b64071c: Render textarea values supplied through SSR spreads as text content rather than invalid HTML attributes.
-
b3586e8: Validate document-shell templates in the document context (#3259). The
validatepass round-trips templates through a body-context fragment parse, which strips<html>/<head>/<body>wrappers no matter how well-formed the markup — so once #3099 made validate failures compile errors, a root component owning the document shell failed to compile in plain client mode, and merely importing it (the jsdom component-test configuration) was fatal. Shell-rooted templates now parse as a document and the shell element is compared back — the analogue of the synthetic<table>wrap for table partials, in both the Babel plugin and the native compiler. Genuine restructuring (an implied<head>, flow content in<head>, a<p>split in<body>) still errors. Since<template>parsing flattens shells, actually client-creating one now throws a descriptive dev-mode error fromtemplate()pointing athydrate()— the failure moved from every import to the one broken act. -
f21e060: Flash falsy no-JS outcomes (
0,false,"",null) instead of silently dropping them: the flash decode and the no-JS handler now decide structurally (result presence,Responseshape) rather than by truthiness, and dispatch no longer erodes a returnednulltoundefinedon its way to the handler; anundefinedoutcome keeps its current no-cookie behavior pending ruling (#3248) -
ecdfc3a: An awaited
renderToStream(...)result now freezes the request's response head at completion — the render commitsevent.responseright before its final dispose — sohttpStatus/httpHeaderdeclarations survive intocreateSSRResponse(html, event), which sees the already-committed stub and passes it through. Previously the thenable disposed the render owner before resolving, while the head was still open, so every scope-tied declaration's cleanup retracted it: a page callinghttpStatus(404)rendered throughawait renderToStream(...)came back as a 200 and itshttpHeaderwrites vanished. The piped forms are unchanged (they already froze at shell flush), and so are the retraction semantics themselves — a scope disposed mid-render, such as an errored boundary that recovered, still retracts its declarations. Integrations no longer need to commit the stub fromonCompleteAllto work around this. -
5cee0f7: Guard enumerable failure channels carried on Error results: the result-encoding guard walk now descends Error-prototyped carriers (which seroval encodes with their own properties) so a rejecting promise, erroring stream, or throwing iterable assigned onto a returned Error is sanitized and torn down like any other channel, while the carrier keeps its prototype, message, and own data (#3235).
-
6bb51c9: Keep
-0off the server-function JSON fast path.JSON.stringify(-0)is"0", so a signed zero admitted byisJSONSaferode the fast path and arrived as+0— a silent sign flip on the exact guard that already refusesNaNand the infinities for the same reason.-0now answers "not JSON-safe" and rides the codec, which spells it exactly, on both legs (argument lists and results). -
ff2ecf1: Own every promise the server-function decoder mints. A rejected promise decoded out of a peer's payload — a rejection frame arriving mid-stream, or an atomic rejected-promise node settling synchronously during decode — had no owner when the consumer never read (or abandoned) the slot, and escaped as an unhandled rejection that ends a Node consumer under its default policy. The decoder now attaches a noop rejection handler at mint time, mirroring the encode side's
guardedPromiseownership (#3216); real consumers still observe the rejection unchanged. -
c0bc9ba: Adopt a
transformFlightResultResponse via the ownership seam (ownResponse) before stamping the mutation's cookies and accumulated headers onto it, so a Response the integration retains (a memoized shell) never accumulates one caller's session cookies and serves them to the next (#3234, completing #3155) -
ace227e: Canonicalize resource identity qualifiers instead of comparing raw prop values, so two declarations of one request dedupe to one
<link>on both sides of hydration.falsenow means absent, matching both attribute writers:crossorigin={cond && "anonymous"}no longer emits a second, byte-identical link when the condition is false.crossoriginis compared by its CORS state rather than its spelling. It is a CORS settings attribute with three states — absent is No CORS,use-credentials(ASCII case-insensitive) is Use Credentials, and every other present value including"", a bare attribute and an invalid one is Anonymous — so the same font is no longer preloaded once per spelling, and the client adopts the server's link instead of mounting a second one for a request the browser already has.Qualifier values are length-prefixed, so a value containing the identity delimiters can no longer collide with a different qualifier set and silently suppress another resource (
type: "a:media=b"andtype: "a", media: "b"were one identity).Client-side adoption of a mount-once head resource now matches a server-emitted element on the full request identity rather than the href alone: two preloads sharing an href still differ if their destination, CORS mode, type, media or source set differ. The document client, the standalone frame client and the server all apply the same rules.
-
b7b17ab: Make
extractBodyown the stream it decodes: the body is read where it lies, never from an internal clone. An unread tee branch queues the whole payload in memory for the life of the read and defeats backpressure and cancellation — the same ownership defect fixed for the upload leg inbufferBodyWithin(#3217–#3219).decodeResponsekeeps its documented contract (an integration's response stays readable) by cloning at its own entry — a branch that is then read in full; the client transport decodes the response it owns directly, and the server's argument road reuses its one deliberate clone (kept soevent.requeststays readable) for the empty-body inspection instead of teeing again. This is the clone half of #3244 only; connection teardown on completion is deliberately not included. -
ed6b605: Throw a clear configuration error for invalid
wrapInvocationvalues (#3238). A value other than a function orundefined—null,false, an options bag in the wrong slot — used to fail in the quietest available direction: falsy values silently took per-invocation policy (auth, logging) off the call, truthy non-functions threw a bare "not a function" mid-dispatch. The hook is now validated at the point it is resolved for an invocation, on both roads (HTTP dispatch and direct SSR calls), with an error namingwrapInvocationand the received type;undefinedstays the one spelling of absence. -
6c9f8f4: Refuse an unrecognized
X-Server-Function-Formattag before the decode switch runs. The content-type sniffing branches (there for untagged form posts) matched regardless of the tag, so a body tagged with a format this build has no case for — version skew from a newer peer, or a duplicated header thatHeaders.getjoins into one unknown value — was silently reinterpreted as a form and the function ran on an argument it was never sent. Such bodies now answer 400 before dispatch, with a development message naming version skew; untagged bodies keep the sniffing, and an untagged empty body stays a zero-argument call (#3214). -
3393fb6: Refuse to flash when no storable cookie exists for a no-JS outcome (#3249). The degrade ladder (#3137) bounds the input echo and the result but never looked at
url— pathname + search of a request the caller chose — so a long enough form action pushed the fully-degraded payload past the ~4 KB cookie ceiling and the encoder emitted a cookie the browser discards whole, withtruncated: trueinside asserting a degradation that never stored.encodeFlashCookienow returnsnullwhen even the degraded payload cannot fit, and the no-JS handler falls back to the plain redirect — never an oversized cookie, never a url truncated to a prefix that would attach the outcome to a submission it does not identify. Cookie naming, attributes, and refusal/redirect statuses are untouched (#3239, #3250 pending). -
d601119: Remove the experimental patch channel and patch-mode list driver (always opt-in, never default). Graph-native regions own value delivery and the unified-For design owns list structure, so the channel's parallel delivery machinery is retired:
patch.ts/patch-driver.tsdeleted, the compiler-contract exports (registerPatch/registerRowOps/registerSlotPatch/patchableRaw,patchDriver/rowProof/driveList) removed, thepatchDrivercompiler option dropped from both compilers, the insert$llseam stripped, and the write-side channel struct dieted to the single written-keys bound (t.wk) the core fold/notify paths actually use. Store-family app bundles reclaim up to ~900 B brotli; every measured tier shrinks. -
ace227e: Support
imagesrcsetandimagesizesin typed image preloads, including the standard form withouthref. Candidate URLs insideimagesrcsetmust already be resolved by the integration.The responsive pair is image-only. On any other destination the attribute is dropped and the link still ships — an integration that computes
imagesrcsetfor every asset keeps its script and style preloads. An empty or non-string value counts as absent for the same reason, so a source set is never emitted as garbage the browser cannot parse. A descriptor whose only source was such a filtered attribute is dropped entirely rather than emitted as a<link rel="preload">with nothing to fetch.mountHeadResourcecan adopt a source-set link: it has no href, so it matches a server-emitted link on a null href plus the identity qualifiers — the rule the frame client already applied.Development builds warn when
imagesrcsetuses a width descriptor withoutimagesizes(the source size falls back to100vw, so the preload can miss the image the<img>selects), and when a manifest source set carries a relative candidate — candidates are not joined with_base, so they resolve against the document URL whichever base the manifest declares. That check walks the source set the way the spec's parser does, so commas inside a candidate URL are not mistaken for candidate separators. -
d2b50e9: Revert #3187's deferred element creation in
Dynamic. String-componentDynamicis once again an element at component creation on the client —spreadandrefcallbacks run then, matching the hydration path — instead of a thunk materialized by the consuminginsert(). The deferral moved ref writes into the render phase of the flush for<Dynamic>inside<Portal>, wherecreateTrackedEffectreaders never observed them (#3291: every Kobalte popper rendered unpositioned). The namespace of ambiguous tags (a,script,style,title) rendered throughDynamicinside SVG content is an accepted limitation, as in 1.x; use a static element for those. -
84a94bc: Scope deferred work nested inside plain-object and array carriers to the producing call's request event (#3241, completing #3222). The HTTP road already applied the wrapping in the encoded representation (the guard walk's rebuilt shells); the direct SSR road only looked at the returned value itself, so
return { rows: cursor() }ran its generator under the render's ambient event — two concurrent direct calls read and wrote each other'slocals, and the render's own. The direct road now descends plain-object/array carriers and hands the caller a shallow-rebuilt carrier with the bound wrappers in the deferred slots; the user's returned containers are never written into, and results with nothing deferred keep their identity. Set/Map members, class instances, and frozen/non-writable slots are deliberately out of the carrier set (pinned by test): bodies reached through them stay bound to nothing, as before. -
80ff52e: Add development server builds —
dist/server.dev.*forsolid-jsand@solidjs/web, andframes/dist/server.dev.*— selected by thedevelopmentexport condition nested undernode/worker/deno(nesting is required: those conditions precede the top-leveldevelopmentkey, so a top-level entry never matched on a server). Until now SSR had no dev build: the only server artifact was built with_SOLID_DEV_stripped, so the server runtime's dev checks (head/preload descriptor validation,useHeadwarnings, the committed-response header guard) never ran outside the test suite.The server entries now gate their public dev flags on the same
_SOLID_DEV_replace as their internals instead of hard-coding them:solid-js's serverDEVis@solidjs/signals'DEVobject in the dev artifact (soDEV.diagnostics.subscribe/capturework server-side) andundefinedin prod;@solidjs/web's serverisDevistruein the dev artifact andfalsein prod.Behavior change for dev SSR hosts that pass the
developmentcondition (Vite dev does by default): a header write after the response has committed now throws with the offending header named, where the production artifact continues toconsole.errorand drop the write.Also runs
replaceDev(false)onsolid-js's production server build so a future_SOLID_DEV_gate insrc/server/cannot constant-fold into the dev branch in production. -
a1ff286: Run
transformResultfor plain thrown errors as its documentation already promises: the hook now runs once at the thrown path's entry (context.thrownset) for every thrown value — not only thrown Response/envelope shapes — and the response tail is selected from its output, while the wire stays sanitized and a hook that itself throws is contained as a sanitized 500 (#3247). -
Updated dependencies [215de3b]
-
Updated dependencies [1a1e2f2]
-
Updated dependencies [7c14e23]
-
Updated dependencies [c6c415b]
-
Updated dependencies [bd22ac8]
-
Updated dependencies [6c8c956]
-
Updated dependencies [d5aba4b]
-
Updated dependencies [3ae0ca0]
-
Updated dependencies [1a1e2f2]
-
Updated dependencies [6c8c956]
-
Updated dependencies [1a1e2f2]
-
Updated dependencies [b6a90f9]
-
Updated dependencies [3424f9a]
-
Updated dependencies [6c8c956]
-
Updated dependencies [f4d3c87]
-
Updated dependencies [d601119]
-
Updated dependencies [ac5159a]
-
Updated dependencies [de1c8b5]
-
Updated dependencies [80ff52e]
-
Updated dependencies [01e3a57]
-
Updated dependencies [6c8c956]
- solid-js@2.0.0-rc.7