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 withoutskipLibCheck— the packaged-types check covers every public@solidjs/webspecifier. -
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. Own keys only, per source: a key an earlier source owns and a later source merely inherits resolves to the earlier source's value (the proxy'sinwalk saw the inherited one) — spread has always applied own properties only.@solidjs/signalsgains an@internalmergeSources(). Guarded by the Tier-1spread-enumeratebench (merge(static, reactive)row).readShallow()re-maps a class array element-wise instead of copy-on-write (−58 B brotli onweb.js;classNameallocates for an array anyway, measured at parity). -
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