create-rivet 0.1.0-alpha.0 → 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.
- package/README.md +10 -3
- package/deploy.d.ts +1 -1
- package/deploy.js +4 -4
- package/index.d.ts +6 -4
- package/index.js +13 -5
- package/main.d.ts +4 -1
- package/main.js +142 -26
- package/output.d.ts +24 -0
- package/output.js +66 -0
- package/package.json +3 -1
- package/prompt.d.ts +1 -0
- package/prompt.js +2 -2
- package/templates/starter-minimal/.agents/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-minimal/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-minimal/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-minimal/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-minimal/.claude/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-minimal/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-minimal/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-minimal/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-minimal/.cursor/rules/rivet-add-route.mdc +10 -10
- package/templates/starter-minimal/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-minimal/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
- package/templates/starter-minimal/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-minimal/.cursor/rules/rivet.mdc +7 -7
- package/templates/starter-minimal/.github/copilot-instructions.md +7 -7
- package/templates/starter-minimal/AGENTS.md +69 -87
- package/templates/starter-minimal/CLAUDE.md +69 -87
- package/templates/starter-minimal/README.md +4 -4
- package/templates/starter-minimal/package.json +2 -1
- package/templates/starter-minimal/src/document.ts +7 -6
- package/templates/starter-server/.agents/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-server/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-server/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-server/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-server/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-server/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-server/.claude/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-server/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-server/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-server/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-server/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-server/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-server/.cursor/rules/rivet-add-route.mdc +10 -10
- package/templates/starter-server/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-server/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
- package/templates/starter-server/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-server/.cursor/rules/rivet.mdc +7 -7
- package/templates/starter-server/.github/copilot-instructions.md +7 -7
- package/templates/starter-server/AGENTS.md +69 -87
- package/templates/starter-server/CLAUDE.md +69 -87
- package/templates/starter-server/README.md +8 -7
- package/templates/starter-server/package.json +2 -1
- package/templates/starter-server/serve.js +2 -0
- package/templates/starter-server/src/document.ts +7 -6
- package/templates/starter-server/src/routes/index.tsx +5 -5
- package/templates/starter-server/vite.config.ts +1 -1
- package/templates/starter-static/.agents/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-static/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-static/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-static/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-static/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-static/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-static/.claude/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-static/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-static/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-static/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-static/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-static/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-static/.cursor/rules/rivet-add-route.mdc +10 -10
- package/templates/starter-static/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-static/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
- package/templates/starter-static/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-static/.cursor/rules/rivet.mdc +7 -7
- package/templates/starter-static/.github/copilot-instructions.md +7 -7
- package/templates/starter-static/AGENTS.md +69 -87
- package/templates/starter-static/CLAUDE.md +69 -87
- package/templates/starter-static/README.md +7 -6
- package/templates/starter-static/package.json +2 -1
- package/templates/starter-static/src/components/Counter.tsx +2 -6
- package/templates/starter-static/src/components/ReadingPlan.tsx +31 -0
- package/templates/starter-static/src/document.ts +7 -6
- package/templates/starter-static/src/routes/about.tsx +14 -1
- package/templates/starter-static/src/routes/index.tsx +2 -2
- package/templates/starter-ui/.agents/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-ui/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-ui/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-ui/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-ui/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-ui/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-ui/.claude/skills/rivet-add-route/SKILL.md +10 -10
- package/templates/starter-ui/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-ui/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
- package/templates/starter-ui/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
- package/templates/starter-ui/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
- package/templates/starter-ui/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
- package/templates/starter-ui/.cursor/rules/rivet-add-route.mdc +10 -10
- package/templates/starter-ui/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-ui/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
- package/templates/starter-ui/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-ui/.cursor/rules/rivet.mdc +7 -7
- package/templates/starter-ui/.github/copilot-instructions.md +7 -7
- package/templates/starter-ui/AGENTS.md +69 -87
- package/templates/starter-ui/CLAUDE.md +69 -87
- package/templates/starter-ui/README.md +6 -5
- package/templates/starter-ui/package.json +2 -1
- package/templates/starter-ui/src/components/LikeButton.tsx +1 -1
- package/templates/starter-ui/src/components/ReadingPreferences.tsx +18 -0
- package/templates/starter-ui/src/document.ts +7 -6
- package/templates/starter-ui/src/routes/index.tsx +3 -1
- 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
|
|
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
|
|
16
|
-
- Component
|
|
17
|
-
-
|
|
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
|
|
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:(…)" })`.
|
|
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:
|
|
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.
|
|
@@ -106,7 +105,7 @@ import { For, Show, Switch, Match } from "@rivetjs/runtime/dom";
|
|
|
106
105
|
|
|
107
106
|
- `For`: `by` key is required; `item` and `index` are **accessors** (`todo()`, `index()`). Use for lists that change. `.map()` is fine for fixed data (loader data, constants).
|
|
108
107
|
- Conditionals: `cond() && <p />`, ternary, or `Show`. Always read the signal (`cond()`, not `cond`).
|
|
109
|
-
- Other helpers: `Async`, `Suspense`, `ErrorBoundary`, `Dynamic`, `Portal`, `Provider`, `lazyComponent`, `clientOnly`.
|
|
108
|
+
- Other helpers: `Async`, `resource` (async data as a signal: `const user = resource(() => id(), (id, info) => load(id, info.signal))`; keep it in one `const` in the component, not destructured; fetchers may be passed by name; read inside `Suspense` so the server waits), `Suspense`, `ErrorBoundary`, `Dynamic`, `Portal`, `Provider`, `lazyComponent`, `clientOnly`.
|
|
110
109
|
- A function child `{() => …}` re-runs when the signals its body reads change.
|
|
111
110
|
|
|
112
111
|
### Refs
|
|
@@ -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)} />; //
|
|
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.
|
|
136
|
+
- The provided value is read once: provide a signal for changing values.
|
|
138
137
|
|
|
139
138
|
## 3. Resumability rules
|
|
140
139
|
|
|
141
|
-
|
|
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
|
-
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
-
|
|
161
|
-
-
|
|
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
|
|
@@ -175,32 +163,32 @@ src/
|
|
|
175
163
|
docs/[[lang]]/x.tsx optional param
|
|
176
164
|
files/[...path].tsx rest param
|
|
177
165
|
(marketing)/p.tsx /p (group: no segment, shares a layout)
|
|
178
|
-
api/time.ts endpoint (exports onGet/onPost…, returns Response)
|
|
166
|
+
api/time.ts endpoint (exports onGet(…)/onPost(…)…, returns Response)
|
|
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
|
|
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
|
|
186
174
|
|
|
187
175
|
```tsx
|
|
188
|
-
import { redirect, notFound, type PageProps, type RequestEvent } from "@rivetjs/router";
|
|
176
|
+
import { redirect, notFound, type PageProps, type RequestEvent, load, actions } from "@rivetjs/router";
|
|
189
177
|
|
|
190
|
-
export const
|
|
178
|
+
export const loader = load(async ({ params }: RequestEvent<"blog/[slug]">) => {
|
|
191
179
|
const post = await findPost(params.slug);
|
|
192
180
|
if (!post) throw notFound();
|
|
193
181
|
return { post };
|
|
194
|
-
};
|
|
182
|
+
});
|
|
195
183
|
|
|
196
|
-
export const
|
|
184
|
+
export const forms = actions({
|
|
197
185
|
// <form method="post" action="?/comment">
|
|
198
186
|
comment: async ({ request }: RequestEvent<"blog/[slug]">) => {
|
|
199
187
|
const text = String((await request.formData()).get("text") ?? "");
|
|
200
188
|
await saveComment(text);
|
|
201
189
|
return redirect("/blog", 303);
|
|
202
190
|
},
|
|
203
|
-
};
|
|
191
|
+
});
|
|
204
192
|
|
|
205
193
|
export const head = { title: "Post" }; // or ({ data, params, url }) => Head
|
|
206
194
|
|
|
@@ -209,15 +197,15 @@ export default function Post({ data, params }: PageProps<"blog/[slug]">) {
|
|
|
209
197
|
}
|
|
210
198
|
```
|
|
211
199
|
|
|
212
|
-
- Server exports: `
|
|
213
|
-
- `event`: `request`, `url`, `params`, `locals`, `signal`, `cookies`, `setHeader`.
|
|
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.
|
|
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
|
|
217
205
|
// Avoid in a layout
|
|
218
206
|
<header>{data.user}</header>;
|
|
219
207
|
// Prefer
|
|
220
|
-
export default function Layout({ data, children }: LayoutProps<typeof
|
|
208
|
+
export default function Layout({ data, children }: LayoutProps<typeof loader>) {
|
|
221
209
|
return (
|
|
222
210
|
<>
|
|
223
211
|
<header>{data().user}</header>
|
|
@@ -229,12 +217,12 @@ export default function Layout({ data, children }: LayoutProps<typeof serverLoad
|
|
|
229
217
|
|
|
230
218
|
### Live pages
|
|
231
219
|
|
|
232
|
-
- `export const live = true` + `LivePageProps<typeof
|
|
220
|
+
- `export const live = true` + `LivePageProps<typeof loader>`: `data` becomes an accessor (`data().customers`). Same-URL revalidation (a `@rivetjs/forms` submit, `invalidate()`, search-param change) updates `data` in place: the page is not re-rendered, signals (an open drawer, typed input, focus) survive. Use it for forms in drawers / dialogs.
|
|
233
221
|
- Don't copy `data()` into a signal (it never updates). Render lists with `<For each={data().items} by={…}>`.
|
|
234
222
|
|
|
235
223
|
### Links and navigation
|
|
236
224
|
|
|
237
|
-
- Plain `<a href="/about">` works
|
|
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 serverLoad
|
|
|
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`). `
|
|
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`)
|
|
@@ -260,12 +248,13 @@ import { action, invalid } from "@rivetjs/forms/server";
|
|
|
260
248
|
import { actionResult } from "@rivetjs/router/client";
|
|
261
249
|
import { createForm, fieldId, formId, onInput, onBlur, onSubmit, defaultValue, isInvalid, describedBy } from "@rivetjs/forms";
|
|
262
250
|
import { FieldError, ErrorSummary } from "@rivetjs/forms/components";
|
|
251
|
+
import { actions } from "@rivetjs/router";
|
|
263
252
|
|
|
264
|
-
export const
|
|
253
|
+
export const forms = actions({
|
|
265
254
|
contact: action(contactForm, async (value) => {
|
|
266
255
|
/* typed, valid */
|
|
267
256
|
}),
|
|
268
|
-
};
|
|
257
|
+
});
|
|
269
258
|
|
|
270
259
|
export default function Contact() {
|
|
271
260
|
const form = createForm(contactForm, { result: actionResult() });
|
|
@@ -303,6 +292,7 @@ export default function Contact() {
|
|
|
303
292
|
- Compile-time: `t("key")` is replaced by the string for the locale being built; no runtime catalog. One client build per locale.
|
|
304
293
|
- `i18n.config.ts` (`locales`, `defaultLocale`, `catalogs: "locales/{locale}.json"`, `routing`); Vite: `plugins: [i18n(config), rivetRouter()]` — `i18n()` from `@rivetjs/i18n/vite`, **before** the router.
|
|
305
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.
|
|
306
296
|
|
|
307
297
|
```tsx
|
|
308
298
|
import { t } from "@rivetjs/i18n";
|
|
@@ -311,13 +301,14 @@ import { t } from "@rivetjs/i18n";
|
|
|
311
301
|
<p>{t("terms", { link: (c) => <a href="/terms">{c}</a> })}</p>
|
|
312
302
|
```
|
|
313
303
|
|
|
314
|
-
-
|
|
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.
|
|
315
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>`.
|
|
316
307
|
- Generated types: `src/rivet-i18n.d.ts` (do not edit). Locale URLs: `[[lang]]` route folder.
|
|
317
308
|
|
|
318
309
|
## 6. Styles
|
|
319
310
|
|
|
320
|
-
-
|
|
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`.
|
|
321
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)`.
|
|
322
313
|
- Dynamic values: `style={{ "--x": n(), marginTop: "1px" }}` or `style:color={c()}`; `data-*` attributes for state styling.
|
|
323
314
|
|
|
@@ -378,7 +369,7 @@ import { Select } from "@rivetjs/ui/select";
|
|
|
378
369
|
```
|
|
379
370
|
|
|
380
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.
|
|
381
|
-
- 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`
|
|
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.
|
|
382
373
|
|
|
383
374
|
## 9. Commands
|
|
384
375
|
|
|
@@ -388,53 +379,44 @@ import { Select } from "@rivetjs/ui/select";
|
|
|
388
379
|
| `npm run build` | all | `vite build && vite build --ssr` (+ `node prerender.js` for static / ui / minimal) |
|
|
389
380
|
| `npm start` | server | `node serve.js` (`PORT`, `HOST`) |
|
|
390
381
|
| `npm run preview` | static, ui | serve `dist/client` |
|
|
391
|
-
| `npm run check` | all | `rivet sync && rivet-check`
|
|
382
|
+
| `npm run check` | all | `rivet sync && rivet-check && tsgo` |
|
|
392
383
|
|
|
393
384
|
- Create: `npm create rivet@latest my-app -- --template static|server|ui|minimal --yes`.
|
|
394
|
-
- `rivet sync` (bin of `@rivetjs/router`): regenerates `.rivet/` (route types + `.rivet/editor.json`). Run on a fresh clone before `
|
|
395
|
-
- `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
|
|
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.
|
|
396
387
|
- Editor: `@rivetjs/typescript-plugin` in `tsconfig.json` shows the same diagnostics + quick fixes while typing.
|
|
397
388
|
- Measure in a build (`npm run build` + start/preview), not dev.
|
|
398
389
|
- Tests (optional setup): `rivetTest()` from `@rivetjs/testing/vite`; `render`, `fire`, `flush` from `@rivetjs/runtime/testing`; `renderToString` from `@rivetjs/runtime/testing/server`.
|
|
399
390
|
|
|
400
391
|
## 10. RV diagnostic codes
|
|
401
392
|
|
|
402
|
-
- Format `RVnnnn`. Compiler errors fail the build; compiler 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).
|
|
403
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.
|
|
404
395
|
- Ranges: 1xxx JSX / attributes, 2xxx resume, 3xxx imports / helpers / route modules, 4xxx i18n, 5xxx styles, 6xxx slots, 7xxx config, 9xxx internal.
|
|
405
396
|
|
|
406
|
-
| Code | Meaning
|
|
407
|
-
| --------------- |
|
|
408
|
-
| RV1000 | Parse error
|
|
409
|
-
| RV1005 | HTML nesting the parser would repair (`<div>` in `<p>`)
|
|
410
|
-
| RV1012 | Unknown native event (`onclick`, `onTransitionend`)
|
|
411
|
-
| RV1015 | Handler is a literal, not a function
|
|
412
|
-
| RV1016 | Handler expression reads state when it binds
|
|
413
|
-
| RV1101 | Unsafe / unsupported attribute or URL (`innerHTML`, `javascript:`)
|
|
414
|
-
| RV1102 | Invalid `preload` value
|
|
415
|
-
| RV2001 | Browser global read during server rendering
|
|
416
|
-
|
|
|
417
|
-
|
|
|
418
|
-
|
|
|
419
|
-
|
|
|
420
|
-
|
|
|
421
|
-
|
|
|
422
|
-
|
|
|
423
|
-
|
|
|
424
|
-
|
|
|
425
|
-
|
|
|
426
|
-
|
|
|
427
|
-
|
|
|
428
|
-
|
|
|
429
|
-
| RV3008 | Server-only code reached from browser code | keep in `serverLoad` / `*.server.ts` |
|
|
430
|
-
| RV3101 | (warning) Link matches no route | fix the path |
|
|
431
|
-
| RV4001–4004 | i18n key / param / argument / tag errors | see section 5 |
|
|
432
|
-
| RV5001–5003 | ``css`…` `` syntax / usage; invalid or unsafe class / style (style string, two class attrs) | style object, one `class` |
|
|
433
|
-
| RV6001–6004 | Slot errors (`<slot>`, `r:name`, `r:slot`) | see JSX reference, slots |
|
|
434
|
-
| RV7101 / RV7102 | `.rivet/editor.json` missing / stale | `rivet sync` |
|
|
435
|
-
| RV9000 / RV9001 | Unclassified / internal compiler error | report it |
|
|
436
|
-
|
|
437
|
-
- 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 |
|
|
438
420
|
|
|
439
421
|
## 11. Links
|
|
440
422
|
|
|
@@ -450,7 +432,7 @@ import { Select } from "@rivetjs/ui/select";
|
|
|
450
432
|
|
|
451
433
|
## Checklist before reporting done
|
|
452
434
|
|
|
453
|
-
- `npm run check` exits 0; `npm run build` passes
|
|
435
|
+
- `npm run check` exits 0; `npm run build` passes.
|
|
454
436
|
- No destructured props in components that receive changing values; lists that change use `For` with `by`.
|
|
455
437
|
- No browser globals outside handlers / `clientEffect` / `clientEvent` / `clientOnly`.
|
|
456
438
|
- Icons only via `<Icon icon={…} />` + lucide.
|