Skip to content

Latest commit

 

History

History
306 lines (264 loc) · 32.3 KB

File metadata and controls

306 lines (264 loc) · 32.3 KB

solid-rs vs SolidJS 2.0 RC — API support

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

Legend

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:e2e

Unit coverage (parse + codegen, no DOM) is in crates/*/tests/.


1. solid-js — reactivity

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

2. solid-js — stores

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

3. solid-js — context and children

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

4. Components (exported by both solid-js and @solidjs/web)

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

5. @solidjs/web — client renderer

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

6. @solidjs/web — SSR

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

7. @solidjs/web — request/response and server functions

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, TextEncoder and 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/httpHeader are retracted on owner disposal. They register an onCleanup that undoes the write, so awaiting renderToStream into a string discards the entire response head. The emitted entry hands the stream to createSSRResponse instead.
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

8. @solidjs/web subpath packages

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

9. solid-rs language surface

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

10. Summary

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.