create-rivet 0.1.0-alpha.1 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +8 -1
  2. package/deploy.d.ts +1 -1
  3. package/deploy.js +4 -4
  4. package/index.d.ts +6 -4
  5. package/index.js +13 -5
  6. package/main.d.ts +4 -1
  7. package/main.js +142 -26
  8. package/output.d.ts +24 -0
  9. package/output.js +66 -0
  10. package/package.json +3 -1
  11. package/prompt.d.ts +1 -0
  12. package/prompt.js +2 -2
  13. package/templates/starter-minimal/.agents/skills/rivet-add-route/SKILL.md +1 -1
  14. package/templates/starter-minimal/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  15. package/templates/starter-minimal/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
  16. package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  17. package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  18. package/templates/starter-minimal/.claude/skills/rivet-add-route/SKILL.md +1 -1
  19. package/templates/starter-minimal/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  20. package/templates/starter-minimal/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
  21. package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  22. package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  23. package/templates/starter-minimal/.cursor/rules/rivet-add-route.mdc +1 -1
  24. package/templates/starter-minimal/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  25. package/templates/starter-minimal/.cursor/rules/rivet-port-react-component.mdc +3 -3
  26. package/templates/starter-minimal/.cursor/rules/rivet.mdc +6 -6
  27. package/templates/starter-minimal/.github/copilot-instructions.md +6 -6
  28. package/templates/starter-minimal/AGENTS.md +56 -78
  29. package/templates/starter-minimal/CLAUDE.md +56 -78
  30. package/templates/starter-minimal/README.md +3 -3
  31. package/templates/starter-minimal/package.json +2 -1
  32. package/templates/starter-minimal/src/document.ts +7 -6
  33. package/templates/starter-server/.agents/skills/rivet-add-route/SKILL.md +1 -1
  34. package/templates/starter-server/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  35. package/templates/starter-server/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
  36. package/templates/starter-server/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  37. package/templates/starter-server/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  38. package/templates/starter-server/.claude/skills/rivet-add-route/SKILL.md +1 -1
  39. package/templates/starter-server/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  40. package/templates/starter-server/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
  41. package/templates/starter-server/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  42. package/templates/starter-server/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  43. package/templates/starter-server/.cursor/rules/rivet-add-route.mdc +1 -1
  44. package/templates/starter-server/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  45. package/templates/starter-server/.cursor/rules/rivet-port-react-component.mdc +3 -3
  46. package/templates/starter-server/.cursor/rules/rivet.mdc +6 -6
  47. package/templates/starter-server/.github/copilot-instructions.md +6 -6
  48. package/templates/starter-server/AGENTS.md +56 -78
  49. package/templates/starter-server/CLAUDE.md +56 -78
  50. package/templates/starter-server/README.md +5 -4
  51. package/templates/starter-server/package.json +2 -1
  52. package/templates/starter-server/serve.js +2 -0
  53. package/templates/starter-server/src/document.ts +7 -6
  54. package/templates/starter-static/.agents/skills/rivet-add-route/SKILL.md +1 -1
  55. package/templates/starter-static/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  56. package/templates/starter-static/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
  57. package/templates/starter-static/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  58. package/templates/starter-static/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  59. package/templates/starter-static/.claude/skills/rivet-add-route/SKILL.md +1 -1
  60. package/templates/starter-static/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  61. package/templates/starter-static/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
  62. package/templates/starter-static/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  63. package/templates/starter-static/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  64. package/templates/starter-static/.cursor/rules/rivet-add-route.mdc +1 -1
  65. package/templates/starter-static/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  66. package/templates/starter-static/.cursor/rules/rivet-port-react-component.mdc +3 -3
  67. package/templates/starter-static/.cursor/rules/rivet.mdc +6 -6
  68. package/templates/starter-static/.github/copilot-instructions.md +6 -6
  69. package/templates/starter-static/AGENTS.md +56 -78
  70. package/templates/starter-static/CLAUDE.md +56 -78
  71. package/templates/starter-static/README.md +6 -5
  72. package/templates/starter-static/package.json +2 -1
  73. package/templates/starter-static/src/components/Counter.tsx +2 -6
  74. package/templates/starter-static/src/components/ReadingPlan.tsx +31 -0
  75. package/templates/starter-static/src/document.ts +7 -6
  76. package/templates/starter-static/src/routes/about.tsx +14 -1
  77. package/templates/starter-static/src/routes/index.tsx +2 -2
  78. package/templates/starter-ui/.agents/skills/rivet-add-route/SKILL.md +1 -1
  79. package/templates/starter-ui/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  80. package/templates/starter-ui/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
  81. package/templates/starter-ui/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  82. package/templates/starter-ui/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  83. package/templates/starter-ui/.claude/skills/rivet-add-route/SKILL.md +1 -1
  84. package/templates/starter-ui/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  85. package/templates/starter-ui/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
  86. package/templates/starter-ui/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  87. package/templates/starter-ui/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  88. package/templates/starter-ui/.cursor/rules/rivet-add-route.mdc +1 -1
  89. package/templates/starter-ui/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  90. package/templates/starter-ui/.cursor/rules/rivet-port-react-component.mdc +3 -3
  91. package/templates/starter-ui/.cursor/rules/rivet.mdc +6 -6
  92. package/templates/starter-ui/.github/copilot-instructions.md +6 -6
  93. package/templates/starter-ui/AGENTS.md +56 -78
  94. package/templates/starter-ui/CLAUDE.md +56 -78
  95. package/templates/starter-ui/README.md +5 -4
  96. package/templates/starter-ui/package.json +2 -1
  97. package/templates/starter-ui/src/components/LikeButton.tsx +1 -1
  98. package/templates/starter-ui/src/components/ReadingPreferences.tsx +18 -0
  99. package/templates/starter-ui/src/document.ts +7 -6
  100. package/templates/starter-ui/src/routes/index.tsx +3 -1
  101. package/templates/starter-ui/vite.config.ts +3 -1
@@ -6,15 +6,15 @@ Read this before editing this project. Rivet looks like React / Solid JSX but is
6
6
 
7
7
  ## 1. What Rivet is
8
8
 
9
- Rivet is a compiled, resumable JSX framework. A Rust compiler turns `.tsx` into server HTML (streamed) and DOM templates. A component function runs **once, on the server**; the browser never re-runs it. It **resumes** the page from the HTML plus serialized state: only event handlers, bindings that read signals, and client effects run in the browser, and their code loads on intent / visibility / idle. State is fine-grained signals (`@rivetjs/reactivity`). Routing is file-based (`@rivetjs/router`), i18n is compile-time (`@rivetjs/i18n`), forms are progressive (`@rivetjs/forms`), UI is `@rivetjs/ui` (styled compound components) on headless `@rivetjs/cdk`.
9
+ Rivet is a compiled, resumable JSX framework. A Rust compiler turns `.tsx` into server HTML (streamed) and DOM templates. A component function renders **on the server**. The browser **resumes** an interactive boundary by running its ordinary compiled DOM function over the existing HTML on its first event, signal wake or client trigger, using the recorded signal values. The loader can fetch code ahead of use; client effects can also start boundaries without a user event. State is fine-grained signals (`@rivetjs/reactivity`). Routing is file-based (`@rivetjs/router`), i18n is compile-time (`@rivetjs/i18n`), forms are progressive (`@rivetjs/forms`), UI is `@rivetjs/ui` (styled compound components) on headless `@rivetjs/cdk`.
10
10
 
11
11
  ## 2. Differences from React / Solid
12
12
 
13
13
  ### Mental model
14
14
 
15
- - No virtual DOM, no re-renders. A signal change updates only the text / attribute / row that reads it. No `useMemo`, `useCallback`, dependency arrays, or hooks rules: `signal()` / `computed()` can be created anywhere.
16
- - Component body = server code. `window`, `document`, `localStorage` there are a compile error (RV2001). `console.log` in the body prints in the terminal.
17
- - `typeof window !== "undefined"` branches in a component never run in the browser. Use a handler, `clientEffect`, `clientEvent` or `clientOnly`.
15
+ - No virtual DOM, no re-renders. A signal change updates only the text / attribute / row that reads it. No `useMemo`, `useCallback`, dependency arrays, or React hooks ordering rules. In resumable components, create per-item state in a child component rather than calling `signal()` / `createStore()` in a loop (RV2104).
16
+ - Component bodies run during SSR and again when their boundary resumes. Browser-global reads (`window`, `document`, `localStorage`) during server rendering are errors (RV2001). Keep browser-only work in handlers or client helpers; a body log can run on both sides.
17
+ - Do not branch rendered markup on `typeof window`: the two sides must render the same structure. Use a handler, `clientEffect`, `clientEvent` or `clientOnly` for browser-only work.
18
18
 
