Releases: solidjs/solid
Release list
solid-js@2.0.0-rc.8
Patch Changes
-
01ac18c: Compiler
componentNamesoption: component owner labels that survive minification. With the flag on, DOM output carries the tag as written in source as a thirdcreateComponentargument —<Home />compiles tocreateComponent(Home, props, "Home"),<Ui.Button />to"Ui.Button",<this.Row />to"this.Row"— and the dev and observe runtimes label the component's owner with it (<Home>in diagnosticownerPaths, attribution chains, and the devtools_component.name), falling back toComp.nameas before. Until now an observe-tier production bundle reported hot scopes and holds under whatever the minifier left of the function name (<Xt> › <Kn>), and alazy()or HMR wrapper hid the tag name even in dev. Off by default and byte-identical output when off; SSR (which inlines the call) and universal output never emit it; the productioncreateComponentignores the argument. Both compilers implement it in parity (shared fixtures, cross-mode ratchet).@solidjs/vite-pluginenables it for the dev andobservepostures. -
711b557: Move the #3338 diagnostics out of prod bytes. The
lazy()"not preloaded" explanation and the document-root preload-failure framing are dev-only; prod keeps terse messages and, at a document root, hands the preload failure itself toreportError(no wrapperError). ThehaltReactivityreportErrorhand-off is compacted. -
7d985b6: Fix SSR XSS: strings yielded by flow-control memos rendered unescaped
<Show when={s}>{s}</Show>,<For>{v => v}</For>,<Dynamic component={() => s} />,
<Switch>/<Match>, boundary fallbacks and any component that returns a string through a
memo rendered that string raw on the server. The server flow controls return memos for
hydration-id alignment;escape()passed functions through by identity, and the resolver
appended whatever they later produced without escaping.One rule now:
escape(x)at a hole covers everything reachable fromx— strings, array
items, and what a function yields when the resolver calls it (a deferred-escape wrapper).
Finished{ t }nodes pass through.Loadingescapes its content the way it already
escaped its fallback. The compilers stop wrapping fragment / mixed component children in
_$escape(they are values; escaping them too double-escaped through
<Comp>{props.children}</Comp>), and a single-expression fragment at a hole keeps the
hole's wrap. Live-hole tags ride the wrapper and$slotsurvives the array copy, so
frames behave as before. -
fe3ab92: Make a failed lazy() hydration observable instead of a silently dead page (#3338):
- The client's "was not preloaded before hydration" error no longer says to add a Loading boundary — none is required for root-level
lazy(). It now names the actual cause: the server serialized no client entry for the module (check the server log for "Asset manifest returned no client assets for module"), or the hydration id namespaces are misaligned. - An uncaught error that halts the reactive system is handed to
reportErrorwhere the platform provides it, so it reacheswindow.onerror/ error monitoring. Creation-time throws (a lazy miss during the hydration render) are converted to status by ancestor recomputes and never reached the top; console.error was their only trace. hydrate()'s "module preload failed → fall back to client render" path no longer runs for a document root, where a client render is impossible (the shell cannot be created) and died deep in the walk with an unrelated "Hydration Mismatch" as an unhandled rejection. It now reports an explicit error carrying the preload failure as its cause.
- The client's "was not preloaded before hydration" error no longer says to add a Loading boundary — none is required for root-level
-
0961d97: Observe tier: first-class interaction records and a typed record channel.
attribution.interactions()andInteractionEvent: one record perwithInteractiondispatch withat,handlerMs,writes,runs,created(computations built in its runs),runMs, theholdsandnavigationsattached, andsettledMs/outcome(idle|committed|held) once everything it caused is through.attribution.subscribe(type, listener)for"rerun" | "interaction" | "hold" | "navigation", delivered synchronously as each record completes; the baresubscribe(listener)form is unchanged.RerunEvent.atandHoldEvent.at— absolute times on theperformance.now()clock beside the existing durations.HoldEvent.acknowledgementsreplacesacknowledgedBy: one{ kind, source, reader? }per affordance,readerthe owner path of the effect that painted it.feedback().sources[].acknowledgedBystill ranks bykind:source.@solidjs/diagnosticsartifact format version 4 (holds carryacknowledgements; assertion evidence likewise).NavigationRef.paramsvalues may beundefined(an optional segment left unbound).OBSERVE.exclude(owner)/OBSERVE.isExcluded(subject)— an observer rendering inside the app it watches marks its own subtree; diagnostics about it are suppressed and the engine records none of its runs.solid-jsre-exports the tier types from its root:InteractionRef,NavigationRef,OriginRef,DiagnosticEventand friends, and the engine's record types (ChangeOrigin,RerunEvent,HoldEvent,NavigationEvent,InteractionEvent, …).
-
1807f7f: Observe tier: split dev-only checks from production-legal observability wiring.
Breaking (pre-release):
DEV.diagnosticsmoved to a newOBSERVEexport
—OBSERVE.diagnostics.{subscribe,capture,emit},OBSERVE.subjectOf(event).
DEVkeeps the devtools surface (hooks,getChildren/getSignals/
getParent/getSources/getObservers) and gains the console face
(DEV.report,DEV.setConsoleFooter— formerly
DEV.diagnostics.setConsoleFooter). Both are exported from@solidjs/signals
andsolid-js(client and server).Breaking (pre-release): the attribution engine is its own entry.
DEV.attribution.enable()and friends are now
import { attribution } from "solid-js/attribution"(or
@solidjs/signals/attribution) —enable/disable/subscribe/history/why/ subscriptions/costs/waterfalls/holds/feedback/markFlight/format/formatOrigin,
plus the record types (RerunEvent,ChangeRecord,ChangeOrigin,
HoldEvent, …) which were previously unexported. The runtime keeps only the
core's side asOBSERVE.attribution:install(hooks)/installed(the hook
slot an engine — built-in or a devtools' own — installs into) and
withInteraction(ref, fn)(the frame the web runtime opens around every event
dispatch;fn()when no engine is installed). A build that never imports the
engine never ships it: the observe tier costs ~1.3 KB brotli over prod on the
CSR scenario, the engine 9.7 KB more when enabled. The import is legal in
every tier — prod resolves an inert engine with the same surface.
@solidjs/diagnosticsrequiresOBSERVEand imports the engine itself; it now
works against observe builds.New build tier. Every package with wiring ships
<entry>.observe.{js,cjs}
beside its prod and dev artifacts, selected by a newobserveexport condition
(listed afterdevelopment, so dev still wins when both are set): signals
dist/observe/+dist/node.observe.cjs(each with anattributionentry
besideindex; the flat dev/CJS builds are code-split so both entries share
one module instance), solid-jssolid.observe.*and
server.observe.*, webweb.observe.*, universaluniversal.observe.*.
Observe builds keep attribution hook sites, owner labels (_name, flow-control
memo names, component roots), graph edge counters and the diagnostics channel;
they fold out strict-read checks, invariants, forbidden-scope guards, devtools
brands and all console output. Entries without wiring (frames, server-functions,
storage, h, html, element) fall through to prod underobserve. Signals gates
on__OBSERVE__(dev implies observe; asserted at init), solid-js/web/universal
on the"_SOLID_OBSERVE_"literal. Default prod artifacts are unchanged apart
from the newOBSERVE = undefinedexport;_nameis reserved from property
mangling so the cross-package label survives in the observe tree.
OBSERVE.diagnostics.emitaccepts an explicitownerPathfor hosts whose
owners are not signals' owners (the SSR runtime). -
a39415c: Breaking: all runtime packages are ESM only and declare
engines.node >= 22.12.Every
.cjsartifact, everyrequirebranch in the exports maps, and thetypes-cjs/declaration mirrors are gone. Node 22.12+ loads ESM throughrequire()natively, so a CommonJS host resolves the same files through the same export conditions it always did (browser,node,development,observe, …) — there is one module graph per tier rather than two to keep in step.mainnow points at the ESM server entry.For consumers:
- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later.
require("solid-js")keeps working. - TypeScript CommonJS projects: use
module: "NodeNext"(TS 5.8+), which type-checksrequire()of ESM packages;module: "Node16"will report TS1479. - Jest: needs Node 22.12+ for
require(esm); any preset that maps specifiers to.cjspaths (assolid-jestdoes for Solid 1.x) has nothing to map to and must be updated.
@solidjs/signalsdrops its flatdist/node*.cjsbuilds; its ESM entries (dist/prod/,dist/observe/,dist/dev.js) are the only ones.@solidjs/babel-pluginand@solidjs/compiler(build-time tooling loaded by Babel/Node) are unchanged. -
8cfa272: Require
serovalandseroval-plugins~1.6.7(minor-locked, as before). Seroval 1.6 ships bundled declarations with no extensionless relative imports, so the@solidjs/webserver-functions,serializationandframestype surfaces now type-check undermodule: NodeNextfrom a CommonJS project without `skipLib...
@solidjs/web@2.0.0-rc.8
Patch Changes
-
711b557: Move the #3338 diagnostics out of prod bytes. The
lazy()"not preloaded" explanation and the document-root preload-failure framing are dev-only; prod keeps terse messages and, at a document root, hands the preload failure itself toreportError(no wrapperError). ThehaltReactivityreportErrorhand-off is compacted. -
9e6c867: Document the
createEvent(request)contract: the request is a standards-shapedRequestand nothing more. Body-size enforcement may hand a rebuiltRequest, so host-specific fields on the inbound object are not carried; hosts surface platform handles on the event from their own request. -
7d985b6: Fix SSR XSS: strings yielded by flow-control memos rendered unescaped
<Show when={s}>{s}</Show>,<For>{v => v}</For>,<Dynamic component={() => s} />,
<Switch>/<Match>, boundary fallbacks and any component that returns a string through a
memo rendered that string raw on the server. The server flow controls return memos for
hydration-id alignment;escape()passed functions through by identity, and the resolver
appended whatever they later produced without escaping.One rule now:
escape(x)at a hole covers everything reachable fromx— strings, array
items, and what a function yields when the resolver calls it (a deferred-escape wrapper).
Finished{ t }nodes pass through.Loadingescapes its content the way it already
escaped its fallback. The compilers stop wrapping fragment / mixed component children in
_$escape(they are values; escaping them too double-escaped through
<Comp>{props.children}</Comp>), and a single-expression fragment at a hole keeps the
hole's wrap. Live-hole tags ride the wrapper and$slotsurvives the array copy, so
frames behave as before. -
3b4db21: Fix an rc.7 SSR hydration regression: a self-closing element that spreads props containing
children(<a {...props} />in a wrapper component) read the compiledchildrengetter twice on the server, building the child element twice and consuming a hydration id the client never allocates. Every element after the first such spread then hydrated against the wrong node and the client halted.ssrElementagain reads each spread key at most once and never readschildrenwhen JSX children are present; the textareavalue/defaultValue-as-content behaviour from #3286 is preserved. -
ded39d2: The flash cookie key is now derived with PBKDF2 instead of a single SHA-256 hash.
- Guessing a weak
secretfrom a captured cookie is now 100,000 times more expensive. - Set
secretto a high-entropy value of 32 bytes or more. The option docs show how to generate one. - Flash cookies in flight when you deploy read as no flash, the same as a secret rotation.
- Guessing a weak
-
fe3ab92: Make a failed lazy() hydration observable instead of a silently dead page (#3338):
- The client's "was not preloaded before hydration" error no longer says to add a Loading boundary — none is required for root-level
lazy(). It now names the actual cause: the server serialized no client entry for the module (check the server log for "Asset manifest returned no client assets for module"), or the hydration id namespaces are misaligned. - An uncaught error that halts the reactive system is handed to
reportErrorwhere the platform provides it, so it reacheswindow.onerror/ error monitoring. Creation-time throws (a lazy miss during the hydration render) are converted to status by ancestor recomputes and never reached the top; console.error was their only trace. hydrate()'s "module preload failed → fall back to client render" path no longer runs for a document root, where a client render is impossible (the shell cannot be created) and died deep in the walk with an unrelated "Hydration Mismatch" as an unhandled rejection. It now reports an explicit error carrying the preload failure as its cause.
- The client's "was not preloaded before hydration" error no longer says to add a Loading boundary — none is required for root-level
-
1807f7f: Observe tier: split dev-only checks from production-legal observability wiring.
Breaking (pre-release):
DEV.diagnosticsmoved to a newOBSERVEexport
—OBSERVE.diagnostics.{subscribe,capture,emit},OBSERVE.subjectOf(event).
DEVkeeps the devtools surface (hooks,getChildren/getSignals/
getParent/getSources/getObservers) and gains the console face
(DEV.report,DEV.setConsoleFooter— formerly
DEV.diagnostics.setConsoleFooter). Both are exported from@solidjs/signals
andsolid-js(client and server).Breaking (pre-release): the attribution engine is its own entry.
DEV.attribution.enable()and friends are now
import { attribution } from "solid-js/attribution"(or
@solidjs/signals/attribution) —enable/disable/subscribe/history/why/ subscriptions/costs/waterfalls/holds/feedback/markFlight/format/formatOrigin,
plus the record types (RerunEvent,ChangeRecord,ChangeOrigin,
HoldEvent, …) which were previously unexported. The runtime keeps only the
core's side asOBSERVE.attribution:install(hooks)/installed(the hook
slot an engine — built-in or a devtools' own — installs into) and
withInteraction(ref, fn)(the frame the web runtime opens around every event
dispatch;fn()when no engine is installed). A build that never imports the
engine never ships it: the observe tier costs ~1.3 KB brotli over prod on the
CSR scenario, the engine 9.7 KB more when enabled. The import is legal in
every tier — prod resolves an inert engine with the same surface.
@solidjs/diagnosticsrequiresOBSERVEand imports the engine itself; it now
works against observe builds.New build tier. Every package with wiring ships
<entry>.observe.{js,cjs}
beside its prod and dev artifacts, selected by a newobserveexport condition
(listed afterdevelopment, so dev still wins when both are set): signals
dist/observe/+dist/node.observe.cjs(each with anattributionentry
besideindex; the flat dev/CJS builds are code-split so both entries share
one module instance), solid-jssolid.observe.*and
server.observe.*, webweb.observe.*, universaluniversal.observe.*.
Observe builds keep attribution hook sites, owner labels (_name, flow-control
memo names, component roots), graph edge counters and the diagnostics channel;
they fold out strict-read checks, invariants, forbidden-scope guards, devtools
brands and all console output. Entries without wiring (frames, server-functions,
storage, h, html, element) fall through to prod underobserve. Signals gates
on__OBSERVE__(dev implies observe; asserted at init), solid-js/web/universal
on the"_SOLID_OBSERVE_"literal. Default prod artifacts are unchanged apart
from the newOBSERVE = undefinedexport;_nameis reserved from property
mangling so the cross-package label survives in the observe tree.
OBSERVE.diagnostics.emitaccepts an explicitownerPathfor hosts whose
owners are not signals' owners (the SSR runtime). -
a71e42e: Rebuild the buffered server-function request from its url, method, headers and signal instead of through the
Requestcopy constructor, so a host adapter's lazy request (Nitro via srvx) no longer fails every POST with 400 "Malformed server function arguments"; only a failed upload read answers 400 now, a failure to put the bytes back surfaces as its own error. -
a39415c: Breaking: all runtime packages are ESM only and declare
engines.node >= 22.12.Every
.cjsartifact, everyrequirebranch in the exports maps, and thetypes-cjs/declaration mirrors are gone. Node 22.12+ loads ESM throughrequire()natively, so a CommonJS host resolves the same files through the same export conditions it always did (browser,node,development,observe, …) — there is one module graph per tier rather than two to keep in step.mainnow points at the ESM server entry.For consumers:
- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later.
require("solid-js")keeps working. - TypeScript CommonJS projects: use
module: "NodeNext"(TS 5.8+), which type-checksrequire()of ESM packages;module: "Node16"will report TS1479. - Jest: needs Node 22.12+ for
require(esm); any preset that maps specifiers to.cjspaths (assolid-jestdoes for Solid 1.x) has nothing to map to and must be updated.
@solidjs/signalsdrops its flatdist/node*.cjsbuilds; its ESM entries (dist/prod/,dist/observe/,dist/dev.js) are the only ones.@solidjs/babel-pluginand@solidjs/compiler(build-time tooling loaded by Babel/Node) are unchanged. -
8cfa272: Require
serovalandseroval-plugins~1.6.7(minor-locked, as before). Seroval 1.6 ships bundled declarations with no extensionless relative imports, so the@solidjs/webserver-functions,serializationandframestype surfaces now type-check undermodule: NodeNextfrom a CommonJS project withoutskipLibCheck— the packaged-types check covers every public@solidjs/webspecifier. -
839c05e: Keep the server-function dispatch registry on
globalThisso a re-evaluated runtime (Vite's SSR program reload after an edit in dev) shares one registry with the RPC seam:query()-declared reads no longer answer 405 after the first HMR update (#3346). -
4e730a9:
spread()reads amerge()proxy through its sources instead of through the proxy. A spread mixed with other attributes compiles tospread(el, merge(statics, () => rest)); going through the proxy cost merge'skeys()(aSetplus an own-enumerable scan of every source) and then, per key, a right-to-leftinwalk of the sources, on every run. The spread now iterates the flattened sources directly — the union of own string keys, later sources overriding earlier,children/refexcluded — and enumerates each source through the same single-trap path asreadShallow().omit()is not a merge and stays opaque: it is enumerated through its own filtering trap. O...
@solidjs/universal@2.0.0-rc.8
Patch Changes
-
1807f7f: Observe tier: split dev-only checks from production-legal observability wiring.
Breaking (pre-release):
DEV.diagnosticsmoved to a newOBSERVEexport
—OBSERVE.diagnostics.{subscribe,capture,emit},OBSERVE.subjectOf(event).
DEVkeeps the devtools surface (hooks,getChildren/getSignals/
getParent/getSources/getObservers) and gains the console face
(DEV.report,DEV.setConsoleFooter— formerly
DEV.diagnostics.setConsoleFooter). Both are exported from@solidjs/signals
andsolid-js(client and server).Breaking (pre-release): the attribution engine is its own entry.
DEV.attribution.enable()and friends are now
import { attribution } from "solid-js/attribution"(or
@solidjs/signals/attribution) —enable/disable/subscribe/history/why/ subscriptions/costs/waterfalls/holds/feedback/markFlight/format/formatOrigin,
plus the record types (RerunEvent,ChangeRecord,ChangeOrigin,
HoldEvent, …) which were previously unexported. The runtime keeps only the
core's side asOBSERVE.attribution:install(hooks)/installed(the hook
slot an engine — built-in or a devtools' own — installs into) and
withInteraction(ref, fn)(the frame the web runtime opens around every event
dispatch;fn()when no engine is installed). A build that never imports the
engine never ships it: the observe tier costs ~1.3 KB brotli over prod on the
CSR scenario, the engine 9.7 KB more when enabled. The import is legal in
every tier — prod resolves an inert engine with the same surface.
@solidjs/diagnosticsrequiresOBSERVEand imports the engine itself; it now
works against observe builds.New build tier. Every package with wiring ships
<entry>.observe.{js,cjs}
beside its prod and dev artifacts, selected by a newobserveexport condition
(listed afterdevelopment, so dev still wins when both are set): signals
dist/observe/+dist/node.observe.cjs(each with anattributionentry
besideindex; the flat dev/CJS builds are code-split so both entries share
one module instance), solid-jssolid.observe.*and
server.observe.*, webweb.observe.*, universaluniversal.observe.*.
Observe builds keep attribution hook sites, owner labels (_name, flow-control
memo names, component roots), graph edge counters and the diagnostics channel;
they fold out strict-read checks, invariants, forbidden-scope guards, devtools
brands and all console output. Entries without wiring (frames, server-functions,
storage, h, html, element) fall through to prod underobserve. Signals gates
on__OBSERVE__(dev implies observe; asserted at init), solid-js/web/universal
on the"_SOLID_OBSERVE_"literal. Default prod artifacts are unchanged apart
from the newOBSERVE = undefinedexport;_nameis reserved from property
mangling so the cross-package label survives in the observe tree.
OBSERVE.diagnostics.emitaccepts an explicitownerPathfor hosts whose
owners are not signals' owners (the SSR runtime). -
a39415c: Breaking: all runtime packages are ESM only and declare
engines.node >= 22.12.Every
.cjsartifact, everyrequirebranch in the exports maps, and thetypes-cjs/declaration mirrors are gone. Node 22.12+ loads ESM throughrequire()natively, so a CommonJS host resolves the same files through the same export conditions it always did (browser,node,development,observe, …) — there is one module graph per tier rather than two to keep in step.mainnow points at the ESM server entry.For consumers:
- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later.
require("solid-js")keeps working. - TypeScript CommonJS projects: use
module: "NodeNext"(TS 5.8+), which type-checksrequire()of ESM packages;module: "Node16"will report TS1479. - Jest: needs Node 22.12+ for
require(esm); any preset that maps specifiers to.cjspaths (assolid-jestdoes for Solid 1.x) has nothing to map to and must be updated.
@solidjs/signalsdrops its flatdist/node*.cjsbuilds; its ESM entries (dist/prod/,dist/observe/,dist/dev.js) are the only ones.@solidjs/babel-pluginand@solidjs/compiler(build-time tooling loaded by Babel/Node) are unchanged. -
Updated dependencies [01ac18c]
-
Updated dependencies [711b557]
-
Updated dependencies [7d985b6]
-
Updated dependencies [fe3ab92]
-
Updated dependencies [0961d97]
-
Updated dependencies [1807f7f]
-
Updated dependencies [a39415c]
-
Updated dependencies [8cfa272]
-
Updated dependencies [4e730a9]
- solid-js@2.0.0-rc.8
@solidjs/signals@2.0.0-rc.8
Patch Changes
-
21c5460: Router-agnostic navigation attribution:
OBSERVE.attribution.withOrigin({ kind: "navigation", name, to, from, params }, fn)A navigation in Solid 2 is a plain write to the location; the runtime already sees everything it costs (the hold behind route data, the re-runs, the silence) but not that the writes were a navigation, or to which route.
withOriginis the seam where a router says so, around its write — the one router-specific line, living in the router. From it the attribution engine:- stamps the writes with a
navigationorigin (newChangeOrigin.kind, withname/to/from/params), nested under the enclosing interaction — including through an action step — so cause chains read— navigation to /users/:id (under click on a.nav "Alice"); - names holds by route:
HoldEvent.origin,SILENT_HOLD/LONG_HOLDmessages that start from the navigation, anddata.navigationon the event; - keeps one
NavigationEventper frame (attribution.navigations()), settled exactly once ascommitted(a plain drain took the writes),held(with theHoldEventattached), orsuperseded(a later write replaced them before they landed); - folds settled navigations per route into
feedback().navigations.
The ref is read late on purpose.
name/to/paramsare re-read from the object when the navigation settles, so a router whose match is coarse at write time (a lazy route subtree resolving inside the hold) assigns the exact pattern onto the same object and every consumer reads it — no second API. A redirect is declared withredirect: n(the hop depth routers already track) and folds onto the pending navigation instead of opening one: one record, timed from the user's request, the abandoned destination kept inNavigationEvent.redirects,feedback().navigations[].redirectedcounting them, andformatOriginreadingnavigation to /login (redirected from /users/42).Hold census fix: a
latest()/isPending()companion now counts as acknowledgement only when an effect reads it, through however many memos. Memos compute eagerly, so a router's internalcreateMemo(() => isPending(location))used to clearSILENT_HOLDfor every navigation whether or not anything rendered it.One new core hook,
flushEnd, fires once perflush()drain so the engine has the "committed and effects ran" instant for writes no transition held. Prod builds are unchanged (the hook site folds out; only the inert attribution twin gained the new empty queries). - stamps the writes with a
-
711b557: Move the #3338 diagnostics out of prod bytes. The
lazy()"not preloaded" explanation and the document-root preload-failure framing are dev-only; prod keeps terse messages and, at a document root, hands the preload failure itself toreportError(no wrapperError). ThehaltReactivityreportErrorhand-off is compacted. -
1354a53: Fix a render effect that reads sources written by two concurrent, non-entangled transactions committing the wrong value and then never updating (#3322). Effects have one value slot and do not entangle transactions, so the second transaction's recompute overwrote the value the first still owed a run for; the first's silent commit then published it, and the second found nothing left to run. Such effects are now re-derived against the committed world at each owed commit, ahead of the effect phase. The same mechanism covers a mainline recompute of an effect a live transaction had computed (the transaction's commit re-derives it), and render effects recomputing with no transaction active no longer see a foreign transaction's staged signal through the read fast path — the mask the slow path already applied.
-
ae0ec3f: Fix
deep()over optimistic and derived stores missing writes it should hear, and derived views churning row identities under an optimistic overlay (#3323).deep(view)/deep(view[i])over a derived view —createOptimisticStore(base)or a projection whose backing is another store — never re-ran when the base store was written, while per-key reads on the same view did. A view's targets chain to the inner store's proxies and base writes bump the inner record's witness nodes; the walk never subscribed them, and it resolved children to fresh non-chained wrappers of the base raw instead of the chained row targets the view serves. The walk now reads through the whole chain and resolves children to the targets the get trap would serve.deep()over any optimistic store was deaf to every write on a row added under a held action: the row lives in presence/value overrides, not the committed backing, and the walk enumerated the raw backing. TheownKeys/getOwnPropertyDescriptortrap bodies are now shared helpers the walk uses, so the walk sees exactly what readers see.- A derived view's untouched rows came back as fresh proxies for the life of an optimistic action (
view.map(r => r)was O(n) new identities per action) and snapped back at settle. The optimistic diff compared the inner store's child proxies to the draft's raws and marked every row changed; it now compares unwrapped values. Chained targets serving from a pending backing resolve inner-owned raws to the inner proxy before wrapping, andsnapshot()composes outer overrides below the root.
Only
deep(), the chained-view read path, and the optimistic diff changed; plain-store reads and writes are unaffected. -
1c9e9e7: Fix a held transaction being re-entered while an unrelated flush finalizes — through a store commit hook (
deep()readers of a projection), a boundary check, or a recompute — and that flush then committing the transaction's state and running its effects as if it still owned the batch, leaving the UI permanently stale once the transaction settled (#3319). Finalization now captures the batch it started with and settles nothing an entered transaction adopted (a completing transaction still settles its own separate containers). Effects follow ownership: a run is applied by the commit of the transaction that computed its value, so the entering flush still applies everything it computed mainline — the write that caused it reads and renders together — while runs owned by the still-held transaction park with it and release when it completes. Optimistic lanes are unaffected; they apply their own effects ahead of their transaction by design. -
b5bd6fb: Store twins of the lane-authority fixes. An optimistic store's optimistic write after a
yieldnow reveals on its lane with its derivations when the action's transaction already holds the same truth (#3330 store twin): adoptions under a live transaction hold on optimistic families too, a held adoption notifies its nodes at write time so the commit promotes silently instead of re-running every subscriber, and a tentative write is judged against the view readers see rather than the swapped-in backing. A derived optimistic store's own truth landing a different value over a tentative edit supersedes the override for the graph now, with action provenance (#3331 store twin): the authoritative landing reaches the engine's supersession, and a tracked reader of a superseded node reads the committed truth once the landing has committed ahead of the override's revert. Surfaced alongside and fixed in core: a reader that first links to a node while another transaction holds a staged write (an effect created during the hold, a store key first read under it) read the committed value but never learned of the commit — such readers now re-derive when the transaction reveals. Plain-storereconcileinside an action, and store keys first read under a held adoption, hold like every other write: handlers read committed,latest()the staged value, and the reveal comes with the transaction. -
b5bd6fb: A memo deriving from an optimistic value now reveals together with the override when the override is written after its transaction already staged the same derived result (e.g.
setOptimisticafter anawaitinside an action whose earlier write produced the same value). The lane recompute compared its result against the transaction-held value instead of the value on screen, called it unchanged, and left the derivation stale until the action committed (#3330). -
b5bd6fb: Fix optimistic lanes merged through a shared reader releasing their reveal while one member's async is still in flight (#3335). A lane's hold is a property of each pending async node — looked up in whichever live transaction observed it — not of the merged root's transaction, which after a cross-transaction merge recorded only one member's observations. A memo reading two optimistic values now reveals with both, as it does for plain signals (A15).
-
b5bd6fb: An optimistic override is superseded the moment its source recomputes the node with a different value — its own async landing, or a sync recompute driven by an upstream change (
createOptimistic(() => asyncMemo())): tracked derivations (memos, downstream async) recompute from the arrived truth immediately as held transaction work, instead of waiting for the override's own downstream flight to finish first — so the correction no longer takes two sequential round-trips (#3331). The override remains the displayed value for untracked reads and the applied frame until the transaction commits;latest()returns the arrived value andisPending()istruewhile they differ. An equal landing confirms silently. Only the override's own question or a newer one supersedes: when two rapid actions overlap on one node, the older action's late answer is held to the commit without moving the graph — a slow source does not leak back in over the user's latest intent. -
b5bd6fb: fix(signals): a reveal of a foreign-held flight shows the committed value unless the flight's inputs are visible; a same-value re-prediction renews the override's provenance
The A15 reveal corollary is re-ruled (review on #3347): a stale (render) reader that lands on a node pe...
@solidjs/html@2.0.0-rc.8
Patch Changes
-
a1ed7f3: Support
{/* ... */}style comments in tagged JSX templates, in addition to<!-- ... -->HTML comments. They are skipped at tokenize time, can contain template expressions, and may span string chunks. -
a39415c: Breaking: all runtime packages are ESM only and declare
engines.node >= 22.12.Every
.cjsartifact, everyrequirebranch in the exports maps, and thetypes-cjs/declaration mirrors are gone. Node 22.12+ loads ESM throughrequire()natively, so a CommonJS host resolves the same files through the same export conditions it always did (browser,node,development,observe, …) — there is one module graph per tier rather than two to keep in step.mainnow points at the ESM server entry.For consumers:
- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later.
require("solid-js")keeps working. - TypeScript CommonJS projects: use
module: "NodeNext"(TS 5.8+), which type-checksrequire()of ESM packages;module: "Node16"will report TS1479. - Jest: needs Node 22.12+ for
require(esm); any preset that maps specifiers to.cjspaths (assolid-jestdoes for Solid 1.x) has nothing to map to and must be updated.
@solidjs/signalsdrops its flatdist/node*.cjsbuilds; its ESM entries (dist/prod/,dist/observe/,dist/dev.js) are the only ones.@solidjs/babel-pluginand@solidjs/compiler(build-time tooling loaded by Babel/Node) are unchanged. -
Updated dependencies [711b557]
-
Updated dependencies [9e6c867]
-
Updated dependencies [7d985b6]
-
Updated dependencies [3b4db21]
-
Updated dependencies [ded39d2]
-
Updated dependencies [fe3ab92]
-
Updated dependencies [1807f7f]
-
Updated dependencies [a71e42e]
-
Updated dependencies [a39415c]
-
Updated dependencies [8cfa272]
-
Updated dependencies [839c05e]
-
Updated dependencies [4e730a9]
-
Updated dependencies [ab4c40c]
- @solidjs/web@2.0.0-rc.8
@solidjs/h@2.0.0-rc.8
Patch Changes
-
a39415c: Breaking: all runtime packages are ESM only and declare
engines.node >= 22.12.Every
.cjsartifact, everyrequirebranch in the exports maps, and thetypes-cjs/declaration mirrors are gone. Node 22.12+ loads ESM throughrequire()natively, so a CommonJS host resolves the same files through the same export conditions it always did (browser,node,development,observe, …) — there is one module graph per tier rather than two to keep in step.mainnow points at the ESM server entry.For consumers:
- ESM apps, Vite, Vitest, Bun, Deno, workers: no change.
- CommonJS Node apps: require Node 22.12 or later.
require("solid-js")keeps working. - TypeScript CommonJS projects: use
module: "NodeNext"(TS 5.8+), which type-checksrequire()of ESM packages;module: "Node16"will report TS1479. - Jest: needs Node 22.12+ for
require(esm); any preset that maps specifiers to.cjspaths (assolid-jestdoes for Solid 1.x) has nothing to map to and must be updated.
@solidjs/signalsdrops its flatdist/node*.cjsbuilds; its ESM entries (dist/prod/,dist/observe/,dist/dev.js) are the only ones.@solidjs/babel-pluginand@solidjs/compiler(build-time tooling loaded by Babel/Node) are unchanged. -
Updated dependencies [711b557]
-
Updated dependencies [9e6c867]
-
Updated dependencies [7d985b6]
-
Updated dependencies [3b4db21]
-
Updated dependencies [ded39d2]
-
Updated dependencies [fe3ab92]
-
Updated dependencies [1807f7f]
-
Updated dependencies [a71e42e]
-
Updated dependencies [a39415c]
-
Updated dependencies [8cfa272]
-
Updated dependencies [839c05e]
-
Updated dependencies [4e730a9]
-
Updated dependencies [ab4c40c]
- @solidjs/web@2.0.0-rc.8
@solidjs/element@2.0.0-rc.8
Patch Changes
- Updated dependencies [01ac18c]
- Updated dependencies [711b557]
- Updated dependencies [9e6c867]
- Updated dependencies [7d985b6]
- Updated dependencies [3b4db21]
- Updated dependencies [ded39d2]
- Updated dependencies [fe3ab92]
- Updated dependencies [0961d97]
- Updated dependencies [1807f7f]
- Updated dependencies [a71e42e]
- Updated dependencies [a39415c]
- Updated dependencies [8cfa272]
- Updated dependencies [839c05e]
- Updated dependencies [4e730a9]
- Updated dependencies [ab4c40c]
- solid-js@2.0.0-rc.8
- @solidjs/web@2.0.0-rc.8
@solidjs/diagnostics@2.0.0-rc.8
Patch Changes
-
0961d97: Observe tier: first-class interaction records and a typed record channel.
attribution.interactions()andInteractionEvent: one record perwithInteractiondispatch withat,handlerMs,writes,runs,created(computations built in its runs),runMs, theholdsandnavigationsattached, andsettledMs/outcome(idle|committed|held) once everything it caused is through.attribution.subscribe(type, listener)for"rerun" | "interaction" | "hold" | "navigation", delivered synchronously as each record completes; the baresubscribe(listener)form is unchanged.RerunEvent.atandHoldEvent.at— absolute times on theperformance.now()clock beside the existing durations.HoldEvent.acknowledgementsreplacesacknowledgedBy: one{ kind, source, reader? }per affordance,readerthe owner path of the effect that painted it.feedback().sources[].acknowledgedBystill ranks bykind:source.@solidjs/diagnosticsartifact format version 4 (holds carryacknowledgements; assertion evidence likewise).NavigationRef.paramsvalues may beundefined(an optional segment left unbound).OBSERVE.exclude(owner)/OBSERVE.isExcluded(subject)— an observer rendering inside the app it watches marks its own subtree; diagnostics about it are suppressed and the engine records none of its runs.solid-jsre-exports the tier types from its root:InteractionRef,NavigationRef,OriginRef,DiagnosticEventand friends, and the engine's record types (ChangeOrigin,RerunEvent,HoldEvent,NavigationEvent,InteractionEvent, …).
-
1807f7f: Observe tier: split dev-only checks from production-legal observability wiring.
Breaking (pre-release):
DEV.diagnosticsmoved to a newOBSERVEexport
—OBSERVE.diagnostics.{subscribe,capture,emit},OBSERVE.subjectOf(event).
DEVkeeps the devtools surface (hooks,getChildren/getSignals/
getParent/getSources/getObservers) and gains the console face
(DEV.report,DEV.setConsoleFooter— formerly
DEV.diagnostics.setConsoleFooter). Both are exported from@solidjs/signals
andsolid-js(client and server).Breaking (pre-release): the attribution engine is its own entry.
DEV.attribution.enable()and friends are now
import { attribution } from "solid-js/attribution"(or
@solidjs/signals/attribution) —enable/disable/subscribe/history/why/ subscriptions/costs/waterfalls/holds/feedback/markFlight/format/formatOrigin,
plus the record types (RerunEvent,ChangeRecord,ChangeOrigin,
HoldEvent, …) which were previously unexported. The runtime keeps only the
core's side asOBSERVE.attribution:install(hooks)/installed(the hook
slot an engine — built-in or a devtools' own — installs into) and
withInteraction(ref, fn)(the frame the web runtime opens around every event
dispatch;fn()when no engine is installed). A build that never imports the
engine never ships it: the observe tier costs ~1.3 KB brotli over prod on the
CSR scenario, the engine 9.7 KB more when enabled. The import is legal in
every tier — prod resolves an inert engine with the same surface.
@solidjs/diagnosticsrequiresOBSERVEand imports the engine itself; it now
works against observe builds.New build tier. Every package with wiring ships
<entry>.observe.{js,cjs}
beside its prod and dev artifacts, selected by a newobserveexport condition
(listed afterdevelopment, so dev still wins when both are set): signals
dist/observe/+dist/node.observe.cjs(each with anattributionentry
besideindex; the flat dev/CJS builds are code-split so both entries share
one module instance), solid-jssolid.observe.*and
server.observe.*, webweb.observe.*, universaluniversal.observe.*.
Observe builds keep attribution hook sites, owner labels (_name, flow-control
memo names, component roots), graph edge counters and the diagnostics channel;
they fold out strict-read checks, invariants, forbidden-scope guards, devtools
brands and all console output. Entries without wiring (frames, server-functions,
storage, h, html, element) fall through to prod underobserve. Signals gates
on__OBSERVE__(dev implies observe; asserted at init), solid-js/web/universal
on the"_SOLID_OBSERVE_"literal. Default prod artifacts are unchanged apart
from the newOBSERVE = undefinedexport;_nameis reserved from property
mangling so the cross-package label survives in the observe tree.
OBSERVE.diagnostics.emitaccepts an explicitownerPathfor hosts whose
owners are not signals' owners (the SSR runtime). -
Updated dependencies [21c5460]
-
Updated dependencies [711b557]
-
Updated dependencies [1354a53]
-
Updated dependencies [ae0ec3f]
-
Updated dependencies [1c9e9e7]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [b5bd6fb]
-
Updated dependencies [05725e8]
-
Updated dependencies [27aee36]
-
Updated dependencies [fe3ab92]
-
Updated dependencies [51c201f]
-
Updated dependencies [2fa7539]
-
Updated dependencies [0961d97]
-
Updated dependencies [1807f7f]
-
Updated dependencies [645ec0d]
-
Updated dependencies [12c3be9]
-
Updated dependencies [3a5fe8c]
-
Updated dependencies [a39415c]
-
Updated dependencies [dd1d4ed]
-
Updated dependencies [4e730a9]
-
Updated dependencies [4935c7d]
-
Updated dependencies [0f14430]
- @solidjs/signals@2.0.0-rc.8
@solidjs/compiler@2.0.0-rc.8
Patch Changes
-
01ac18c: Compiler
componentNamesoption: component owner labels that survive minification. With the flag on, DOM output carries the tag as written in source as a thirdcreateComponentargument —<Home />compiles tocreateComponent(Home, props, "Home"),<Ui.Button />to"Ui.Button",<this.Row />to"this.Row"— and the dev and observe runtimes label the component's owner with it (<Home>in diagnosticownerPaths, attribution chains, and the devtools_component.name), falling back toComp.nameas before. Until now an observe-tier production bundle reported hot scopes and holds under whatever the minifier left of the function name (<Xt> › <Kn>), and alazy()or HMR wrapper hid the tag name even in dev. Off by default and byte-identical output when off; SSR (which inlines the call) and universal output never emit it; the productioncreateComponentignores the argument. Both compilers implement it in parity (shared fixtures, cross-mode ratchet).@solidjs/vite-pluginenables it for the dev andobservepostures. -
8366e09: A
"use server"function declaration nested inside another function is now extracted like any other server function.- Previously the directive on a nested declaration was silently ignored: the body shipped to the client and ran there, while captures from the enclosing function were still rejected at compile time.
- The declaration is hoisted to a
constat the top of its block, so calling it before its source position still works. - Its id follows the binding path, such as
outer.inner.
-
7d985b6: Fix SSR XSS: strings yielded by flow-control memos rendered unescaped
<Show when={s}>{s}</Show>,<For>{v => v}</For>,<Dynamic component={() => s} />,
<Switch>/<Match>, boundary fallbacks and any component that returns a string through a
memo rendered that string raw on the server. The server flow controls return memos for
hydration-id alignment;escape()passed functions through by identity, and the resolver
appended whatever they later produced without escaping.One rule now:
escape(x)at a hole covers everything reachable fromx— strings, array
items, and what a function yields when the resolver calls it (a deferred-escape wrapper).
Finished{ t }nodes pass through.Loadingescapes its content the way it already
escaped its fallback. The compilers stop wrapping fragment / mixed component children in
_$escape(they are values; escaping them too double-escaped through
<Comp>{props.children}</Comp>), and a single-expression fragment at a hole keeps the
hole's wrap. Live-hole tags ride the wrapper and$slotsurvives the array copy, so
frames behave as before. -
a181e4d: A
"use server"directive on a method, getter, or setter is now a compile error instead of being silently ignored.- Those forms are never extracted, so the body kept running wherever it was called, browser included.
- Covers object literal methods and accessors, and class methods, accessors, and constructors.
- Assign a function to a property instead, which the pass does extract.
-
6bf2bf8: An arrow marked
"use server"that readsthisorargumentsis now a compile error instead of silently breaking at runtime.- The arrow is extracted to module top level, where
thisis undefined andargumentsdoes not exist. - A marked
functionis unaffected, and so is anyfunctionor class nested inside the server function.
- The arrow is extracted to module top level, where
-
bb905db: A module-level
"use server"module that exports something other than a server function is now a compile error instead of producing a client build with the export missing.- Covers re-exports,
export *, class and enum exports, destructured exports, and exports declared without an initializer. - The message names the export, its position, and what to do instead.
- Type-only and
declareexports are erased and stay allowed.
- Covers re-exports,
-
c0299bf: Server-function ids now name a function by its binding path, so two same-named functions no longer share one id.
makeA'ssubmitbecomesmakeA.submit-<hash>instead ofsubmit-<hash>.- Adding a same-named function no longer re-points the ids of the existing ones.
- Ids for functions in objects and classes pick up those names too, such as
handlers.save. - A named function contributes its own name as well, so
register(function saveHandler() {})insidewireiswire.saveHandlerrather than sharing an ordinal with its siblings.
-
ab4c40c: Object-valued
style/classbindings are read in the TRACKED half of their effect.style()andclassName()enumerate their object in the effect's untracked commit phase, so a proxy value — a store sub-object (style={state.style},class={row.classes}), merged props, anything arriving through a spread — was identity-reactive only: in-place key mutations never re-applied, and every leaf read trippedSTRICT_READ_UNTRACKEDin dev. Both compilers now wrap the compute value of a non-inlinestyle={expr}/class={expr}in a new compiler primitive,readShallow(), andspread()applies it to those two keys as it copies.readShallowis an identity passthrough for strings, plain objects and proxy-free arrays (a fresh literal is already the compute's own — the common case pays atypeof); a proxy is copied with oneownKeystrap (its own trap keeps the key set tracked) plus one tracked read per key; arrays are re-mapped only when an element is a proxy. Inline literals are untouched — they already compile per property. Provably-string expressions (string/template literals, concatenation) and literal objects/arrays skip the wrap at compile time. New Tier-1 benchstyle-class-object: plain-object rows at parity; store-backed rows go from identity-only (and, in dev, ~97 ms per 500 elements of diagnostics) to per-key reactive at ~3.5 ms. Octane svg-dashboard (prod build, store-backed style/attrs through spread): mount at parity, style_spread_pulse −6%, select_toggle −7%.spread()shares the same enumeration: its compute half copied the source withfor…in+hasOwn, which on a proxy source (merge()/omit(),{...props}in a component, store records — nearly every spread) is anownKeystrap plus twogetOwnPropertyDescriptortraps per key, each allocating a descriptor and a getter closure. It now takes the key set from oneReflect.ownKeystrap (the trap keeps the key set tracked) and reads each string key once; plain sources useObject.keys, the exact own-enumerable set the old loop yielded. New Tier-1 benchspread-enumerate(500 elements, 8 keys):merge(static, reactive)376 → 537 ops/s (+43%), store record 253 → 415 ops/s (+64%), plain object at parity.
@solidjs/babel-plugin@2.0.0-rc.8
Patch Changes
-
01ac18c: Compiler
componentNamesoption: component owner labels that survive minification. With the flag on, DOM output carries the tag as written in source as a thirdcreateComponentargument —<Home />compiles tocreateComponent(Home, props, "Home"),<Ui.Button />to"Ui.Button",<this.Row />to"this.Row"— and the dev and observe runtimes label the component's owner with it (<Home>in diagnosticownerPaths, attribution chains, and the devtools_component.name), falling back toComp.nameas before. Until now an observe-tier production bundle reported hot scopes and holds under whatever the minifier left of the function name (<Xt> › <Kn>), and alazy()or HMR wrapper hid the tag name even in dev. Off by default and byte-identical output when off; SSR (which inlines the call) and universal output never emit it; the productioncreateComponentignores the argument. Both compilers implement it in parity (shared fixtures, cross-mode ratchet).@solidjs/vite-pluginenables it for the dev andobservepostures. -
7d985b6: Fix SSR XSS: strings yielded by flow-control memos rendered unescaped
<Show when={s}>{s}</Show>,<For>{v => v}</For>,<Dynamic component={() => s} />,
<Switch>/<Match>, boundary fallbacks and any component that returns a string through a
memo rendered that string raw on the server. The server flow controls return memos for
hydration-id alignment;escape()passed functions through by identity, and the resolver
appended whatever they later produced without escaping.One rule now:
escape(x)at a hole covers everything reachable fromx— strings, array
items, and what a function yields when the resolver calls it (a deferred-escape wrapper).
Finished{ t }nodes pass through.Loadingescapes its content the way it already
escaped its fallback. The compilers stop wrapping fragment / mixed component children in
_$escape(they are values; escaping them too double-escaped through
<Comp>{props.children}</Comp>), and a single-expression fragment at a hole keeps the
hole's wrap. Live-hole tags ride the wrapper and$slotsurvives the array copy, so
frames behave as before. -
ab4c40c: Object-valued
style/classbindings are read in the TRACKED half of their effect.style()andclassName()enumerate their object in the effect's untracked commit phase, so a proxy value — a store sub-object (style={state.style},class={row.classes}), merged props, anything arriving through a spread — was identity-reactive only: in-place key mutations never re-applied, and every leaf read trippedSTRICT_READ_UNTRACKEDin dev. Both compilers now wrap the compute value of a non-inlinestyle={expr}/class={expr}in a new compiler primitive,readShallow(), andspread()applies it to those two keys as it copies.readShallowis an identity passthrough for strings, plain objects and proxy-free arrays (a fresh literal is already the compute's own — the common case pays atypeof); a proxy is copied with oneownKeystrap (its own trap keeps the key set tracked) plus one tracked read per key; arrays are re-mapped only when an element is a proxy. Inline literals are untouched — they already compile per property. Provably-string expressions (string/template literals, concatenation) and literal objects/arrays skip the wrap at compile time. New Tier-1 benchstyle-class-object: plain-object rows at parity; store-backed rows go from identity-only (and, in dev, ~97 ms per 500 elements of diagnostics) to per-key reactive at ~3.5 ms. Octane svg-dashboard (prod build, store-backed style/attrs through spread): mount at parity, style_spread_pulse −6%, select_toggle −7%.spread()shares the same enumeration: its compute half copied the source withfor…in+hasOwn, which on a proxy source (merge()/omit(),{...props}in a component, store records — nearly every spread) is anownKeystrap plus twogetOwnPropertyDescriptortraps per key, each allocating a descriptor and a getter closure. It now takes the key set from oneReflect.ownKeystrap (the trap keeps the key set tracked) and reads each string key once; plain sources useObject.keys, the exact own-enumerable set the old loop yielded. New Tier-1 benchspread-enumerate(500 elements, 8 keys):merge(static, reactive)376 → 537 ops/s (+43%), store record 253 → 415 ops/s (+64%), plain object at parity.