Every Solid 2.0 API solid-rs can transpile, running in one page. It is the
executable half of the repo-root comparison.md: that table says what is
supported, this says what the support actually does.
The page is one card per group of APIs. Each card names the functions it exercises — as a row of pills, so the page reads as a checklist — and then renders their output directly underneath. Almost every value on the page is asserted verbatim by one of the e2e suites, which is why they are set in monospace: they are output, not prose.
| File | Component | Demonstrates |
|---|---|---|
src/main.rs |
App |
the page shell: one <Section> per group of APIs |
src/section.rs |
Section |
the card frame — title, the API pills, the note, and the .lab styling hook |
src/basics.rs |
Basics |
create_signal / create_memo / create_effect, a dynamic attribute, if/else if/else, for, match, and the same signal driving a child's prop |
src/counter.rs |
Counter |
a self-contained signal + memo + effect with onclick handlers |
src/greeting.rs |
Greeting |
props struct, memo over props, method call |
src/frag.rs |
Frag |
fragment component (multiple root nodes) |
src/features.rs |
Features |
options objects, effect cleanup, keyed For + fallback, Show keyed, match guards, vec!/struct literals/for-loops, <Portal>, <Dynamic>, <Errored>, #[default(…)] props |
src/advanced.rs |
Advanced, Card, ThemeLabel, Boom |
create_store, create_render_effect, untrack, on_cleanup, create_unique_id, merge + {..spread}, class:/style:/on: (the last one a native listener for a custom event), {repeat … as i}, context, component children, panic! caught by <Errored>, <Loading> / <Reveal> / <NoHydration> / <Hydration> |
src/owner.rs |
OwnerLab |
get_owner, run_with_owner, is_disposed, get_observer, create_reaction, create_tracked_effect, flush, is_equal |
src/stores.rs |
StoreLab |
reconcile, store_path, map_array, snapshot, is_wrappable, deep, resolve |
src/misc.rs |
MiscLab, Badge, Kids |
omit, children, flatten, repeat |
src/async_lab.rs |
AsyncLab |
create_optimistic, create_optimistic_store, create_projection, is_pending, latest, on_settled |
src/web.rs |
WebLab |
dynamic, use_head, is_server / is_dev, affects, refresh |
src/action_lab.rs |
ActionLab |
#[action] — one call is one transaction |
src/ext.rs |
ExtLab |
enable_external_source (last on the page on purpose) |
src/npm.rs |
NpmLab |
#[js("nanoid")] extern "C" { … } — a package from [dependencies], bound and bundled |
src/theme.rs |
(const) | a module-level const holding a create_context(…), shared across files |
src/chart.rs |
HeavyChart |
lazy! — code-split into its own bundle chunk, loaded on demand |
There is no hand-written package.json. What this project needs from npm is in
solid-rs.toml:
[dependencies]
nanoid = "^5.0.0" # imported by the app, so it is in the bundle
[dev-dependencies]
"@tailwindcss/cli" = "^4.0.0" # compiles css/styles.css
jsdom = "^29.0.0" # the DOM the e2e suites render intosolid-rs install generates the package.json npm needs and runs
npm install. A build fails rather than installing anything itself — a build
that reaches the network without being asked is how a build stops being
reproducible — and it notices when the table has changed since the last
install.
src/npm.rs shows the other half: #[js("nanoid")] extern "C" { fn nanoid(n: i32) -> String; } makes the name importable, and the emitted module carries
import { nanoid } from "nanoid". The block is wasm-bindgen's shape and groups
everything from one package, but there is nothing behind it to marshal — an
imported function is just a function. Declaring it is what makes the import
exist: an undeclared name transpiles to a call on nothing.
Tailwind, compiled by @tailwindcss/cli from css/styles.css into
dist/styles.css. The dependency is in this project's own package.json, not
in the solid-rs toolchain: which stylesheet compiler an app uses is the app's
choice, unlike the Solid version, which is a property of the transpiler. The
build runs it because css/styles.css exists and starts with
@import "tailwindcss" — there is no setting for it.
The split between the two ways of writing Tailwind here is deliberate. The page
shell (main.rs, section.rs) carries utilities in the markup, because there
the layout is the thing you want to read. The lab interiors are styled
structurally with @apply, because there every element already carries a
semantic class the e2e suites select on, and repeating ten utilities across
ninety elements would bury the API each element exists to demonstrate.
# from the repo root (build the CLI first)
cargo build -p solid-rs
cd examples/comparison
# emit JSX only (no node/npm needed) — prints the generated .jsx
../../target/debug/solid-rs check
# full build (needs Node.js >= 18). The Solid toolchain is provisioned into
# ~/.cache/solid-rs on first build; `solid-rs install` is for this project's
# own dependencies, declared in [dependencies] in solid-rs.toml.
../../target/debug/solid-rs build # or: solid-rs build
../../target/debug/solid-rs dev # build to dist-dev/ + serve on :3000, hot reloadOpen http://localhost:3000. Editing a component under src/ swaps it in place
without reloading the page, so state elsewhere in the app is kept.
solid-rs install reads the [dependencies] and [dev-dependencies] tables
in solid-rs.toml, generates the package.json npm needs, and runs
npm install. There is no hand-written package.json and no index.html: the
document is generated, the stylesheet is linked because css/styles.css
exists, and the tab title is set from Rust with use_head.
e2e/render.mjs asserts the initial DOM and e2e/reactivity.mjs clicks the
buttons and asserts that signals, memos, effects, props, Show, For and
Switch all update (jsdom, no browser needed):
solid-rs install
solid-rs build
node e2e/render.mjs && node e2e/reactivity.mjs && node e2e/dev.mjse2e/reactivity.mjs also covers the <Errored> catch path: clicking swaps in
a component whose body is panic!("boom"), the boundary renders its render-prop
fallback with the thrown message, and clicking that recovers through reset().
The throw must happen inside a computation — a component body or a memo — for
Solid to route it to a boundary; thrown straight from the click handler it would
escape as an unhandled window error, which is why it goes through a signal.
e2e/dev.mjs asserts a development build of this page is clean. solid-rs dev builds against Solid's development runtime (hot reload requires it), and
that runtime enforces invariants the production build silently allows — writing
reactive state during render, reading a reactive value outside a tracking scope,
returning a non-cleanup from an effect callback. An uncaught one halts the
reactive system, so these are not advisory. Several were real defects here,
invisible until dev mode was turned on. Where such a write is deliberate it is
declared with SignalOptions { owned_write: true }; where it cannot be (a store
has no such option) the write was moved out of render.
e2e/hmr.mjs and e2e/hmr_wire.mjs cover hot module replacement — the swap
itself, and the dev server's poll that drives it. Both build their own fixture
project (two files, no package.json), so they also check that a solid-rs project
really can be nothing but Rust.
e2e/hydrate.mjs covers mode = "hydrate". It needs its own build, because
hydrate mode also injects the _$HY bootstrap script into the document:
solid-rs build --mode hydrate --out dist-hydrate
node e2e/hydrate.mjsThat covers hydrate without a server. For real server-rendered markup
(and backend-generated initial state) see examples/ssr.
solid-rs.tomlnames the entry component (App); everysrc/*.rsfile is parsed and each#[component]fn becomes onedist/js/<Name>.jsxmodule, plusdist/js/main.jsxthat mounts the entry.const LazyChart: Lazy = lazy!(HeavyChart);code-splits a component. The macro names a component, not a path — every component is its own<Name>.jsxmodule, so the specifier is derived. The const's name must differ from the component's, or callers would emit a static import and the chunk would be folded back intomain.js. The e2e proves the split is real by assertingmain.jsdoes not contain the chart's markup. (Undermode = "ssr"useclient_only!instead — seeexamples/ssr.)- Method names are passed through to JS verbatim, so the sample uses
JS-style names (
.split(","),.toUpperCase()) where the browser will execute them. create_effectuses Solid 2.0's two-closure form:create_effect(move || compute, move |value| side_effect). The effect phase is emitted as a block body because Solid 2.0 treats its return value as a cleanup function.create_resourceis not used: the Solid 2.0 RC removed it (resource redesign);create_optimisticis the current alternative. Naming it (orcreate_computed/batch/on/split_props) is a compile error that points at the replacement.- Any snake_case name in the runtime registry
(
crates/solid-rs-core/src/runtime.rs) can be called directly: it is camel-cased, auto-imported fromsolid-jsor@solidjs/web, and checked against its signature (argument count, closure arity, options objects, and how the result may be bound). A local binding with the same name shadows the import. - The registry holds only Solid 2.0 application API. Compiler-emitted
primitives are rejected with a pointer to the supported form — e.g.
merge_propssays to usemergefromsolid-js(which is whatAdvanceddoes), andinsert/spread/assignsay the JSX transform emits them for you. - Module-level
constitems are emitted intodist/js/<module>.consts.jsxand imported by name, sosrc/theme.rs'sThemeContextis one object shared by the Provider inAdvancedand theuse_contextinThemeLabel. Featuresexercises the Solid 2.0 feature set: options objects oncreate_signal/create_memo(a struct literal likeSignalOptions { name: "x" }becomes a JS object literal), an effect phase thatreturns a cleanup function, a keyed<For>with afallback(the keyed child callback receives an accessor, so{item.label}is emitted asitem().label),Show keyed, a guardedmatcharm (5 if n() > 3 => …),vec![…]/ struct literals /for-loops / compound assignment inside an effect phase, and the container builtins<Portal mount={…}>,<Dynamic component={Comp}>and<Errored fallback={<…/>}>(all take children and a JSX-valued prop).#[default(…)]gives a prop a fallback value (static props only — a reactive prop stays asprops.x).