Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

comparison — the solid-rs feature lab

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

Dependencies

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 into

solid-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.

Styling

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.

Run it

# 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 reload

Open 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.

End-to-end tests

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.mjs

e2e/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.mjs

That covers hydrate without a server. For real server-rendered markup (and backend-generated initial state) see examples/ssr.

Notes

  • solid-rs.toml names the entry component (App); every src/*.rs file is parsed and each #[component] fn becomes one dist/js/<Name>.jsx module, plus dist/js/main.jsx that 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>.jsx module, 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 into main.js. The e2e proves the split is real by asserting main.js does not contain the chart's markup. (Under mode = "ssr" use client_only! instead — see examples/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_effect uses 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_resource is not used: the Solid 2.0 RC removed it (resource redesign); create_optimistic is the current alternative. Naming it (or create_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 from solid-js or @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_props says to use merge from solid-js (which is what Advanced does), and insert/spread/assign say the JSX transform emits them for you.
  • Module-level const items are emitted into dist/js/<module>.consts.jsx and imported by name, so src/theme.rs's ThemeContext is one object shared by the Provider in Advanced and the use_context in ThemeLabel.
  • Features exercises the Solid 2.0 feature set: options objects on create_signal/create_memo (a struct literal like SignalOptions { name: "x" } becomes a JS object literal), an effect phase that returns a cleanup function, a keyed <For> with a fallback (the keyed child callback receives an accessor, so {item.label} is emitted as item().label), Show keyed, a guarded match arm (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 as props.x).