19
19
  ### JSX
20
20
 
@@ -34,8 +34,6 @@ Rivet is a compiled, resumable JSX framework. A Rust compiler turns `.tsx` into
34
34
  ```
35
35
 
36
36
  - Two-way form binding: `<input model:value={name} />`, `<input type="checkbox" model:checked={agree} />`, also `<select model:value={choice}>`. The value must be a signal name/property.
37
- - No JSX inside handlers, effects or computeds (RV2006): render JSX in the component, switch it with a signal.
38
- - No `this` in handlers (RV2007): use `event.currentTarget`.
39
37
 
40
38
  ### Signals (`@rivetjs/reactivity`)
41
39
 
@@ -50,15 +48,16 @@ untrack(count); // read without subscribing (there is no .peek())
50
48
  ```
51
49
 
52
50
  - Avoid `count(5)` (not a setter) and `count.value`.
51
+ - In an `effect` / `computed` / `clientEffect`, write from the current value with `update`: `count.set(count() + 1)` there subscribes the effect to its own write and re-runs it until the 10000-run cutoff (RV1017). In event handlers `count.set(count() + 1)` is fine (nothing tracks there).
53
52
  - `computed` / `effect` must be synchronous (an async fn is a type error).
54
- - Stores: `createStore` from `@rivetjs/reactivity/store`.
53
+ - Stores: `createStore`, `read` and `write` from `@rivetjs/reactivity/store`: `const state = createStore({ done: false })`, `read(state, "done")`, `write(state, "done", (done) => !done)`. Writes copy data; do not mutate a value returned by `read`.
55
54
 
56
55
  ### Effects, lifecycle, cleanup
57
56
 
58
57
  - In components use `clientEffect` from `@rivetjs/runtime/dom`: recorded on the server, runs only in the browser, re-runs when its signals change. Return a function for cleanup.
59
- - `effect` from `@rivetjs/reactivity` runs immediately where called — in a component that means on the server only. Use it in plain TS / tests.
58
+ - `effect` from `@rivetjs/reactivity` runs immediately where called, including during SSR and when the component resumes. Use `clientEffect` for browser-only work.
60
59
  - `onCleanup(fn)` (reactivity) registers cleanup for the current effect / root; throws outside one.
61
- - Options: `clientEffect(fn, { when: "idle" | "load" | "visible" | "interaction" | "media:(…)" })`. Default `visible` if it uses a ref, else `idle`.
60
+ - Options: `clientEffect(fn, { when: "idle" | "load" | "visible" | "interaction" | "media:(…)" })`. On derived pages, the default and `load` schedule idle adoption; `visible` observes the boundary's first element intersecting the viewport. `interaction` / `media:` adopt on the first event in the boundary, not on a media-query change. Once the boundary runs, the effect starts in a microtask.
62
61
  - Async `clientEffect`: only signals read before the first `await` are tracked; returned cleanup works only from non-async effects.
63
62
 
64
63
  ```tsx
@@ -76,7 +75,7 @@ clientEffect(() => {
76
75
  ### Props are live (Solid-style)
77
76
 
78
77
  - `<Badge count={count()} />` passes a getter; the child sees updates when it reads `props.count` where it is shown.
79
- - Destructuring reads once: on a resumed page a destructuring child is re-created on the first change (local signals reset). Read `props.x`.
78
+ - Destructuring reads once: a destructuring child never sees later values. Read `props.x`.
80
79
  - Object spread `{ ...props }` / `Object.assign` reads every getter once; JSX spreads `<button {...props} />` stay live.
81
80
  - Defaults / forwarding: `mergeProps`, `splitProps` from `@rivetjs/runtime/props`.
82
81
  - `count={count}` passes the signal itself (child calls `props.count()` / writes it). Use for two-way props.
@@ -116,7 +115,7 @@ import { ref } from "@rivetjs/runtime/dom";
116
115
  const input = ref<HTMLInputElement>();
117
116
  <input ref={input} />; // input() → element | undefined (signal-backed)
118
117
  let el!: HTMLCanvasElement;
119
- <canvas ref={(e) => (el = e)} />; // callback must assign a local (RV2002)
118
+ <canvas ref={(e) => (el = e)} />; // or a callback
120
119
  ```
121
120
 
122
121
  ### Context
@@ -134,31 +133,20 @@ import { Provider } from "@rivetjs/runtime/dom";
134
133
  const theme = useContext(ThemeContext); // import { useContext } from "@rivetjs/reactivity"
135
134
  ```
136
135
 
137
- - The provided value is read once: provide a signal for changing values. It must be serializable (data / signals, no service objects).
136
+ - The provided value is read once: provide a signal for changing values.
138
137
 
139
138
  ## 3. Resumability rules
140
139
 
141
- Everything a handler, effect, computed or binding uses from its component (its **captures**) is written into the HTML. It must be serializable.
140
+ The server writes the HTML plus a short record per interactive component (it has a handler, reads a signal in its markup, or uses an effect): where it is, its signal values, its props when they are plain data. The first event, signal wake or client trigger runs the boundary's compiled DOM function over the existing HTML. Handlers, functions, service objects and JSX props have no capture-serialization rules: the browser reconstructs them when the component runs.
142
141
 
143
- - OK: primitives, plain objects/arrays, `Date`, `URL`, `URLSearchParams`, `Map`, `Set`, `RegExp`, typed arrays, `ArrayBuffer`, `DataView`, `Error`, `bigint`, signals, `computed`, and functions Rivet compiled: inline arrows, local `const` functions, inline callback props, functions in object literals, returned functions, inline functions written into a value a handler uses (`api.inc = () => …`, `signal(() => …)`, `[() => …]`), local functions stored by name into such a value (`api.open = open`, `list.push(open)`), module-level or imported functions used by name inside a component (called by a handler, passed as a prop, kept in a local const, put in an object / array / call argument).
144
- - Not OK: class instances (`new Intl.NumberFormat()`, `new Api()` → RV2003; a data class declared in the component module resumes — no `#private` field, no base other than a data class of that module, no decorator, no `window` / `document` read while constructing, else RV2102; `reconstruct(() => new Api(…))` from `@rivetjs/runtime/dom` builds it in the browser on first read (no value in the page); a resource holder resumes with `[serialize]()` + `static [revive](data)`, a package class with `registerClass(Cls, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`), `Promise`, `WeakMap`, `Symbol`, `.bind(...)` written inside an object literal (RV2014; inline in a prop / handler or a local `const inc = add.bind(null, 2)` of a compiled function used by name is OK; `.bind(ctx)` of a `this` function stays RV2007), methods / getters, functions using `this`, functions created at runtime by libraries (`memoize(fn)`), module-level / imported functions put into objects only outside a component (module level, hooks, helpers).
145
-
146
- ```tsx
147
- // Avoid: captures a class instance
148
- const fmt = new Intl.NumberFormat("en", { style: "currency", currency: "EUR" });
149
- <button onClick={() => alert(fmt.format(n()))}>Pay</button>
150
- // Prefer: create it where it runs
151
- <button onClick={() => alert(new Intl.NumberFormat("en", { style: "currency", currency: "EUR" }).format(n()))}>Pay</button>
152
- ```
153
-
154
- - Wrap foreign functions in an arrow: `onPress={() => analytics.track("save")}`.
155
- - Imports are not captures: import modules at the top, create objects inside the handler, keep browser objects (EventSource, chart instances) inside `clientEffect`.
156
- - Render props and item renderers take functions: `({ close }) => …` on Dialog / Modal / Popover, `{(item) => …}` on ListBox / Select / Tabs, Tabs `panel`. Plain JSX children are fine elsewhere: a closed overlay renders nothing on the server and creates its content when it opens.
157
- - Render a function child as `{props.children}` directly; a copy in a local (`const c = props.children`) is not tracked on resumed pages.
158
- - `For each={xs}` (accessor) keeps resumed rows and branches better than `xs().map(...)`.
142
+ - Render the same thing on both sides: no `Date.now()` / `Math.random()` / `new Date()` / `crypto.randomUUID()` in a render (RV2103) — keep it in a signal (`signal(Date.now())`, the server value ships) or compute it in a handler / `clientEffect`. One signal per creation site: no `signal()` inside a loop (RV2104 warning). Browser globals only in handlers / `clientEffect` / `clientEvent` / `clientOnly` (RV2001).
143
+ - Keep signal/store values as plain JSON data (null, booleans, finite numbers, strings, arrays and plain objects). In this derived implementation a recorded non-JSON signal value is omitted; adoption uses its browser initializer and a structural mismatch falls back to browser rendering. Do not rely on it retaining server state. The richer resume-format encoding below applies to route data.
144
+ - Plain-data props can let the build make a component independent. Functions, services and JSX props are allowed; a component given non-data props adopts with its parent. There is no handler-capture restriction.
145
+ - Route loader/action data must be encodable (RV2107 runtime warning; the page renders on the server but cannot resume): no functions, symbols, promises, unregistered class instances. Resume-format data also supports `Date`, `Map`, `Set`, `URL`, `bigint` and typed arrays. Classes: `registerClass(Cls, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`, imported on server and browser.
146
+ - Render a function child as `{props.children}` directly. `For each={xs}` with `by` for lists that change.
159
147
  - `.ts` files are not server-rewritten: import `@rivetjs/runtime/dom` (`For`, `Show`, `clientEffect`, …) only from `.tsx` files, else the DOM runtime is bundled into SSR.
