Scope: solid-js@2.0.0-rc.1 and @solidjs/web@2.0.0-rc.1 (the versions pinned
in examples/).
This file lists only the public API — the surface solid-rs can target.
Every export whose .d.ts marks it @internal or "Compiler-emitted
primitive" is excluded: those are emitted by the JSX transform or consumed by
the runtime, and hand-writing them is not a supported way to build an app.
Naming one in Rust is a compile error that points at the supported form.
The lists below are exhaustive against the installed packages. They were
derived mechanically (enumerate Object.keys() of each entry point, then read
each symbol's declaring doc comment; where a symbol is declared in more than
one package the re-exporting package's docs win — solid-js documents what it
re-exports from @solidjs/signals, and its verdict governs).
| Surface | Public exports | ✅ | 🟡 | ➖ | ❌ |
|---|---|---|---|---|---|
solid-js |
56 | 52 | — | — | 4 |
@solidjs/web |
39 | 22 | — | 5 | 12 |
@solidjs/web/storage |
1 | — | — | 1 | — |
@solidjs/web/serialization |
9 | — | — | 9 | — |
@solidjs/web/server-functions |
28 | 4 | — | — | 24 |
@solidjs/web/frames |
14 | 12 | — | 1 | 1 |
| Mark | Meaning |
|---|---|
| ✅ | Supported and covered by an end-to-end test |
| 🟡 | Signature-checked and callable, but no e2e coverage |
| ➖ | Supported by the runtime, but deliberately not emitted — a better API covers the case |
| ❌ | Not supported |
Signature-checked means the API is in the runtime registry
(crates/solid-rs-core/src/runtime.rs) with its signature: writing
some_api(...) in Rust emits someApi(...), adds the right import, and
checks the call — argument count (across overloads), closure arity, options
objects, and how the result may be bound (let (a, b) = … for pair-returning
primitives, statement-only for void ones). Misuse is a compile error with
file:line:col. The registry covers 49 of the 49 public functions in the
two main packages that are callable from application code; the rest of the
public surface is components (§4), server-only (§7), or not callable at all.
End-to-end tests. examples/comparison covers the client path:
e2e/render.mjs (initial DOM), e2e/reactivity.mjs (clicks → signal/store/
memo/effect/Show/For/Repeat/Switch/prop/attribute updates), e2e/hydrate.mjs
(mode = "hydrate"). examples/ssr covers the server path: e2e/ssr.mjs
asserts the prerendered HTML, the serialized backend state, and that hydration
claims the server's nodes rather than rebuilding them.
cd examples/comparison && solid-rs build && npm run test:e2e
npm run build:hydrate && npm run test:e2e:hydrate
cd ../ssr && solid-rs build && npm run test:e2eUnit coverage (parse + codegen, no DOM) is in crates/*/tests/.
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
createSignal |
✅ | let (x, set_x) = create_signal(init[, Options { … }]); |
e2e: read, write, re-read. Options object supported (a struct literal → JS object literal), with keys camel-cased like any other name — owned_write: true reaches Solid as ownedWrite. Ordinary struct literals keep their keys, since field access is not rewritten |
createMemo |
✅ | let m = create_memo(move |[prev]| expr[, Options { … }]); |
e2e: initial + recompute. The compute closure may take the previous value and may have a block body |
createEffect |
✅ | create_effect(move || compute, move |value| effect[, Options { … }]); |
Solid 2.0 two-closure form. e2e: initial run + 9 clicks without halting. The effect phase may return () => cleanup (emitted as a block body, so a plain value return is never misread as a cleanup) |
createRenderEffect |
✅ | create_render_effect(move || compute, move |value| …); |
e2e: runs during render, reruns on a store write |
createRoot |
✅ | implicit | render/hydrate create the root. e2e: whole app |
createUniqueId |
✅ | let id = create_unique_id(); |
e2e: used as an element id through a merge spread |
untrack |
✅ | let v = untrack(move || store.count); |
e2e: the untracked read does not resubscribe |
onCleanup |
✅ | on_cleanup(move || …); |
a bare call statement. e2e: registration (the test app never unmounts, so disposal is not exercised) |
createReaction |
✅ | let track = create_reaction(move || …); |
e2e: track subscribes, the body fires on the next write |
createTrackedEffect |
✅ | create_tracked_effect(move || { … }); |
single closure, may return a cleanup. e2e: reruns on a tracked write |
runWithOwner |
✅ | run_with_owner(owner, move || …) |
e2e: the body runs inside the captured owner |
getOwner / getObserver |
✅ | get_owner() / get_observer() |
e2e: an owner exists in a component body, an observer does not |
flush |
✅ | flush(); / let v = flush(move || …); |
both overloads. e2e: the closure form returns its value after draining |
isDisposed |
✅ | is_disposed(owner) |
e2e: false for a live owner |
isEqual |
✅ | is_equal(a, b) |
e2e: both outcomes |
isPending |
✅ | is_pending(move || …) |
e2e: false with nothing in flight |
latest |
✅ | latest(move || …) |
e2e: reads the newest value |
onSettled |
✅ | on_settled(move || …); |
statement-only. e2e: fires |
affects |
✅ | affects(target[, key]); |
statement-only. e2e |
refresh |
✅ | refresh(target); |
statement-only. e2e |
repeat |
✅ | repeat(count, move |i| …) |
the primitive; the {repeat … as i} syntax emits the <Repeat> component instead. count is an accessor (move || 3), not a number. e2e |
action |
✅ | #[action] async fn save(args) { … yield; … } |
takes a generator function (=> Generator | AsyncGenerator), which is not an expression — so it is spelled as an item and emitted as action(async function* (…) { … }). yield is a real reserved Rust keyword, so syn parses it natively. Writing action(…) as a call is a compile error pointing here. e2e |
enableExternalSource |
✅ | enable_external_source(Config { … }); |
installs a global wrapper around every computation created afterwards. e2e (rendered last in the app for exactly that reason) |
createOptimistic |
✅ | let (v, set_v) = create_optimistic(init); |
the 2.0 replacement for resources. e2e: the override reverts once nothing is in flight |
createProjection |
✅ | let p = create_projection(move |draft| { … }, Seed { … }); |
the closure must have a block body: an expression body returns the assigned value, and Solid treats a returned value as the whole new state. e2e |
enableHydration |
❌ | — | undocumented and unnecessary: hydrate() installs the hydration runtime itself. Naming it is an error pointing at mode = "hydrate" |
createComponent |
❌ | implicit | compiler-emitted: every #[component] fn and <Foo /> call compiles to it, but it is not callable by hand |
NotReadyError |
❌ | — | there is no throw in the source language |
DEV |
❌ | — | dev tooling |
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
createStore |
✅ | let (s, set_s) = create_store(S { … }); |
e2e: property reads drive the DOM; the mutating-draft setter (set_s(move |d| { d.count = d.count + 1; })) updates it |
merge |
✅ | let m = merge(a, b); |
the application-facing prop-merge helper in 2.0 (mergeProps is compiler-internal). e2e: merged into an element via a {..m} spread |
createOptimisticStore |
✅ | let (s, set_s) = create_optimistic_store(S { … }); |
e2e |
mapArray |
✅ | map_array(list, move |item| …) |
list is an accessor, and the result is one too. e2e |
reconcile |
✅ | set_s(reconcile(next)) |
e2e: replaces the list wholesale |
storePath |
✅ | set_s(store_path("user", "name", "Grace")) |
variadic path segments then the value. e2e |
omit |
✅ | omit(props, "a", "b") |
e2e: the named prop is gone, the rest survive |
resolve |
✅ | resolve(move || …) |
returns a Promise, so .then(…) it — rendering it directly produces nothing. e2e |
snapshot |
✅ | snapshot(s) |
e2e: an untracked plain copy |
deep |
✅ | deep(v) |
e2e |
isWrappable |
✅ | is_wrappable(v) |
e2e: both outcomes |
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
createContext |
✅ | const Theme: Context<String> = create_context("light"); (module level) |
e2e. Module-level const/static items are emitted into dist/js/<module>.consts.jsx and imported by every component that names them, so one context object is shared across files |
useContext |
✅ | let theme = use_context(Theme); |
e2e: reads "dark" through the Provider |
| Provider | ✅ | <Theme value="dark"> … </Theme> |
the context value is the Provider component in Solid 2.0. e2e |
children |
✅ | let c = children(move || props.children); |
JSX children already arrive as the children prop (§9). e2e |
flatten |
✅ | flatten(children[, Options { … }]) |
e2e: flattens a resolved children() to a countable array |
lazy |
✅ | lazy!(Component) |
code-splits the named component into its own chunk. e2e in render, hydrate and ssr mode — under SSR it must sit inside a <Loading> boundary, whose fallback is what the server renders |
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
Show |
✅ | {if cond [keyed] { … } else if … { … } else { … }} |
e2e: all three branches + updates. keyed supported. Branches may be markup or a single expression ({if c { "a" } else { x + "b" }} → the expression is evaluated, not rendered as text) |
Switch / Match |
✅ | {match expr { lit => …, lit if guard => …, _ => … }} |
e2e: initial arm + update, incl. a guarded arm |
For |
✅ | {for item in expr [keyed expr] { … } [fallback { … }]} |
e2e: initial list + a keyed list clearing to its fallback. With a custom key the child callback receives an accessor (write {item.label}, emitted as item().label) |
Repeat |
✅ | {repeat count [from start] as i { … } [fallback { … }]} |
e2e: 3 rows with the index binding |
Errored |
✅ | <Errored fallback={…}> … </Errored> |
e2e: renders children, catches a panic! from a child component, and recovers through reset(). fallback takes a JSX element or a render prop: fallback={move |err, reset| { … }} — err is an accessor. A render prop taking no parameters is a compile error: Solid picks the render-prop path by reading the closure's arity, so a zero-parameter one is treated as a static value and the closure itself gets rendered |
Loading |
✅ | <Loading fallback={<Spinner/>} [on={…}]> … </Loading> |
e2e: children render, fallback stays out of the DOM. The pending path needs async reads, which the source language lacks |
Reveal |
✅ | <Reveal order="together"> … </Reveal> |
e2e: wraps a <Loading> group |
Portal |
✅ | <Portal mount={el}> … </Portal> |
e2e: renders children into document.body |
Dynamic |
✅ | <Dynamic component={Comp} …props /> |
e2e: renders Greeting with forwarded props |
Hydration |
✅ | <Hydration> … </Hydration> |
re-enters hydration inside a <NoHydration> subtree. e2e: renders children |
NoHydration |
✅ | <NoHydration> … </NoHydration> |
skips hydration for its subtree. e2e: renders children |
HydrationScript |
❌ | — | the component form of generateHydrationScript; the build injects the script into index.html directly instead |
Not in the 2.0 RC (1.x names — using one is an "unknown component" error):
Suspense, SuspenseList, Index, ErrorBoundary (became Errored).
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
render |
✅ | implicit (mode = "render", the default) |
the generated main.jsx mounts the entry. e2e |
hydrate |
✅ | implicit (mode = "hydrate" / "ssr") |
the client bundle is compiled with hydratable: true (without it hydration silently re-renders) and the build injects the _$HY bootstrap. e2e: examples/ssr hydrates real server markup and the claimed nodes survive |
| spread syntax | ✅ | <div {..expr}> / <Comp {..expr} /> |
e2e: class/title/id arrive from a merge(…) object. The JSX transform emits the underlying mergeProps/spread. A component call with a spread skips prop checking |
ref |
✅ (syntax) | ref={move |node| { el = node; }} with let mut el = … |
emitted as the JSX ref prop; let mut emits let (not const) so the binding is assignable. Not e2e-tested |
style / classList objects |
✅ (syntax) | style={Style { color: "red" }}, classList={CL { active: true }} |
struct literals → JS object literals. The namespaced forms are e2e-tested instead |
class: style: prop: attr: bool: use: |
✅ | class:active={cond}, style:color="red", use:directive={v} |
namespaced attribute names pass through verbatim (on: is the exception — it is expanded, above). e2e: class:active toggles on a store write, style:color applies |
on: namespace |
✅ | on:click=move || … |
Solid's non-delegated-listener form: the name after the colon is taken verbatim, so on:MyEvent reaches a custom event that delegation — a fixed, case-insensitive set — can never see. Expanded at emit to the runtime's own addEvent(el, name, handler) with delegation off, bound through the element's ref (the only hook the transform offers onto the element): the JSX transform cannot be trusted with it — it does not strip the namespace, so it would listen for an event named :click, and it lowercases the name on the way. A user ref is folded in and called first. Under SSR the ref does not run, so the binding waits for hydration. On a component it is just a prop of that name. unit + e2e (render, reactivity, hydrate, ssr) |
| event delegation | ✅ (implicit) | onclick=move || … |
events are emitted as JSX props and delegated by the runtime. e2e: 9 clicks across two components |
clientOnly |
✅ | client_only!(Component) |
code-splits, renders fallback on the server, loads in the browser. e2e under mode = "ssr" |
dynamic |
✅ | dynamic(move || …) |
the function form; <Dynamic> is the component. Must be a module-level const to be usable as a JSX tag. e2e |
useHead |
✅ | use_head(Tag { … }); |
statement-only. The text comes from the tag's children prop, not textContent. This is how a solid-rs app sets its title — there is no hand-written index.html. Under SSR the tags reach the served <head> through the render's onHead callback, because the render is a fragment with no </head> of its own. e2e: the served HTML and document.title |
isServer / isDev |
✅ | is_server / is_dev |
imported as values, not called. e2e |
| API | Status | solid-rs syntax | Notes |
|---|---|---|---|
renderToString |
➖ | — | not emitted: it is synchronous, so async reads inside a <Loading> boundary serialize the fallback. renderToStream is used instead |
generateHydrationScript |
✅ | implicit | injected into index.html by the build in hydrate/ssr mode. e2e |
renderToStream |
✅ | mode = "ssr" |
the emitted server.jsx awaits it, which resolves with the fully settled HTML — every <Loading> boundary (a lazy! component, an async memo) is resolved before the page is served. e2e. Errors route through onError and the promise still resolves, so renderPage captures and rethrows. The chunk-at-a-time pipe/readable forms are not wired to the HTTP response yet |
SSR is no longer prerendering. solid-rs preview renders per request in
the embedded QuickJS engine: the incoming method, URL, headers and body reach
the app, and the status, headers and cookies it writes during the render go
back on the wire. #[server] async fn adds RPC on top, dispatched at
/_server by the same process.
Two things made this possible and are worth stating, because they are not obvious from the API list:
- QuickJS has no Fetch API.
Request,Response,Headers,URL,FormData,ReadableStream,TextEncoderand friends are supplied by a shim (crates/solid-rs-ssr/src/web.js) that the SSR realm evaluates before the bundle. Its conformance suite runs under both Node and QuickJS — under Node the shim no-ops, so the assertions are checked against the real platform. httpStatus/httpHeaderare retracted on owner disposal. They register anonCleanupthat undoes the write, so awaitingrenderToStreaminto a string discards the entire response head. The emitted entry hands the stream tocreateSSRResponseinstead.
| API | Status | Notes |
|---|---|---|
getRequestEvent |
✅ | get_request_event(). e2e |
createRequestEvent, createSSRResponse |
✅ | used by the emitted server entry, not exposed to source |
httpStatus, httpHeader |
✅ | http_status / http_header. e2e |
redirect, reload, respond |
✅ | exposed; a page redirect is http_header("location", …), which createSSRResponse promotes. e2e |
parseCookieHeader, serializeCookie |
✅ | parse_cookie_header / serialize_cookie. e2e |
clearFlashCookie, hasFlashCookie |
❌ | the no-JS form-post convention is not wired up |
markSafeError |
✅ | opts a thrown error out of production sanitization; reachable via throw!(mark_safe_error(e)) — panic! builds its own Error, so there would be nothing to mark. e2e: a plain panic! in a #[server] fn reaches the browser as "Internal Server Error", a marked one keeps its message |
ResponseEnvelope, isResponseEnvelope, SAFE_ERROR, isSafeError |
➖ | internal to the protocol; nothing in the source language holds one |
createResponseStub, commitEventResponse, composeMiddleware |
❌ | no middleware layer |
getExpectedRedirectStatus, HREF, isHref, REVALIDATE_HEADER |
❌ | router-facing |
getServerFunctionMetadata, isServerFunction |
❌ | introspection; GET(…) and live(…) declarations are not exposed yet |
Separate entry points, none targeted. Listed so the absence is deliberate.
| Package | Public exports | Status | What it is |
|---|---|---|---|
@solidjs/web/storage |
1 (provideRequestEvent) |
➖ | async-local request context. Not imported: it needs node:async_hooks, which QuickJS lacks. A render gets a fresh realm serving exactly one request, so the emitted entry parks a plain value on Symbol.for("solid.RequestContext") — for one request per realm that is not an approximation of async context, it is equivalent |
@solidjs/web/serialization |
9 | ➖ | in use beneath solid-rs, never named in it: the RPC layer picks the encoding per value — plain JSON when the value is JSON-safe, seroval's framed format otherwise (a Date in a #[server] return arrives as a Date). #[server_state] still uses plain JSON.stringify, which is why that payload must be plain data |
@solidjs/web/server-functions |
28 | ✅ (4) | the RPC layer behind #[server] async fn: registerServerReference + createServerReference in the server half, a fetch-backed createServerReference in the client half, handleServerFunctionRequest at /_server. Single-flight mutations are wired when the app marks its router with #[router]: collectFlightData on the server, and subscribeFlightData on the client, which the router registers itself. Flash cookies and GET/live declarations are not exposed yet |
@solidjs/web/frames |
14 | ✅ (12) | server components behind #[server_component] async fn: serverComponentResponse streams the markup, frameTransformDirectResult renders it inline during document SSR, ServerComponentPlugin lets one travel through the hydration data as a reference, and installServerComponents is the client's whole binding — the dx-frame boundary, the response handler and the placeholder registry. frameTransformFlightResult folds a server component among single-flight data. And the layer below dispatch is nameable from Rust: server_component_response / render_to_frame_stream / is_frame_stream_response on the server, create_frame / create_frame_host / create_frame_element / get_frame_host / apply_frame_response on the client. Those five are client-dist-only, so they are routed through a generated two-halves loader whose server half throws — a component compiles into both bundles, and only one of them has a DOM. frameTransformResult is ➖: reimplemented rather than called, because upstream reads the function id from a WeakMap written only in the server-functions bundle and read only in the frames bundle, two prebuilt dists that never share it. asyncArg is the one ❌ left |
Source-level constructs, not runtime APIs.
| Construct | Status | Notes |
|---|---|---|
#[component] fn Name() -> Jsx |
✅ | return type enforced; e2e across both examples |
Props struct (props: NameProps) |
✅ | e2e: static + dynamic props, memo over props, #[default(…)] values. Props without a default are required. Defaulted props are destructured with a fallback and must be static — a reactive prop stays props.x so it stays live |
| Component children | ✅ | <Card label="x"><p>child</p></Card> — passed as the children prop, read as {props.children}. e2e |
Module-level const / static |
✅ | emitted into dist/js/<module>.consts.jsx and imported by name; how contexts are shared. e2e |
#[server_state] [async] fn name() -> T |
✅ | backend-generated initial state: runs only in Node, is serialized into the page as window.__SOLID_RS_STATE__, read by components via name(). Compiled into a shared __state.jsx so the body never reaches the browser. Requires mode = "ssr"; one per app; no parameters. e2e |
#[action] [async] fn name(args…) -> T |
✅ | transactional mutation: emitted as export const name = action(async function* (args) { … }) into the module's consts file and imported by name. Each call is one transaction batching every write between yields; .await gets a typed result but writes after it escape the transaction, so put a bare yield before them. Call it from an event handler, never during render. Module-level, so component state is passed in (e.g. a setter). e2e |
#[server] async fn name(args…) -> T |
✅ | RPC. The body runs on the server over either path: an ordinary in-process call during the document render, a POST /_server from the browser after hydration. Emitted into a per-target module pair — the browser build compiles __server.client.jsx under the name __server.jsx, so it is handed references with no bodies rather than bodies it is trusted to shake out. Consts reachable only from a #[server] body move into the server half too. async is required: the client half is always a fetch, so a synchronous body would resolve differently in the two builds from identical source. Names are app-global (one emitted module) and dispatch ids are <module>/<name> — never #, which the runtime treats as a reserved separator and truncates at. e2e, including a real click in a real page against a real server |
#[server_component] async fn Name(args…) -> Jsx |
✅ | A server component: same registration and same endpoint as #[server], but the body ends in html! and so returns markup. That difference is the whole protocol — the runtime streams a function result as a frame (HTML plus slot positions) and serializes anything else as a value. Rendered where the data is, so the data never ships: the e2e asserts the rows a story is built from appear in no client chunk. Both dispatch paths again — inline in the document at t = 0 (adopted into a dx-frame boundary), a frame stream that morphs into that same boundary after hydration. Mounted with <Dynamic component={…}/>; the runtime keys content per (function, arguments) and mounts per call site, so changing arguments delivers into the mounted instance rather than remounting it |
#[router] const Name = createRouter(…) |
✅ | Names which module-level const holds the router instance. One thing hangs off it and it is worth the attribute: the SSR entry hands that route tree to createFlightDataCollector, which is the server half of single-flight mutations — after a mutation the matched routes' preloads are re-run for the post-mutation URL and their query results are folded into the same response, with the mutation's own set-cookie already applied. The client half needs no wiring: the router subscribes as the transport's flight consumer on its own, and that subscription is what sends X-Single-Flight in the first place — so without the attribute the header goes out, nothing answers it, and every mutation costs a second round trip. At most one per app; rejected in render mode, which builds no server entry. e2e at the protocol level (examples/flight) |
slot!(name) |
✅ | A client-owned position inside a server component's markup. The server emits a marker range and never sees what fills it; the client fills the range and never learns what surrounds it. At t = 0 the client's own content is rendered server-side inside the range — the one hydration-time exception — and it survives later server morphs, which the e2e checks by node identity rather than by markup |
mode = "render" | "hydrate" | "ssr" |
✅ | solid-rs.toml [project] mode, overridable with solid-rs build --mode. e2e + CLI tests |
html! elements + static/dynamic attributes |
✅ | e2e: data-count={count()} updates on click |
| Hyphenated + namespaced attribute names | ✅ | data-*, aria-*, class:, style:, prop:, attr:, bool:, use:, on:. unit + e2e |
Spread attributes {..expr} |
✅ | unit + e2e |
Event handlers on* = move || expr |
✅ | any on* name; camel-cased on emit |
Render props (fallback={move |a, b| { … }}) |
✅ | recognized for fallback and children; elsewhere {move |…| …} is an ordinary Rust closure |
{if} / {for} / {match} / {repeat} |
✅ | → Show / For / Switch+Match / Repeat. e2e |
| Fragments | ✅ | multiple root nodes. e2e |
<Component … /> calls |
✅ | validated against declarations (unknown component/prop, missing prop → error with line:col) |
| Component body statements | ✅ | let x = …, let (a, b) = … (tuple → array destructuring), bare call statements, create_effect/create_render_effect, one html!. A non-call statement is an error |
let mut x = … |
✅ | emits let instead of const |
| Runtime signature checking | ✅ | argument count across overloads, closure arity, options-object shape, result binding. Unit-tested |
| Non-2.0 / internal APIs | ✅ | 1.x names (create_resource, create_computed, batch, on, split_props) and internals (merge_props, insert, spread, create_owner, create_error_boundary, …) are errors naming the supported form |
| Expression support | ✅ | literals, paths, calls, binary/unary, indexing, method calls, field access, move closures, let in closures, if/match as expressions, struct literals (→ objects), vec![…] (→ arrays), compound assignment, while/for-of, return, match guards, .await and yield (in #[server_state] / #[action] bodies) |
println! / format! macros |
✅ | → template literals. e2e |
async / await |
✅ | in a #[server_state] body, an #[action] body, and any async closure (async move || …). A bare .await in a #[component] body is a compile error — the component is emitted as a plain function, so it would be a JS syntax error; the message points at create_memo(async move || …) |
yield |
✅ | only inside #[action] — it is an action's transaction-safe suspension point. Anywhere else is a compile error (it would land in a non-generator function). yield and yield expr both parse |
dynamic import(…) |
✅ | via lazy! / client_only!, which name a component and derive the specifier (every component is its own module) |
lazy! under mode = "ssr" |
✅ | works once built with the official @dom-expressions/compiler (its dom/ssr targets allocate identical hydration ids; the old @oxc-solid-js/compiler did not — see BUGS-2.md). Requires a <Loading> boundary; client_only! is the alternative that takes its own fallback |
throw / error propagation |
✅ | panic!("bad {}", x) → throw Error(…), plus throw!(expr) to throw a value as it stands; todo!/unimplemented!/unreachable! lower the same way with std's messages. Emitted as a bare throw wherever a statement slot exists, as an IIFE in value position. e2e: caught on the client with err() and recovery via reset(), caught during the server render and replayed on hydrate, and thrown from a #[server] function |
Result / ? |
❌ | not modelled. Ok/Err/Some/None are compile errors naming the replacement — they previously emitted references to JS globals that do not exist |
Covered end to end: signals, memos, both effect kinds, untrack,
onCleanup, createUniqueId, stores (createStore + the mutating-draft
setter, merge), context (create/use plus the Provider component, shared
across modules via module-level consts), every control-flow and boundary
component (Show/Switch/Match/For/Repeat/Errored/Loading/
Reveal/Portal/Dynamic/Hydration/NoHydration), spread and namespaced
attributes, render props, component children, render, hydrate,
renderToStream + generateHydrationScript, #[server_state], #[action],
async memos, code splitting via lazy! / client_only!, and the server half:
per-request rendering with the real request in and the real status, headers and
cookies out, plus #[server] async fn RPC over both the in-process and the
HTTP path, and #[server_component] async fn server components — markup
rendered where the data lives, streamed as frames, with slot! marking the
client-owned positions inside it.
Plus, since every remaining 🟡 was closed: the owner and scheduling helpers
(getOwner/getObserver/runWithOwner/isDisposed/flush/createReaction/
createTrackedEffect/isEqual), the async reads (isPending/latest/
onSettled/affects/refresh), the optimistic and projection primitives, the
store helpers (reconcile/storePath/mapArray/snapshot/deep/
isWrappable/omit/resolve), children/flatten, the repeat primitive,
enableExternalSource, and dynamic/useHead/isServer/isDev. Nothing is
signature-checked-but-unproven any more — there is no 🟡 column left to fill.
Not supported (❌): Result/? are not modelled — failure is raised with
panic!/throw! and caught by <Errored>. Ok/Err/Some/None are
compile errors naming the replacement.
Async is
expressible — in #[server_state] and #[action] bodies and in async closures
— just not directly in a #[component] body, which is a plain function.
Code splitting is fully covered by lazy! / client_only!, in every mode.
Beyond Solid's own API. An app can also import from npm. Packages are
declared in [dependencies] / [dev-dependencies] in solid-rs.toml and
installed by solid-rs install (which generates the package.json npm needs —
there is none to write by hand), then bound in Rust with a wasm-bindgen-shaped
extern block:
#[js("nanoid")]
extern "C" {
fn nanoid(size: i32) -> String;
}Named, #[js(default)], #[js(namespace)] and side-effect-only (an empty
block) imports are all expressible. Nothing is type-checked against the package
— solid-rs transpiles rather than binding across an ABI — but the declaration is
what makes the import exist, and the import is emitted only into the modules
that use the name, so a package does not leak into chunks that do not need it.
e2e: examples/comparison calls nanoid and asserts the package is bundled.