160
- - Components outside `src/routes` need no registration: `rivetRouter` resumes every `.tsx` / `.jsx` file a route imports.
161
- - Errors: build/prerender prints `Cannot resume <file>#<Component>$<hash>: function is not serializable`. The dev server names the capture (``capture `<name>` ``); dev builds (`NODE_ENV=development vite build`) give its position (`capture #<i>`) and the value path. Fixes: build the object inside the component; a library object holding functions → `reconstruct(() => createChart(…))` (`@rivetjs/runtime/dom`; module level = one server instance per process, no request state in it); a class instance → `reconstruct(() => new X(…))`, `[serialize]()` + `static [revive](data)`, or for a package class `registerClass(X, { id, serialize, revive })` (`@rivetjs/runtime/resume-format`).
148
+ - With the default `resume.appModules: true`, project `.tsx` / `.jsx` files imported by routes need no registration (outside `node_modules` and within the project root).
149
+ - A component whose markup differs in the browser renders there instead of reusing the server nodes (dev prints a warning naming it).
162
150
  - Playwright tests: the first trusted mouse click per document flushes DOM updates one task later. After a page's first `page.click`, wait (`expect(...).toHaveText` retries / `waitForFunction`), don't read the DOM synchronously.
163
151
 
164
152
  ## 4. File routes
@@ -179,7 +167,7 @@ src/
179
167
  _404.tsx _error.tsx
180
168
  ```
181
169
 
182
- - Files/folders starting with `_` (except `_layout`, `_404`, `_error`), `.`, `*.test.*`, `*.d.ts` are ignored. `*.server.ts` = server-only module: browser code cannot use it (RV3008 in a route module's browser part); a page that imports one itself becomes a document route (full page load), a component, layout or error page that reaches one fails the build (RV3011).
170
+ - Files/folders starting with `_` (except `_layout`, `_404`, `_error`), `.`, `*.test.*`, `*.spec.*`, `*.d.ts` are ignored. `*.server.ts` = server-only module: browser code cannot use it (RV3008 in a route module's browser part); a page that imports one itself becomes a document route (full page load), a component, layout or error page that reaches one fails the build (RV3011).
183
171
  - `.rivet/` is generated (route types `routes.d.ts`, editor manifest): never edit; `rivet sync` regenerates it.
184
172
 
185
173
  ### Page module exports
@@ -210,7 +198,7 @@ export default function Post({ data, params }: PageProps<"blog/[slug]">) {
210
198
  ```
211
199
 
212
200
  - Server exports: `load(…)`, `actions(…)`, `guard(…)` (throw/return `redirect()`, `deny()`, `notFound()`, `error()`), `headers(…)`, `entries(…)` (prerender params), `prerender`, `onRequest(…)` / `onGet(…)` / `onPost(…)` …. `load(fn, { defer: true })` = the page streams without waiting: `data` is a `Deferred` handle, render it only through `<Async load={() => data}>` (layouts: `data()`) or `resource(() => data)`, never `data.field`; it cannot set headers / cookies. The compiler strips them (and code only they use) from the browser build.
213
- - `event`: `request`, `url`, `params`, `locals`, `signal`, `cookies`, `setHeader`.
201
+ - `event`: `request`, `url`, `params`, `locals`, `signal`, `cookies`, `setHeader`, `appendHeader`.
214
202
  - Page props: `{ data, params, url }` — plain values. Layout props (`LayoutProps`): `data()`, `params()`, `url()` are **accessors**, plus `children`.
215
203
 
216
204
  ```tsx
@@ -234,7 +222,7 @@ export default function Layout({ data, children }: LayoutProps<typeof loader>) {
234
222
 
235
223
  ### Links and navigation
236
224
 
237
- - Plain `<a href="/about">` works (client navigation). Typed: `import { Link } from "@rivetjs/router/link"`; `<Link to="/items/42">`.
225
+ - Plain `<a href="/about">` works; it uses client navigation when the page has the client router and document navigation otherwise. Typed: `import { Link } from "@rivetjs/router/link"`; `<Link to="/items/42">`.
238
226
  - `import { navigate, invalidate, useSearchParams, setSearchParams, beforeLeave, actionResult } from "@rivetjs/router/client"`.
239
227
  - Under a base path, plain anchors need `withBase("/x")` from `@rivetjs/router`.
240
228
  - Never call `history.pushState` yourself: the router does not see it.
@@ -242,7 +230,7 @@ export default function Layout({ data, children }: LayoutProps<typeof loader>) {
242
230
  ### Modes (`vite.config.ts`)
243
231
 
244
232
  - `rivetRouter()` = `mode: "server"` (render per request; `npm run build` → `dist/server`, `npm start`).
245
- - `rivetRouter({ mode: "static" })` = prerender to HTML files (`prerender.js` / `prerenderBuild`). `load(…)` runs at build time. A page with nothing to resume (no handlers, signals in markup, client effects, forms) ships zero JS and its links load documents; `clientRouter: true` keeps in-place navigation on every page.
233
+ - `rivetRouter({ mode: "static" })` = prerender to HTML files (`prerender.js` / `prerenderBuild`). `load(…)` runs at build time. A page with nothing to resume (no handlers, signals in markup, client effects, forms) ships zero JS and its links load documents (any mode: the client router is opt-in); `clientRouter: true` keeps in-place navigation on every page.
246
234
  - `mode: "csr"` = SPA, no server code.
247
235
 
248
236
  ### Forms (`@rivetjs/forms`)
@@ -304,6 +292,7 @@ export default function Contact() {
304
292
  - Compile-time: `t("key")` is replaced by the string for the locale being built; no runtime catalog. One client build per locale.
305
293
  - `i18n.config.ts` (`locales`, `defaultLocale`, `catalogs: "locales/{locale}.json"`, `routing`); Vite: `plugins: [i18n(config), rivetRouter()]` — `i18n()` from `@rivetjs/i18n/vite`, **before** the router.
306
294
  - Catalogs: JSON, nested keys → dotted, values ICU MessageFormat (`"Hello, {name}!"`, plural / select).
295
+ - Component strings (`@rivetjs/cdk` / `@rivetjs/ui` labels, date / number formats) ship every locale unless the app declares the ones it serves: `rivetRouter({ locales: ["en-US", "de-DE"] })` (default: `i18n()` `locales`). Declared, other languages load their strings on first use and may show `en-US` text until then: list every language you serve.
307
296
 
308
297
  ```tsx
309
298
  import { t } from "@rivetjs/i18n";
@@ -312,13 +301,14 @@ import { t } from "@rivetjs/i18n";
312
301
  <p>{t("terms", { link: (c) => <a href="/terms">{c}</a> })}</p>
313
302
  ```
314
303
 
315
- - Key = string literal; params = inline object literal. Unknown key RV4001, missing / extra param RV4002, bad args RV4003, tags in attributes RV4004.
304
+ - Use a literal key and inline object params. A fixed-prefix template key such as ``t(`nav.${id}`)`` is also supported for plain-text messages without params. Unknown key RV4001, missing / extra param RV4002, bad args RV4003, tags in attributes RV4004.
316
305
  - Server code outside rendering (emails in actions): `t.in("de", "cart.title")`. Never `String(t(…))` on the server.
306
+ - Current language: `locale()` from `@rivetjs/i18n` (call during render; loaders / actions: `event.locals.locale`). `t()` values work in attributes (`Picture alt`), route `head` and kit `<Title>` / `<Meta>`.
317
307
  - Generated types: `src/rivet-i18n.d.ts` (do not edit). Locale URLs: `[[lang]]` route folder.
318
308
 
319
309
  ## 6. Styles
320
310
 
321
- - Templates use Tailwind v4: `src/styles.css` (`@import "tailwindcss"; @source "./**/*.{ts,tsx}";`), listed in `rivetRouter({ styles: ["src/styles.css"] })`, plus `tailwindcss()` Vite plugin. Write utilities in `class`.
311
+ - Static, server and UI starters use Tailwind v4 (minimal has no styling tools): `src/styles.css` (`@import "tailwindcss"; @source "./**/*.{ts,tsx}";`), listed in `rivetRouter({ styles: ["src/styles.css"] })`, plus `tailwindcss()` Vite plugin. Write utilities in `class`.
322
312
  - Scoped CSS (experimental): ``const styles = css`.card { … }` `` at module top level, `import { css, cx } from "@rivetjs/runtime/css"`; use `class={styles.card}`, combine with `cx(styles.card, on() && styles.selected, props.class)`.
323
313
  - Dynamic values: `style={{ "--x": n(), marginTop: "1px" }}` or `style:color={c()}`; `data-*` attributes for state styling.
324
314
 
@@ -379,7 +369,7 @@ import { Select } from "@rivetjs/ui/select";
379
369
  ```
380
370
 
381
371
  - `@rivetjs/cdk`: headless 1:1 ports of React Aria / React Stately / RAC / Angular CDK, one subpath per module (`@rivetjs/cdk/select`, `@rivetjs/cdk/dialog`, `@rivetjs/cdk/overlay`, …). Unstyled: default `rv-*` classes + `data-*` state attributes. Overlays use a div layer in a portal (no native `<dialog>` / `popover`). Use it for custom-styled widgets; prefer `@rivetjs/ui` when its look fits.
382
- - Animations: `@rivetjs/motion` (Motion / motion.dev API, peer `motion` 14.x, no React): `<Motion.div initial={…} animate={…} exit={…} whileHover={…} whilePress={…}>` (`whilePress`, not `whileTap`), `Presence` / `PresenceFor` from `/presence` (not `AnimatePresence`; cdk / ui overlays and kit `Transition` already are presences), `layout` / `layoutId` + `animateLayout(scope, update)` from `/layout` (layout animates inside `animateLayout`, not on every render), `createScroll` / `createInView` / `prefersReducedMotion` accessors. Entrances render final styles + CSS on the server (no JS); values Motion's mini engine cannot animate (`rotateX`, `pathLength`, …) are a dev error: use `animate` from `@rivetjs/motion/animate` in a handler or `clientEffect`. Gestures with physics (drag, flick, carousel, sheet) stay cdk `motion` (`@rivetjs/cdk/motion`); there is no `drag` prop.
372
+ - Animations: `@rivetjs/motion` (Motion / motion.dev API, peer `motion` 14.x, no React): `<Motion.div initial={…} animate={…} exit={…} whileHover={…} whilePress={…}>` (`whilePress`, not `whileTap`), `Presence` / `PresenceFor` from `/presence` (not `AnimatePresence`; cdk / ui overlays and kit `Transition` already are presences), `layout` / `layoutId` + `animateLayout(scope, update)` from `/layout` (layout animates inside `animateLayout`, not on every render), `createScroll` from `/scroll`, `createInView` from `/in-view`, and `prefersReducedMotion` from `/reduced-motion` return accessors. Entrances render final styles + CSS on the server (no JS); values Motion's mini engine cannot animate (`rotateX`, `pathLength`, …) are a dev error: use `animate` from `@rivetjs/motion/animate` in a handler or `clientEffect`. Gestures with physics (drag, flick, carousel, sheet) stay cdk `motion` (`@rivetjs/cdk/motion`); there is no `drag` prop.
383
373
 
384
374
  ## 9. Commands
385
375
 
@@ -389,56 +379,44 @@ import { Select } from "@rivetjs/ui/select";
389
379
  | `npm run build` | all | `vite build && vite build --ssr` (+ `node prerender.js` for static / ui / minimal) |
390
380
  | `npm start` | server | `node serve.js` (`PORT`, `HOST`) |
391
381
  | `npm run preview` | static, ui | serve `dist/client` |
392
- | `npm run check` | all | `rivet sync && rivet-check` |
382
+ | `npm run check` | all | `rivet sync && rivet-check && tsgo` |
393
383
 
394
384
  - Create: `npm create rivet@latest my-app -- --template static|server|ui|minimal --yes`.
395
- - `rivet sync` (bin of `@rivetjs/router`): regenerates `.rivet/` (route types + `.rivet/editor.json`). Run on a fresh clone before `tsc`.
396
- - `rivet-check` (bin of `@rivetjs/language-core`): compiler diagnostics for both targets + type-aware checks for every `.tsx` / `.jsx` of `tsconfig.json`. Flags: `-p <tsconfig|dir>` (repeatable), `--no-types`, `--no-manifest`, `--format text|json`. Exit 0 clean (warnings ok), 1 errors, 2 setup error. Run it after every change and before reporting done; also `npx tsc -p .`.
385
+ - `rivet sync` (bin of `@rivetjs/router`): regenerates `.rivet/` (route types + `.rivet/editor.json`). Run on a fresh clone before `tsgo`. `--quiet` hides status output; `--json` returns one result; `--verbose` includes error stacks. Exit 0 success, 1 failure, 2 usage error.
386
+ - `rivet-check` (bin of `@rivetjs/language-core`): compiler diagnostics for both targets + type-aware checks for every `.tsx` / `.jsx` of `tsconfig.json`. Flags: `-p <tsconfig|dir>` (repeatable), `--no-types`, `--no-manifest`, `--format text|json` / `--json`, `--quiet` (errors only; JSON keeps totals), `--verbose` (stacks; also `DEBUG=rivet`). Repeated warnings share one code/file report with a count; JSON retains their locations. Exit 0 clean (warnings ok), 1 errors/setup failure, 2 invalid arguments. Run it after every change and before reporting done. RV3101 (a link matching no route) stands down when `to` is typed with the generated route union: `tsgo` (TypeScript 7 native, part of `npm run check`) reports TS2820 then.
397
387
  - Editor: `@rivetjs/typescript-plugin` in `tsconfig.json` shows the same diagnostics + quick fixes while typing.
398
388
  - Measure in a build (`npm run build` + start/preview), not dev.
399
389
  - Tests (optional setup): `rivetTest()` from `@rivetjs/testing/vite`; `render`, `fire`, `flush` from `@rivetjs/runtime/testing`; `renderToString` from `@rivetjs/runtime/testing/server`.
400
390
 
401
391
  ## 10. RV diagnostic codes
402
392
 
403
- - Format `RVnnnn`. Compiler errors fail the build; compiler warnings (RV2012–RV2014 on a `.bind` receiver that may be an object with its own `bind`, RV2015 on a returned accessor that cannot resume) print and the module still compiles; the language service adds type-aware warnings.
393
+ - Format `RVnnnn`. Compiler errors fail the build; compiler warnings (RV2104: keyed state created in a loop) print and the module still compiles; the language service adds route-link warnings (RV3101).
404
394
  - Where: Vite (terminal + overlay) as `[RV1016] src/x.tsx:12:8: message`; `rivet-check` as `file:line:col - error RV1016: message`; editor via the TS plugin. The MCP tool `explain_diagnostic("RV1016")` gives the meaning, the mechanical fix and an Avoid / Prefer pair; `check_code` returns the fix and the fixed source for a whole file.
405
395
  - Ranges: 1xxx JSX / attributes, 2xxx resume, 3xxx imports / helpers / route modules, 4xxx i18n, 5xxx styles, 6xxx slots, 7xxx config, 9xxx internal.
406
396
 
407
- | Code | Meaning | Fix |
408
- | --------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
409
- | RV1000 | Parse error | fix syntax |
410
- | RV1005 | HTML nesting the parser would repair (`<div>` in `<p>`) | valid nesting |
411
- | RV1012 | Unknown native event (`onclick`, `onTransitionend`) | `on` + PascalCase |
412
- | RV1015 | Handler is a literal, not a function | pass a function |
413
- | RV1016 | Handler expression reads state when it binds | wrap in an arrow |
414
- | RV1101 | Unsafe / unsupported attribute or URL (`innerHTML`, `javascript:`) | remove; build-time assets |
415
- | RV1102 | Invalid `preload` value | `eager` / `idle` / `visible` / `intent` / `interaction` |
416
- | RV2001 | Browser global read during server rendering | handler / `clientEffect` / `clientEvent` / `clientOnly` |
417
- | RV2002 | Ref callback must assign a local | `ref={(el) => (local = el)}` or `ref()` |
418
- | RV2003 | Captured class instance | pass data; inside handler; `reconstruct(…)` |
419
- | RV2102 | Captured instance of a module class that is not a data class (`#private`, base, resource) | inside handler; `reconstruct(…)`; plain fields |
420
- | RV2006 | JSX inside handler / effect / computed | JSX in component + signal |
421
- | RV2007 | Resumable code captures `this` | `event.currentTarget` |
422
- | RV2009 | Two module references share a browser name | rename one import |
423
- | RV2011 | Mutually recursive functions split by an early exit | move them together before the exit |
424
- | RV2012–RV2014 | (warning; error on a provable function) `.bind` that may not resume | `const g = f.bind(t, …)`; arrow for other callees |
425
- | RV2015 | (warning) component returns a function that cannot resume (anon default / wrapper / `let`) | `export function Name(props) { return () => …; }` |
426
- | RV2016 | (warning) `resource()` that cannot resume (destructured, `let`, not a `const` initializer) | one `const user = resource(…)`; quick fix |
427
- | RV2101 | (warning) captured value not serializable (type-aware) | as RV2003 |
428
- | RV2103 / RV2104 | `resume: { model: "derived" }` only: `Date.now()` / `Math.random()` while rendering / (warning) signal created in a loop | keyed `signal(Date.now())` or `clientEffect`; one signal outside the loop |
429
- | RV3003 / RV3004 | Structural helper namespace import / alias | `import { For } from "@rivetjs/runtime/dom"` |
430
- | RV3006 | `clientOnly` erasure shape | inline `() => import("…")` |
431
- | RV3007 | Route module server export shape (`export *` in a route) | declare server exports directly |
432
- | RV3008 | Server-only code reached from browser code | keep in `load(…)` / `*.server.ts` |
433
- | RV3011 | Server-only module reached from a component, layout or error page (router build) | pass it as props from the page's `load(…)` |
434
- | RV3101 | (warning) Link matches no route | fix the path |
435
- | RV4001–4004 | i18n key / param / argument / tag errors | see section 5 |
436
- | RV5001–5003 | ``css`…` `` syntax / usage; invalid or unsafe class / style (style string, two class attrs) | style object, one `class` |
437
- | RV6001–6004 | Slot errors (`<slot>`, `r:name`, `r:slot`) | see JSX reference, slots |
438
- | RV7101 / RV7102 | `.rivet/editor.json` missing / stale | `rivet sync` |
439
- | RV9000 / RV9001 | Unclassified / internal compiler error | report it |
440
-
441
- - Not a code: render-time `… is not serializable` (see section 3).
397
+ | Code | Meaning | Fix |
398
+ | --------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
399
+ | RV1000 | Parse error | fix syntax |
400
+ | RV1005 | HTML nesting the parser would repair (`<div>` in `<p>`) | valid nesting |
401
+ | RV1012 | Unknown native event (`onclick`, `onTransitionend`) | `on` + PascalCase |
402
+ | RV1015 | Handler is a literal, not a function | pass a function |
403
+ | RV1016 | Handler expression reads state when it binds | wrap in an arrow |
404
+ | RV1101 | Unsafe / unsupported attribute or URL (`innerHTML`, `javascript:`) | remove; build-time assets |
405
+ | RV1102 | Invalid `preload` value | `eager` / `idle` / `visible` / `intent` / `interaction` |
406
+ | RV2001 | Browser global read during server rendering | handler / `clientEffect` / `clientEvent` / `clientOnly` |
407
+ | RV2103 / RV2104 | resume modules: `Date.now()` / `Math.random()` while rendering / (warning) signal created in a loop | keyed `signal(Date.now())` or `clientEffect`; one signal outside the loop |
408
+ | RV2107 | (runtime warning) Route data not encodable (function, symbol, promise, unregistered class) | plain data; `registerClass` |
409
+ | RV3003 / RV3004 | Structural helper namespace import / alias | `import { For } from "@rivetjs/runtime/dom"` |
410
+ | RV3006 | `clientOnly` erasure shape | inline `() => import("…")` |
411
+ | RV3007 | Route module server export shape (`export *` in a route) | declare server exports directly |
412
+ | RV3008 | Server-only code reached from browser code | keep in `load(…)` / `*.server.ts` |
413
+ | RV3011 | Server-only module reached from a component, layout or error page (router build) | pass it as props from the page's `load(…)` |
414
+ | RV3101 | (warning) Link matches no route | fix the path |
415
+ | RV4001–4004 | i18n key / param / argument / tag errors | see section 5 |
416
+ | RV5001–5003 | ``css`…` `` syntax / usage; invalid or unsafe class / style (style string, two class attrs) | style object, one `class` |
417
+ | RV6001–6004 | Slot errors (`<slot>`, `r:name`, `r:slot`) | see JSX reference, slots |
418
+ | RV7101 / RV7102 | `.rivet/editor.json` missing / stale | `rivet sync` |
419
+ | RV9000 / RV9001 | Unclassified / internal compiler error | report it |
442
420
 
443
421
  ## 11. Links
444
422
 
@@ -454,7 +432,7 @@ import { Select } from "@rivetjs/ui/select";
454
432
 
455
433
  ## Checklist before reporting done
456
434
 
457
- - `npm run check` exits 0; `npm run build` passes (prerender / SSR catches non-serializable captures).
435
+ - `npm run check` exits 0; `npm run build` passes.
458
436
  - No destructured props in components that receive changing values; lists that change use `For` with `by`.
459
437
  - No browser globals outside handlers / `clientEffect` / `clientEvent` / `clientOnly`.
460
438
  - Icons only via `<Icon icon={…} />` + lucide.
@@ -6,7 +6,7 @@ One page and one piece of state, no styling tools.
6
6
  npm install
7
7
  npm run dev # dev server
8
8
  npm run build # HTML in dist/client
9
- npm run check # Rivet compiler diagnostics (CI)
9
+ npm run check # Rivet compiler diagnostics + tsgo (CI)
10
10
  ```
11
11
 
12
12
  ## Next steps
@@ -20,7 +20,7 @@ npm run check # Rivet compiler diagnostics (CI)
20
20
  ## Tooling
21
21
 
22
22
  - Node.js 24 or newer (`engines` in `package.json`).
23
- - `npm run check` runs `rivet sync` (writes the gitignored `.rivet/` route types + editor manifest from `vite.config.ts`; also run it before `tsc` on a fresh clone) then `rivet-check`: the Rivet compiler's diagnostics (browser globals read during server rendering, handlers that cannot resume, …) for every `.tsx` file of `tsconfig.json`. Exits 1 on an error; use it in CI.
23
+ - `npm run check` runs `rivet sync` (writes the gitignored `.rivet/` route types + editor manifest from `vite.config.ts`; also run it before `tsgo` on a fresh clone), `rivet-check` the Rivet compiler's diagnostics (browser globals read during server rendering, non-deterministic renders, …) for every `.tsx` / `.jsx` file of `tsconfig.json`, then `tsgo` (TypeScript 7 native type check, incl. typed route links: a `Link` path that matches no route is TS2820). Exits 1 on an error; use it in CI.
24
24
  - `@rivetjs/devtools` is a dev dependency, so `npm run dev` shows the DevTools panel (Alt+Shift+R or the round **R** button). Builds never include it. Turn it off with `rivetRouter({ devtools: false })`.
25
- - `tsconfig.json` lists `@rivetjs/typescript-plugin`: editors that use the project's TypeScript show compiler errors while you type. `tsc` ignores the entry.
25
+ - `tsconfig.json` lists `@rivetjs/typescript-plugin`: editors that use the project's TypeScript show compiler errors while you type. `tsgo` ignores the entry (it loads no plugins); editors run the plugin on the `typescript` 5.9 package, which is why the starter keeps both.
26
26
  - `vite.config.ts` passes options to `rivetRouter()`: `mode`, `styles`, `resume`, `devtools`, `dev.resume`, `transitions`, … All options: the docs page "Configuration" (`reference/config`).
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "dev": "vite",
11
11
  "build": "vite build && vite build --ssr && node prerender.js",
12
- "check": "rivet sync && rivet-check"
12
+ "check": "rivet sync && rivet-check && tsgo"
13
13
  },
14
14
  "dependencies": {
15
15
  "@rivetjs/reactivity": "experimental",
@@ -22,6 +22,7 @@
22
22
  "@rivetjs/devtools": "experimental",
23
23
  "@rivetjs/language-core": "experimental",
24
24
  "@rivetjs/typescript-plugin": "experimental",
25
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
25
26
  "typescript": "^5.9.3",
26
27
  "vite": "^8.3.2"
27
28
  }
@@ -1,14 +1,15 @@
1
- import { headTags, type DocumentShell } from "@rivetjs/router/server";
1
+ import { bodyAttributes, bodyTags, headTags, htmlAttributes, type DocumentShell } from "@rivetjs/router/server";
2
2
 
3
3
  /**
4
- * The HTML around every page. The route `head` (title, meta, links, scripts) is written in full; `input` carries the
5
- * stylesheets, the CSP nonce (server pages) and the client-managed marks of `@rivetjs/kit` head components.
4
+ * The HTML around every page. The route `head` (title, meta, links, scripts, `<html>` / `<body>` attributes, end-of-body
5
+ * scripts) is written in full; `input` carries the stylesheets, the CSP nonce (server pages) and the client-managed
6
+ * marks of `@rivetjs/kit` head components.
6
7
  */
7
8
  const document: DocumentShell = (input) => ({
8
9
  start:
9
- '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">' +
10
+ `<!doctype html><html${htmlAttributes(input.head, { lang: "en" })}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">` +
10
11
  headTags({ ...input.head, title: input.head?.title ?? "Rivet app" }, input) +
11
- "</head><body>",
12
- end: "</body></html>",
12
+ `</head><body${bodyAttributes(input.head)}>`,
13
+ end: bodyTags(input.head, input) + "</body></html>",
13
14
  });
14
15
  export default document;
@@ -32,5 +32,5 @@ export default function Post({ data }: PageProps<"blog/[slug]">) {
32
32
  - Static mode (`rivetRouter({ mode: "static" })`): `load(…)` runs at build time; dynamic `[param]` pages list their params in `entries(…)`.
33
33
  - Server-only helpers go in `*.server.ts` files (browser code cannot import them: RV3008).
34
34
 
35
- 3. Link to it: plain `<a href="/blog/hello">` (client navigation), or typed `<Link to="/blog/hello">` from `@rivetjs/router/link`; programmatic `navigate()` from `@rivetjs/router/client`.
35
+ 3. Link to it: plain `<a href="/blog/hello">` (client navigation when the page has the client router; document navigation otherwise), or typed `<Link to="/blog/hello">` from `@rivetjs/router/link`; programmatic `navigate()` from `@rivetjs/router/client`.
36
36
  4. Run `npm run check` (`rivet sync` regenerates the typed routes in `.rivet/`, `rivet-check` reports `RV` codes; RV3101 = a link matches no route). Fix every diagnostic (`explain_diagnostic(code)`), then `npm run build`.
@@ -1,26 +1,21 @@
1
1
  ---
2
2
  name: rivet-fix-resumability-error
3
- description: 'Diagnose and fix Rivet resumability errors - RV2xxx compiler diagnostics, RV1016 handler errors, "is not serializable" / "Cannot resume" build or prerender errors, browser globals during server rendering, state that resets after the first click. Use when a Rivet build, check or page fails with such an error.'
3
+ description: "Diagnose and fix Rivet resumability errors - RV2xxx compiler diagnostics, RV1016 handler errors, RV2107 route data warnings, browser globals during server rendering, a component that renders in the browser instead of resuming. Use when a Rivet build, check or page fails with such an error."
4
4
  ---
5
5
 
6
6
  <!-- Generated by libs/mcp/ai/sync.ts from libs/mcp/ai/skills/fix-resumability-error.md in the Rivet repository: edit the source there and run the script. In an app created from a starter this file is yours to edit. -->
7
7
 
8
8
  # Fix a resumability error
9
9
 
10
- A Rivet page is rendered on the server and resumed in the browser: handlers, bindings and client effects run there with the values they capture, which the server serialized into the HTML. Errors come from code that cannot cross that line.
10
+ A Rivet page is rendered on the server and resumed in the browser: the first time a component is used, the browser runs its code once over the server HTML, starting from the signal values the server wrote. Errors come from code that renders differently on the two sides or from state/data that cannot be written into the page. Keep signal/store values as plain JSON data; the richer resume-format encoding (including registered classes) applies to route data.
11
11
 
12
12
  1. Get the exact diagnostic: `npm run check` (`file:line:col - error RVnnnn`), the Vite overlay (`[RVnnnn] file:line:col`), or `check_code(source, filename)`. For the fix: `explain_diagnostic("RVnnnn")`; `check_code` also returns the quick-fixed source when one applies.
13
13
  2. Common codes:
14
14
  - RV2001 browser global (`window`, `document`, `localStorage`) read during server rendering → move it into a handler, `clientEffect(() => …)`, `clientEvent(window, "resize", …)` or `clientOnly(() => import("…"))`.
15
- - RV2003 captured class instance (`new Intl.NumberFormat`, an SDK client) → capture plain data; create the object inside the handler or `clientEffect`, or build it with `reconstruct(() => new X(…))` from `@rivetjs/runtime/dom` (the browser runs the factory on first read). A data class declared in the component module (plain fields) resumes as is.
16
- - RV2102 captured instance of a module class that cannot resume as data (`#private` field, a decorator, a field arrow using `super`, a base that is not a data class of the module, `window` / `document` / `WebSocket` read while constructing) → create it inside the handler, build it with `reconstruct(() => new X(…))`, keep only plain fields, or add `[serialize]()` + `static [revive](data)` (from `@rivetjs/runtime/resume-format`; package classes: `registerClass(Cls, { id, serialize, revive })` in a module imported by the server code and the resume entry). At render, `RV2102: X has [serialize] but no static [revive]` → add the `static [revive]`.
17
15
  - RV1016 handler expression reads state when it binds (`onClick={a() ? f : g}`) → wrap: `onClick={(e) => (a() ? f(e) : g(e))}`.
18
- - RV2002 ref callback must assign a local → `ref={(el) => (local = el)}` or `const el = ref()` + `ref={el}`.
19
- - RV2006 JSX inside a handler / effect / computed → render JSX in the component, switch it with a signal.
20
- - RV2007 `this` in resumable code → `event.currentTarget`.
21
- - RV2009 two module references share a browser name (two imports of different modules under one name) → rename one import (`import { fmt as fmtDate } from …`).
22
- - RV2101 (warning, type-aware) a captured value's type does not serialize → as RV2003.
23
- 3. Build / prerender error `Cannot resume <file>#<Component>$<hash>: function is not serializable` (dev server: ``capture `<name>` function is not serializable``; dev build: `capture #<i> …`): a captured function Rivet did not compile (created at runtime by a library such as `memoize(fn)`, a `.bind(…)` written inside an object literal (keep `const inc = add.bind(null, 2)` in a local constant and store `inc`; `.bind(ctx)` of a function using `this` stays RV2007), a method, or held by an object built outside a component: at module level such as `const table = { format: formatEuro }`, or by your own hook / helper such as `function useFormat() { return { format: formatEuro }; }`). Wrap it in an inline arrow, or build the object inside the component (module-level and imported functions used by name there resume by reference). A library object holding functions (a chart, editor, socket client from a package): build it with `reconstruct(() => createChart(…))` from `@rivetjs/runtime/dom` — no value in the page, the browser runs the factory on first read (module level: one server instance per process, never read request state in it). A class instance at render (`X instance is not serializable`): your class → `reconstruct(() => new X(…))` or `[serialize]()` + `static [revive](data)`; a package class → `registerClass(X, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`. `NODE_ENV=development vite build` names the value path. Rules: `get_doc("docs:/best-practices/serializable-captures/")`.
24
- 4. State resets on the first change after resume: a child destructures its props (`function C({ value })`) and is re-created → read `props.value`. Copies of `props.children` in a local are not tracked → render `{props.children}` directly.
16
+ - RV2103 render is not deterministic (`Date.now()`, `Math.random()`, `crypto.randomUUID()` read while rendering) → keep the value in keyed state (`signal(Date.now())`) or read it in a handler / `clientEffect`.
17
+ - RV2104 (warning) keyed state created in a loop → create one signal / store outside the loop and keep the per-item values in it.
18
+ 3. RV2107 (runtime warning) route data not encodable: a loader / action / error value holds a function, symbol, promise or an instance of an unregistered class; the page renders on the server but cannot resume. Return plain data; for a class use `registerClass(X, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`, imported on server and browser.
19
+ 4. Dev warning that a component's markup differs and it renders in the browser: its render reads something that differs between server and browser (a browser global, a time, a random value, data the browser loads differently) → keep it in a signal, or move it into a `clientEffect`.
25
20
  5. `.ts` module importing `@rivetjs/runtime/dom` (bundles the DOM runtime into SSR) → move that code into a `.tsx` file.
26
- 6. Re-run `npm run check` and `npm run build` (prerender exercises serialization); both must be clean.
21
+ 6. Re-run `npm run check` and `npm run build`; both must be clean.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rivet-port-react-component
3
- description: "Port a React (or Solid / Next.js) component or page to Rivet idioms - signals instead of useState, clientEffect instead of useEffect, For with a key, live props, class, serializable handlers. Use when the user pastes React code or asks to convert / migrate a component to Rivet."
3
+ description: "Port a React (or Solid / Next.js) component or page to Rivet idioms - signals instead of useState, clientEffect instead of useEffect, For with a key, live props, class, ordinary function handlers. Use when the user pastes React code or asks to convert / migrate a component to Rivet."
4
4
  ---
5
5
 
6
6
  <!-- Generated by libs/mcp/ai/sync.ts from libs/mcp/ai/skills/port-react-component.md in the Rivet repository: edit the source there and run the script. In an app created from a starter this file is yours to edit. -->
@@ -30,7 +30,7 @@ Map each construct (full table: `get_doc("docs:/start/coming-from-other-framewor
30
30
  Then fix what resuming requires:
31
31
 
32
32
  1. Move browser reads (`window`, `document`, `localStorage`, `matchMedia`) out of the component body into handlers or `clientEffect` (RV2001).
33
- 2. Handlers: inline arrows or local consts. Values they capture must serialize: create class instances (`Intl.*`, SDK clients) inside the handler or build them with `reconstruct(() => new X(…))` (`import { reconstruct } from "@rivetjs/runtime/dom"`; RV2003); wrap library callbacks: `onPress={() => track("save")}`.
33
+ 2. Handlers: plain functions (inline arrows, local consts, imported functions); nothing they use needs to serialize. Keep values that differ between server and browser (`Date.now()`, random ids) in a signal or a handler.
34
34
  3. Render props and item renderers stay functions (`{(item) => …}`).
35
35
  4. Third-party React UI → `@rivetjs/ui` / `@rivetjs/cdk` equivalents: `list_components`, `get_component(name)`; React Aria-style names carry over (`onPress`, `isOpen={signal}`).
36
36
  5. `check_code` the result, fix every RV code (`explain_diagnostic`), then `npm run check`.
@@ -40,13 +40,13 @@ Avoid nested interactive targets: an entire clickable card with nested buttons,
40
40
 
41
41
  **Collection page:** heading + main create action → labeled search/filter controls → result count and collection → pagination. Show bulk actions only when selection exists; `ActionBar` or `Toolbar` may fit. Align filters and table columns to the same content edges. Provide initial-empty and filtered-empty states separately.
42
42
 
43
- **Settings page:** heading/description → grouped `Fieldset` sections → controls/help/errors → save feedback/actions. Group related rows within a section with consistent spacing or separators. Do not wrap every preference in a card. Choose either explicit save or immediate updates for a region and make that behavior visible.
43
+ **Settings page:** heading/description → grouped `Fieldset` sections → controls/help/errors → save feedback/actions. Group related rows within a section with consistent spacing or separators; `CellSwitch`, `CellSelect` and `CellSlider` are full-row settings cells. Do not wrap every preference in a card. Choose either explicit save or immediate updates for a region and make that behavior visible.
44
44
 
45
45
  **Detail/edit page:** summary and status → related content sections → contextual actions. Use a drawer for short contextual edits, a page for long/deep edits, and inline editing for small well-bounded values. Destructive actions belong near the affected object with clear labels and proportionate confirmation.
46
46
 
47
- **Dashboard:** lead with metrics that support a decision, then the trend or collection needed to act. `Kpi`, `TrendChip`, and the chart exports may fit. Every chart needs a readable explanation or data alternative; meaningful units, time range and empty states matter more than decorative chart variety.
47
+ **Dashboard:** lead with metrics that support a decision, then the trend or collection needed to act. `Kpi`, `KpiGroup`, `Widget`, `NumberValue`, `TrendChip`, and the chart exports may fit; `ItemCard` / `ItemCardGroup` for settings rows, `Timeline` for activity history. Every chart needs a readable explanation or data alternative; meaningful units, time range and empty states matter more than decorative chart variety.
48
48
 
49
- **Command picker:** compose a labeled search field and a supported action collection, often using `Autocomplete` + `Menu` for commands. Confirm documented filtering, virtual focus and activation behavior for the installed version. A command menu and a value-selecting list box are different tasks. A dialog may provide a focused enclosing surface. Add pending, no-match, error and clear-query behavior; do not invent a `Command` export or manual arrow-key loop when suitable primitives exist.
49
+ **Command picker:** `Command` (`@rivetjs/ui/command`) is the modal palette: a search field filtering grouped commands with virtual focus, Enter to run, Escape to clear then close; put extra search words in each item's `textValue`. For an inline picker, compose a labeled search field with `Autocomplete` + `Menu`. A command menu and a value-selecting list box are different tasks. Add pending, no-match, error and clear-query behavior; do not write a manual arrow-key loop. For actions on a right click, use `ContextMenu`.
50
50
 
51
51
  ## Reuse at the pattern boundary
52
52
 
@@ -57,7 +57,7 @@ const failures = ["light", "dark"].flatMap((mode) => auditContrast(theme, mode a
57
57
  - Preserve live props: read `props.name`, and use `splitProps`/`mergeProps` from `@rivetjs/runtime/props` for forwarding. Destructuring live props can read them once.
58
58
  - Dynamic collections use `For` from `@rivetjs/runtime/dom` with a stable `by` key; its item and index arguments are accessors. Component render-prop state values can also be accessors; inspect the example/type before reading them.
59
59
  - For renderable props use `Content` from `@rivetjs/runtime/jsx-runtime`. For deferred closed overlays/providers on resumable pages use render-function children `{() => …}` instead of already-created JSX. `AppLayout` named slots also take render functions, not JSX values.
60
- - Keep handlers as inline arrows or supported local functions with serializable captures. Browser reads (`window`, `document`, storage) belong in handlers or `clientEffect`, not server render. Import browser runtime helpers from `.tsx` modules for resume rewriting.
60
+ - Handlers are plain functions; keep render output the same on server and browser (no `Date.now()` / random values in render). Browser reads (`window`, `document`, storage) belong in handlers or `clientEffect`, not server render. Import browser runtime helpers from `.tsx` modules for resume rewriting.
61
61
  - Icons: `import { Icon } from "@rivetjs/kit/icon"` and icon data from `lucide`, then `<Icon icon={Search} />`. Keep decorative icons hidden and icon-only controls named. Do not invent SVG paths or use text glyphs as icons.
62
62
  - CDK is headless: documented `rv-*` classes and `data-*` state support custom CSS, while behavior-critical styles remain inline. Use its overlay primitives, not a hand-built/native-dialog replacement with a different focus/dismissal model.
63
63
 
@@ -32,5 +32,5 @@ export default function Post({ data }: PageProps<"blog/[slug]">) {
32
32
  - Static mode (`rivetRouter({ mode: "static" })`): `load(…)` runs at build time; dynamic `[param]` pages list their params in `entries(…)`.
33
33
  - Server-only helpers go in `*.server.ts` files (browser code cannot import them: RV3008).
34
34
 
35
- 3. Link to it: plain `<a href="/blog/hello">` (client navigation), or typed `<Link to="/blog/hello">` from `@rivetjs/router/link`; programmatic `navigate()` from `@rivetjs/router/client`.
35
+ 3. Link to it: plain `<a href="/blog/hello">` (client navigation when the page has the client router; document navigation otherwise), or typed `<Link to="/blog/hello">` from `@rivetjs/router/link`; programmatic `navigate()` from `@rivetjs/router/client`.
36
36
  4. Run `npm run check` (`rivet sync` regenerates the typed routes in `.rivet/`, `rivet-check` reports `RV` codes; RV3101 = a link matches no route). Fix every diagnostic (`explain_diagnostic(code)`), then `npm run build`.
@@ -1,26 +1,21 @@
1
1
  ---
2
2
  name: rivet-fix-resumability-error
3
- description: 'Diagnose and fix Rivet resumability errors - RV2xxx compiler diagnostics, RV1016 handler errors, "is not serializable" / "Cannot resume" build or prerender errors, browser globals during server rendering, state that resets after the first click. Use when a Rivet build, check or page fails with such an error.'
3
+ description: "Diagnose and fix Rivet resumability errors - RV2xxx compiler diagnostics, RV1016 handler errors, RV2107 route data warnings, browser globals during server rendering, a component that renders in the browser instead of resuming. Use when a Rivet build, check or page fails with such an error."
4
4
  ---
5
5
 
6
6
  <!-- Generated by libs/mcp/ai/sync.ts from libs/mcp/ai/skills/fix-resumability-error.md in the Rivet repository: edit the source there and run the script. In an app created from a starter this file is yours to edit. -->
7
7
 
8
8
  # Fix a resumability error
9
9
 
10
- A Rivet page is rendered on the server and resumed in the browser: handlers, bindings and client effects run there with the values they capture, which the server serialized into the HTML. Errors come from code that cannot cross that line.
10
+ A Rivet page is rendered on the server and resumed in the browser: the first time a component is used, the browser runs its code once over the server HTML, starting from the signal values the server wrote. Errors come from code that renders differently on the two sides or from state/data that cannot be written into the page. Keep signal/store values as plain JSON data; the richer resume-format encoding (including registered classes) applies to route data.
11
11
 
12
12
  1. Get the exact diagnostic: `npm run check` (`file:line:col - error RVnnnn`), the Vite overlay (`[RVnnnn] file:line:col`), or `check_code(source, filename)`. For the fix: `explain_diagnostic("RVnnnn")`; `check_code` also returns the quick-fixed source when one applies.
13
13
  2. Common codes:
14
14
  - RV2001 browser global (`window`, `document`, `localStorage`) read during server rendering → move it into a handler, `clientEffect(() => …)`, `clientEvent(window, "resize", …)` or `clientOnly(() => import("…"))`.
15
- - RV2003 captured class instance (`new Intl.NumberFormat`, an SDK client) → capture plain data; create the object inside the handler or `clientEffect`, or build it with `reconstruct(() => new X(…))` from `@rivetjs/runtime/dom` (the browser runs the factory on first read). A data class declared in the component module (plain fields) resumes as is.
16
- - RV2102 captured instance of a module class that cannot resume as data (`#private` field, a decorator, a field arrow using `super`, a base that is not a data class of the module, `window` / `document` / `WebSocket` read while constructing) → create it inside the handler, build it with `reconstruct(() => new X(…))`, keep only plain fields, or add `[serialize]()` + `static [revive](data)` (from `@rivetjs/runtime/resume-format`; package classes: `registerClass(Cls, { id, serialize, revive })` in a module imported by the server code and the resume entry). At render, `RV2102: X has [serialize] but no static [revive]` → add the `static [revive]`.
17
15
  - RV1016 handler expression reads state when it binds (`onClick={a() ? f : g}`) → wrap: `onClick={(e) => (a() ? f(e) : g(e))}`.
18
- - RV2002 ref callback must assign a local → `ref={(el) => (local = el)}` or `const el = ref()` + `ref={el}`.
19
- - RV2006 JSX inside a handler / effect / computed → render JSX in the component, switch it with a signal.
20
- - RV2007 `this` in resumable code → `event.currentTarget`.
21
- - RV2009 two module references share a browser name (two imports of different modules under one name) → rename one import (`import { fmt as fmtDate } from …`).
22
- - RV2101 (warning, type-aware) a captured value's type does not serialize → as RV2003.
23
- 3. Build / prerender error `Cannot resume <file>#<Component>$<hash>: function is not serializable` (dev server: ``capture `<name>` function is not serializable``; dev build: `capture #<i> …`): a captured function Rivet did not compile (created at runtime by a library such as `memoize(fn)`, a `.bind(…)` written inside an object literal (keep `const inc = add.bind(null, 2)` in a local constant and store `inc`; `.bind(ctx)` of a function using `this` stays RV2007), a method, or held by an object built outside a component: at module level such as `const table = { format: formatEuro }`, or by your own hook / helper such as `function useFormat() { return { format: formatEuro }; }`). Wrap it in an inline arrow, or build the object inside the component (module-level and imported functions used by name there resume by reference). A library object holding functions (a chart, editor, socket client from a package): build it with `reconstruct(() => createChart(…))` from `@rivetjs/runtime/dom` — no value in the page, the browser runs the factory on first read (module level: one server instance per process, never read request state in it). A class instance at render (`X instance is not serializable`): your class → `reconstruct(() => new X(…))` or `[serialize]()` + `static [revive](data)`; a package class → `registerClass(X, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`. `NODE_ENV=development vite build` names the value path. Rules: `get_doc("docs:/best-practices/serializable-captures/")`.
24
- 4. State resets on the first change after resume: a child destructures its props (`function C({ value })`) and is re-created → read `props.value`. Copies of `props.children` in a local are not tracked → render `{props.children}` directly.
16
+ - RV2103 render is not deterministic (`Date.now()`, `Math.random()`, `crypto.randomUUID()` read while rendering) → keep the value in keyed state (`signal(Date.now())`) or read it in a handler / `clientEffect`.
17
+ - RV2104 (warning) keyed state created in a loop → create one signal / store outside the loop and keep the per-item values in it.
18
+ 3. RV2107 (runtime warning) route data not encodable: a loader / action / error value holds a function, symbol, promise or an instance of an unregistered class; the page renders on the server but cannot resume. Return plain data; for a class use `registerClass(X, { id, serialize, revive })` from `@rivetjs/runtime/resume-format`, imported on server and browser.
19
+ 4. Dev warning that a component's markup differs and it renders in the browser: its render reads something that differs between server and browser (a browser global, a time, a random value, data the browser loads differently) → keep it in a signal, or move it into a `clientEffect`.
25
20
  5. `.ts` module importing `@rivetjs/runtime/dom` (bundles the DOM runtime into SSR) → move that code into a `.tsx` file.
26
- 6. Re-run `npm run check` and `npm run build` (prerender exercises serialization); both must be clean.
21
+ 6. Re-run `npm run check` and `npm run build`; both must be clean.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rivet-port-react-component
3
- description: "Port a React (or Solid / Next.js) component or page to Rivet idioms - signals instead of useState, clientEffect instead of useEffect, For with a key, live props, class, serializable handlers. Use when the user pastes React code or asks to convert / migrate a component to Rivet."
3
+ description: "Port a React (or Solid / Next.js) component or page to Rivet idioms - signals instead of useState, clientEffect instead of useEffect, For with a key, live props, class, ordinary function handlers. Use when the user pastes React code or asks to convert / migrate a component to Rivet."
4
4
  ---
5
5
 
6
6
  <!-- Generated by libs/mcp/ai/sync.ts from libs/mcp/ai/skills/port-react-component.md in the Rivet repository: edit the source there and run the script. In an app created from a starter this file is yours to edit. -->
@@ -30,7 +30,7 @@ Map each construct (full table: `get_doc("docs:/start/coming-from-other-framewor
30
30
  Then fix what resuming requires:
31
31
 
32
32
  1. Move browser reads (`window`, `document`, `localStorage`, `matchMedia`) out of the component body into handlers or `clientEffect` (RV2001).
33
- 2. Handlers: inline arrows or local consts. Values they capture must serialize: create class instances (`Intl.*`, SDK clients) inside the handler or build them with `reconstruct(() => new X(…))` (`import { reconstruct } from "@rivetjs/runtime/dom"`; RV2003); wrap library callbacks: `onPress={() => track("save")}`.
33
+ 2. Handlers: plain functions (inline arrows, local consts, imported functions); nothing they use needs to serialize. Keep values that differ between server and browser (`Date.now()`, random ids) in a signal or a handler.
34
34
  3. Render props and item renderers stay functions (`{(item) => …}`).
35
35
  4. Third-party React UI → `@rivetjs/ui` / `@rivetjs/cdk` equivalents: `list_components`, `get_component(name)`; React Aria-style names carry over (`onPress`, `isOpen={signal}`).
36
36
  5. `check_code` the result, fix every RV code (`explain_diagnostic`), then `npm run check`.