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.
Files changed (115) hide show
  1. package/README.md +10 -3
  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 +10 -10
  14. package/templates/starter-minimal/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  15. package/templates/starter-minimal/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
  16. package/templates/starter-minimal/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
  17. package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  18. package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  19. package/templates/starter-minimal/.claude/skills/rivet-add-route/SKILL.md +10 -10
  20. package/templates/starter-minimal/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  21. package/templates/starter-minimal/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
  22. package/templates/starter-minimal/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
  23. package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  24. package/templates/starter-minimal/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  25. package/templates/starter-minimal/.cursor/rules/rivet-add-route.mdc +10 -10
  26. package/templates/starter-minimal/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  27. package/templates/starter-minimal/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
  28. package/templates/starter-minimal/.cursor/rules/rivet-port-react-component.mdc +3 -3
  29. package/templates/starter-minimal/.cursor/rules/rivet.mdc +7 -7
  30. package/templates/starter-minimal/.github/copilot-instructions.md +7 -7
  31. package/templates/starter-minimal/AGENTS.md +69 -87
  32. package/templates/starter-minimal/CLAUDE.md +69 -87
  33. package/templates/starter-minimal/README.md +4 -4
  34. package/templates/starter-minimal/package.json +2 -1
  35. package/templates/starter-minimal/src/document.ts +7 -6
  36. package/templates/starter-server/.agents/skills/rivet-add-route/SKILL.md +10 -10
  37. package/templates/starter-server/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  38. package/templates/starter-server/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
  39. package/templates/starter-server/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
  40. package/templates/starter-server/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  41. package/templates/starter-server/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  42. package/templates/starter-server/.claude/skills/rivet-add-route/SKILL.md +10 -10
  43. package/templates/starter-server/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  44. package/templates/starter-server/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
  45. package/templates/starter-server/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
  46. package/templates/starter-server/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  47. package/templates/starter-server/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  48. package/templates/starter-server/.cursor/rules/rivet-add-route.mdc +10 -10
  49. package/templates/starter-server/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  50. package/templates/starter-server/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
  51. package/templates/starter-server/.cursor/rules/rivet-port-react-component.mdc +3 -3
  52. package/templates/starter-server/.cursor/rules/rivet.mdc +7 -7
  53. package/templates/starter-server/.github/copilot-instructions.md +7 -7
  54. package/templates/starter-server/AGENTS.md +69 -87
  55. package/templates/starter-server/CLAUDE.md +69 -87
  56. package/templates/starter-server/README.md +8 -7
  57. package/templates/starter-server/package.json +2 -1
  58. package/templates/starter-server/serve.js +2 -0
  59. package/templates/starter-server/src/document.ts +7 -6
  60. package/templates/starter-server/src/routes/index.tsx +5 -5
  61. package/templates/starter-server/vite.config.ts +1 -1
  62. package/templates/starter-static/.agents/skills/rivet-add-route/SKILL.md +10 -10
  63. package/templates/starter-static/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  64. package/templates/starter-static/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
  65. package/templates/starter-static/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
  66. package/templates/starter-static/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  67. package/templates/starter-static/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  68. package/templates/starter-static/.claude/skills/rivet-add-route/SKILL.md +10 -10
  69. package/templates/starter-static/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  70. package/templates/starter-static/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
  71. package/templates/starter-static/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
  72. package/templates/starter-static/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  73. package/templates/starter-static/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  74. package/templates/starter-static/.cursor/rules/rivet-add-route.mdc +10 -10
  75. package/templates/starter-static/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  76. package/templates/starter-static/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
  77. package/templates/starter-static/.cursor/rules/rivet-port-react-component.mdc +3 -3
  78. package/templates/starter-static/.cursor/rules/rivet.mdc +7 -7
  79. package/templates/starter-static/.github/copilot-instructions.md +7 -7
  80. package/templates/starter-static/AGENTS.md +69 -87
  81. package/templates/starter-static/CLAUDE.md +69 -87
  82. package/templates/starter-static/README.md +7 -6
  83. package/templates/starter-static/package.json +2 -1
  84. package/templates/starter-static/src/components/Counter.tsx +2 -6
  85. package/templates/starter-static/src/components/ReadingPlan.tsx +31 -0
  86. package/templates/starter-static/src/document.ts +7 -6
  87. package/templates/starter-static/src/routes/about.tsx +14 -1
  88. package/templates/starter-static/src/routes/index.tsx +2 -2
  89. package/templates/starter-ui/.agents/skills/rivet-add-route/SKILL.md +10 -10
  90. package/templates/starter-ui/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  91. package/templates/starter-ui/.agents/skills/rivet-form-in-drawer/SKILL.md +6 -5
  92. package/templates/starter-ui/.agents/skills/rivet-port-react-component/SKILL.md +3 -3
  93. package/templates/starter-ui/.agents/skills/rivet-ui-design/references/component-composition.md +3 -3
  94. package/templates/starter-ui/.agents/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  95. package/templates/starter-ui/.claude/skills/rivet-add-route/SKILL.md +10 -10
  96. package/templates/starter-ui/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
  97. package/templates/starter-ui/.claude/skills/rivet-form-in-drawer/SKILL.md +6 -5
  98. package/templates/starter-ui/.claude/skills/rivet-port-react-component/SKILL.md +3 -3
  99. package/templates/starter-ui/.claude/skills/rivet-ui-design/references/component-composition.md +3 -3
  100. package/templates/starter-ui/.claude/skills/rivet-ui-design/references/rivet-implementation.md +1 -1
  101. package/templates/starter-ui/.cursor/rules/rivet-add-route.mdc +10 -10
  102. package/templates/starter-ui/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
  103. package/templates/starter-ui/.cursor/rules/rivet-form-in-drawer.mdc +6 -5
  104. package/templates/starter-ui/.cursor/rules/rivet-port-react-component.mdc +3 -3
  105. package/templates/starter-ui/.cursor/rules/rivet.mdc +7 -7
  106. package/templates/starter-ui/.github/copilot-instructions.md +7 -7
  107. package/templates/starter-ui/AGENTS.md +69 -87
  108. package/templates/starter-ui/CLAUDE.md +69 -87
  109. package/templates/starter-ui/README.md +6 -5
  110. package/templates/starter-ui/package.json +2 -1
  111. package/templates/starter-ui/src/components/LikeButton.tsx +1 -1
  112. package/templates/starter-ui/src/components/ReadingPreferences.tsx +18 -0
  113. package/templates/starter-ui/src/document.ts +7 -6
  114. package/templates/starter-ui/src/routes/index.tsx +3 -1
  115. package/templates/starter-ui/vite.config.ts +3 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-rivet
2
2
 
3
- Create a [Rivet](https://github.com/rivetjs/rivet) app.
3
+ Create a [Rivet](https://github.com/N1XUS/rivetjs) app.
4
4
 
5
5
  ```sh
6
6
  npm create rivet@latest my-app
@@ -8,7 +8,7 @@ cd my-app
8
8
  npm run dev
9
9
  ```
10
10
 
11
- It asks for a project name, a template, whether to install dependencies and whether to create a git repository. Other package managers: `pnpm create rivet`, `yarn create rivet`, `bun create rivet`.
11
+ On a terminal it asks for any missing project folder and template, a deploy target, and whether to install dependencies and create a git repository. With `--yes` or non-interactive input, defaults are `rivet-app`, `static`, no deploy adapter, install on and git on. Other package managers: `pnpm create rivet`, `yarn create rivet`, `bun create rivet`.
12
12
 
13
13
  ## Templates
14
14
 
@@ -34,13 +34,20 @@ npm create rivet@latest my-app -- --template server --yes --no-git
34
34
  | `-n`, `--name <name>` | `package.json` name (default: the folder name) |
35
35
  | `-y`, `--yes` | ask nothing, take the defaults |
36
36
  | `--no-install` | skip installing dependencies |
37
+ | `--rivet-version <spec>` | override the `@rivetjs/*` dependency spec (default: this release) |
38
+ | `-v`, `--version` | print the create-rivet version |
37
39
  | `--no-git` | skip `git init` |
38
40
  | `--pm <name>` | `npm`, `pnpm`, `yarn` or `bun` (default: the one running the command) |
39
41
  | `-f`, `--force` | write into a folder that is not empty |
42
+ | `--quiet` | errors only, no questions |
43
+ | `--json` | one JSON result, no questions |
44
+ | `--verbose` | error stacks and successful install output (also `DEBUG=rivet`) |
40
45
  | `-h`, `--help` | all options |
41
46
 
42
47
  With npm, options go after `--`.
43
48
 
49
+ Install and git steps show progress and duration. CI/pipes use plain periodic lines; `NO_COLOR` disables color and wins over `FORCE_COLOR`. Exit codes: 0 success, 1 failure (including install/git failure), 2 invalid arguments, 130 cancelled. The created folder remains available after a failed install. `installDependencies()` is asynchronous and returns `Promise<boolean>`.
50
+
44
51
  ## Nx monorepos
45
52
 
46
- The same templates as Nx projects, with cached `build` / `test` / `check` targets: [`@rivetjs/nx`](https://github.com/rivetjs/rivet/tree/main/libs/nx) (`npx create-nx-workspace@latest my-org --preset=@rivetjs/nx`, or `npx nx add @rivetjs/nx` in an existing workspace).
53
+ The same templates as Nx projects, with cached `build` / `test` / `check` targets: [`@rivetjs/nx`](https://github.com/N1XUS/rivetjs/tree/main/libs/nx) (`npx create-nx-workspace@latest my-org --preset=@rivetjs/nx`, or `npx nx add @rivetjs/nx` in an existing workspace).
package/deploy.d.ts CHANGED
@@ -18,7 +18,7 @@ export declare const DEPLOY_TARGETS: readonly [{
18
18
  }, {
19
19
  readonly id: "deno";
20
20
  readonly label: "Deno";
21
- readonly hint: "`deno run dist/server/deno.js` (server template)";
21
+ readonly hint: "`deno run --allow-net --allow-read --allow-env dist/server/deno.js` (server template)";
22
22
  }];
23
23
  export type DeployTarget = (typeof DEPLOY_TARGETS)[number]["id"];
24
24
  /** Wrangler major the cloudflare target was verified with (`wrangler dev --local`, 4.x). */
package/deploy.js CHANGED
@@ -15,7 +15,7 @@ export const DEPLOY_TARGETS = [
15
15
  { id: "netlify", label: "Netlify", hint: "netlify.toml + Netlify Functions (dist/netlify)" },
16
16
  { id: "cloudflare", label: "Cloudflare Workers", hint: "Workers + static assets, `wrangler deploy`" },
17
17
  { id: "bun", label: "Bun", hint: "`bun dist/server/bun.js` (server template)" },
18
- { id: "deno", label: "Deno", hint: "`deno run dist/server/deno.js` (server template)" },
18
+ { id: "deno", label: "Deno", hint: "`deno run --allow-net --allow-read --allow-env dist/server/deno.js` (server template)" },
19
19
  ];
20
20
  /** Wrangler major the cloudflare target was verified with (`wrangler dev --local`, 4.x). */
21
21
  export const WRANGLER_RANGE = "^4.0.0";
@@ -78,10 +78,10 @@ export function deployOverlay(target, pm = "npm") {
78
78
  const label = findDeployTarget(target).label;
79
79
  const how = {
80
80
  vercel: `\`${run("build")}\` also writes \`.vercel/output\` (Build Output API). Deploy it with the Vercel CLI: \`${run("deploy")}\` (\`vercel deploy --prebuilt\`).`,
81
- netlify: `\`netlify.toml\` makes Netlify run \`${run("build")}\` and publish \`dist/netlify\`; the server function goes to \`.netlify/v1\`. Connect the repository in Netlify, or deploy with the Netlify CLI after a build.`,
81
+ netlify: `\`netlify.toml\` makes Netlify run \`${run("build")}\` and publish \`dist/netlify\`; server-mode builds also put a function in \`.netlify/v1\`. Connect the repository in Netlify, or deploy with the Netlify CLI after a build.`,
82
82
  cloudflare: `\`${run("build")}\` writes \`dist/cloudflare\` (worker + static assets + \`wrangler.json\`). \`${run("preview")}\` runs it in the local Workers runtime, \`${run("deploy")}\` uploads it. Keep \`nodejs_compat\` off (the build sets the compatibility flags).`,
83
- bun: `\`${run("build")}\` writes \`dist/server/bun.js\`; \`${run("start")}\` serves the app with Bun (\`PORT\`, \`HOST\`). Bun must be installed.`,
84
- deno: `\`${run("build")}\` writes \`dist/server/deno.js\`; \`${run("start")}\` serves the app with Deno (\`PORT\`, \`HOST\`). Deno must be installed.`,
83
+ bun: `\`${run("build")}\` writes \`dist/server/bun.js\`; \`${run("start")}\` serves the app with Bun (\`PORT\`, \`HOST\`). Bun must be installed. This target removes the Node-only \`serve.js\`, \`Dockerfile\` and \`.dockerignore\`; the earlier Docker instructions do not apply.`,
84
+ deno: `\`${run("build")}\` writes \`dist/server/deno.js\`; \`${run("start")}\` serves the app with Deno (\`PORT\`, \`HOST\`). Deno must be installed. This target removes the Node-only \`serve.js\`, \`Dockerfile\` and \`.dockerignore\`; the earlier Docker instructions do not apply.`,
85
85
  };
86
86
  writeFileSync(readme, readFileSync(readme, "utf8").trimEnd() +
87
87
  `\n\n## Deploy: ${label}\n\n- \`vite.config.ts\` sets \`rivetRouter({ adapter: "${target}" })\`.\n- ${how[target]}\n`);
package/index.d.ts CHANGED
@@ -108,10 +108,12 @@ export declare function createProject(options: CreateOptions): {
108
108
  name: string;
109
109
  files: string[];
110
110
  };
111
- /** `<pm> install` in `dir` (inherits the terminal). Returns whether it succeeded. */
112
- export declare function installDependencies(dir: string, pm: PackageManager, { stdio }?: {
113
- stdio?: "inherit" | "pipe" | undefined;
114
- }): boolean;
111
+ /** `<pm> install` in `dir`; asynchronous so the CLI can report progress. */
112
+ export declare function installDependencies(dir: string, pm: PackageManager, { stdio, onOutput, env, }?: {
113
+ stdio?: "inherit" | "pipe";
114
+ onOutput?: (text: string) => void;
115
+ env?: NodeJS.ProcessEnv;
116
+ }): Promise<boolean>;
115
117
  /** `git init` + first commit, unless git is missing or `dir` is already inside a work tree. */
116
118
  export declare function initGit(dir: string): "created" | "committed" | "inside-repo" | "no-git" | "failed";
117
119
  /** `dir` exists and is a file (not a folder). */
package/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Templates: the published package ships them in `templates/` (copied from the repository's `templates/` at
6
6
  // package time by `bundle.ts`); inside the repository they are read from `templates/` directly.
7
- import { spawnSync } from "node:child_process";
7
+ import { spawn, spawnSync } from "node:child_process";
8
8
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
9
9
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
@@ -159,10 +159,18 @@ export function createProject(options) {
159
159
  writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
160
160
  return { dir, name, files: [...files].sort() };
161
161
  }
162
- /** `<pm> install` in `dir` (inherits the terminal). Returns whether it succeeded. */
163
- export function installDependencies(dir, pm, { stdio = "inherit" } = {}) {
164
- const run = spawnSync(pm, pm === "yarn" ? [] : ["install"], { cwd: dir, stdio, shell: process.platform === "win32" });
165
- return run.status === 0;
162
+ /** `<pm> install` in `dir`; asynchronous so the CLI can report progress. */
163
+ export async function installDependencies(dir, pm, { stdio = "inherit", onOutput, env = process.env, } = {}) {
164
+ const child = spawn(pm, pm === "yarn" ? [] : ["install"], { cwd: dir, stdio, env, shell: process.platform === "win32" });
165
+ child.stdout?.on("data", (chunk) => onOutput?.(String(chunk)));
166
+ child.stderr?.on("data", (chunk) => onOutput?.(String(chunk)));
167
+ return new Promise((done) => {
168
+ child.once("error", (error) => {
169
+ onOutput?.(error.message);
170
+ done(false);
171
+ });
172
+ child.once("close", (code) => done(code === 0));
173
+ });
166
174
  }
167
175
  /** `git init` + first commit, unless git is missing or `dir` is already inside a work tree. */
168
176
  export function initGit(dir) {
package/main.d.ts CHANGED
@@ -4,6 +4,9 @@ export interface MainIO extends PromptIO {
4
4
  error: NodeJS.WritableStream;
5
5
  cwd: string;
6
6
  env: Record<string, string | undefined>;
7
+ json?: boolean;
8
+ quiet?: boolean;
9
+ verbose?: boolean;
7
10
  }
8
- /** Runs the command; resolves with the exit code (0 ok, 1 error, 130 cancelled). */
11
+ /** Runs the command; resolves with the exit code (0 ok, 1 failure, 2 usage, 130 cancelled). */
9
12
  export declare function main(argv: string[], io: MainIO): Promise<number>;
package/main.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // The create-rivet command: arguments (node:util parseArgs), questions (./prompt.ts), then ./index.ts does the work.
2
2
  // npm create rivet@latest [dir] [-- --template <id>] [--yes] [--no-install] [--no-git] …
3
+ import { paint as color, progress, suggestion, summary, debugEnabled } from "./output.js";
3
4
  import { basename, relative, resolve } from "node:path";
4
- import { parseArgs, styleText } from "node:util";
5
+ import { Writable } from "node:stream";
6
+ import { parseArgs } from "node:util";
5
7
  import { commands, createProject, detectPackageManager, findTemplate, initGit, installDependencies, isEmptyDir, isFile, ownVersion, PACKAGE_MANAGERS, TEMPLATES, toPackageName, validatePackageName, } from "./index.js";
6
8
  import { DEPLOY_TARGETS, deployOverlay, deployProblem, findDeployTarget } from "./deploy.js";
7
9
  import { closePrompts, confirm, PromptCancelled, select, text } from "./prompt.js";
@@ -20,15 +22,27 @@ Options:
20
22
  --git / --no-git git init + first commit (default: yes)
21
23
  --pm <name> ${PACKAGE_MANAGERS.join(" | ")} (default: the one running create, else npm)
22
24
  --rivet-version <spec> @rivetjs/* dependency spec (default: this release)
25
+ --quiet errors only; accept defaults without prompts
26
+ --json one JSON result; accept defaults without prompts
27
+ --verbose include error stacks and install output (also DEBUG=rivet)
23
28
  -f, --force write into a folder that is not empty
24
29
  -h, --help this text
25
30
  -v, --version create-rivet version
26
31
 
32
+ Examples:
33
+ npm create rivet@latest my-app -- --template server --yes
34
+ npm create rivet@latest my-app -- --no-install --no-git --json
35
+
36
+ Exit status: 0 success, 1 failure, 2 usage error, 130 cancelled.
37
+
27
38
  Templates:
28
39
  ${TEMPLATES.map((t) => ` ${t.id.padEnd(9)} ${t.label}: ${t.hint}`).join("\n")}
29
40
  `;
30
- /** Runs the command; resolves with the exit code (0 ok, 1 error, 130 cancelled). */
41
+ /** Runs the command; resolves with the exit code (0 ok, 1 failure, 2 usage, 130 cancelled). */
31
42
  export async function main(argv, io) {
43
+ const resultOutput = io.output;
44
+ io = { ...io, json: argv.includes("--json"), quiet: argv.includes("--quiet"), verbose: argv.includes("--verbose") || debugEnabled(io.env) };
45
+ const started = performance.now();
32
46
  let args;
33
47
  try {
34
48
  args = parseArgs({
@@ -36,6 +50,9 @@ export async function main(argv, io) {
36
50
  allowPositionals: true,
37
51
  allowNegative: true,
38
52
  options: {
53
+ quiet: { type: "boolean" },
54
+ json: { type: "boolean" },
55
+ verbose: { type: "boolean" },
39
56
  template: { type: "string", short: "t" },
40
57
  deploy: { type: "string", short: "d" },
41
58
  name: { type: "string", short: "n" },
@@ -51,27 +68,62 @@ export async function main(argv, io) {
51
68
  });
52
69
  }
53
70
  catch (error) {
54
- io.error.write(`${error.message}\n\n${HELP}`);
55
- return 1;
71
+ const message = error.message;
72
+ const unknown = /Unknown option '([^']+)'/.exec(message)?.[1];
73
+ return fail(io, message +
74
+ (unknown
75
+ ? suggestion(unknown, [
76
+ "--template",
77
+ "--deploy",
78
+ "--name",
79
+ "--yes",
80
+ "--install",
81
+ "--no-install",
82
+ "--git",
83
+ "--no-git",
84
+ "--pm",
85
+ "--rivet-version",
86
+ "--force",
87
+ "--help",
88
+ "--version",
89
+ "--quiet",
90
+ "--json",
91
+ "--verbose",
92
+ ])
93
+ : ""), 2);
56
94
  }
57
95
  const { values, positionals } = args;
58
- const paint = (format, value) => styleText(format, value, { stream: io.output });
96
+ io = { ...io, json: !!values.json, quiet: !!values.quiet, verbose: !!values.verbose || debugEnabled(io.env) };
97
+ const paint = (format, value) => color(format, value, io.output, io.env);
59
98
  if (values.help)
60
99
  return (io.output.write(HELP), 0);
61
100
  if (values.version)
62
101
  return (io.output.write(ownVersion() + "\n"), 0);
63
102
  if (positionals.length > 1)
64
- return fail(io, `Expected one folder, got ${positionals.join(" ")}`);
103
+ return fail(io, `Expected one folder, got ${positionals.join(" ")}`, 2);
65
104
  if (values.template && !findTemplate(values.template))
66
- return fail(io, `Unknown template "${values.template}" (${TEMPLATES.map((t) => t.id).join(", ")})`);
105
+ return fail(io, `Unknown template "${values.template}" (${TEMPLATES.map((t) => t.id).join(", ")})`, 2);
67
106
  if (values.deploy && !findDeployTarget(values.deploy))
68
- return fail(io, `Unknown deploy target "${values.deploy}" (${DEPLOY_TARGETS.map((t) => t.id).join(", ")})`);
107
+ return fail(io, `Unknown deploy target "${values.deploy}" (${DEPLOY_TARGETS.map((t) => t.id).join(", ")})`, 2);
69
108
  if (values.pm && !PACKAGE_MANAGERS.includes(values.pm))
70
- return fail(io, `Unknown package manager "${values.pm}" (${PACKAGE_MANAGERS.join(", ")})`);
109
+ return fail(io, `Unknown package manager "${values.pm}" (${PACKAGE_MANAGERS.join(", ")})`, 2);
71
110
  if (values.name && validatePackageName(values.name))
72
- return fail(io, `--name: ${validatePackageName(values.name)}`);
111
+ return fail(io, `--name: ${validatePackageName(values.name)}`, 2);
73
112
  // Questions only on a terminal and without --yes; otherwise every missing answer takes its default.
74
- const ask = !values.yes && Boolean(io.input.isTTY);
113
+ const ask = !values.yes && !io.quiet && !io.json && !io.env.CI && Boolean(io.input.isTTY);
114
+ // Keep one result on stdout in JSON mode; status text has a private sink.
115
+ const diagnosticsIO = io;
116
+ if (io.quiet || io.json)
117
+ io = {
118
+ ...io,
119
+ output: new Writable({
120
+ write(_chunk, _encoding, done) {
121
+ done();
122
+ },
123
+ }),
124
+ };
125
+ let warnings = 0, errors = 0;
126
+ const diagnostics = [];
75
127
  try {
76
128
  io.output.write(`\n${paint(["bold", "blue"], "create-rivet")} ${paint("dim", "v" + ownVersion())}\n\n`);
77
129
  let name = values.name;
@@ -86,10 +138,10 @@ export async function main(argv, io) {
86
138
  const dir = resolve(io.cwd, folder);
87
139
  name ??= toPackageName(basename(dir));
88
140
  if (isFile(dir))
89
- return fail(io, `${folder} is a file`);
141
+ return fail(diagnosticsIO, `${folder} is a file`);
90
142
  if (!isEmptyDir(dir) && !values.force) {
91
143
  if (!ask)
92
- return fail(io, `${folder} is not empty (use --force to write into it)`);
144
+ return fail(diagnosticsIO, `${folder} is not empty (use --force to write into it)`);
93
145
  if (!(await confirm(io, `${folder} is not empty. Write into it anyway (files with the same names are replaced)?`, false)))
94
146
  return cancelled(io);
95
147
  }
@@ -100,7 +152,7 @@ export async function main(argv, io) {
100
152
  : "static");
101
153
  let deploy = values.deploy;
102
154
  if (deploy && deployProblem(deploy, template))
103
- return fail(io, deployProblem(deploy, template));
155
+ return fail(diagnosticsIO, deployProblem(deploy, template), 2);
104
156
  if (!deploy && ask) {
105
157
  const choices = [
106
158
  { value: "none", label: "None", hint: template === "server" ? "Node server (serve.js, Dockerfile)" : "static files (dist/client)" },
@@ -125,21 +177,67 @@ export async function main(argv, io) {
125
177
  const run = commands(pm);
126
178
  let installed = false;
127
179
  if (install) {
128
- io.output.write(`\n${paint("dim", "$ " + run.install)}\n`);
129
- installed = installDependencies(dir, pm);
130
- if (!installed)
131
- io.error.write(paint("yellow", `\n${run.install} failed; run it yourself in ${folder}.\n`));
180
+ const task = progress(`create-rivet: installing with ${pm}`, { stream: io.output, env: io.env, quiet: io.quiet || io.json });
181
+ let output = "";
182
+ try {
183
+ installed = await installDependencies(dir, pm, {
184
+ stdio: "pipe",
185
+ env: { ...process.env, ...io.env, NO_COLOR: "1", FORCE_COLOR: "0" },
186
+ onOutput(text) {
187
+ output = (output + text).slice(-65536);
188
+ },
189
+ });
190
+ }
191
+ finally {
192
+ task.stop(installed ? "Dependencies installed" : "Dependency install failed");
193
+ }
194
+ if (!installed) {
195
+ errors++;
196
+ diagnostics.push({
197
+ severity: "error",
198
+ message: `${run.install} failed in ${dir}`,
199
+ fix: `Run ${run.install} in ${folder} after addressing the package manager error.`,
200
+ details: output,
201
+ });
202
+ if (!io.json)
203
+ io.error.write(`error: ${run.install} failed in ${dir}.\n fix: Run ${run.install} in ${folder} and address the package manager error below.\n${output}\n`);
204
+ }
205
+ if (installed && io.verbose && !io.json)
206
+ io.error.write(output);
132
207
  }
133
208
  if (git) {
134
- const result = initGit(dir);
209
+ const task = progress("create-rivet: initializing git", { stream: io.output, env: io.env, quiet: io.quiet || io.json });
210
+ let result;
211
+ try {
212
+ result = initGit(dir);
213
+ }
214
+ finally {
215
+ task.stop("Git step finished");
216
+ }
217
+ if (result === "no-git" || result === "created")
218
+ warnings++;
219
+ if (result === "failed")
220
+ errors++;
221
+ if (result === "failed" || result === "created" || result === "no-git")
222
+ diagnostics.push({
223
+ severity: result === "failed" ? "error" : "warning",
224
+ message: `Git step in ${dir}: ${result}`,
225
+ fix: result === "no-git"
226
+ ? "Install Git, then run git init in the created folder."
227
+ : "Run git status and check git user.name / user.email in the created folder.",
228
+ });
135
229
  if (result === "committed")
136
230
  io.output.write(`${paint("green", "Git")} repository created with a first commit\n`);
137
231
  else if (result === "created")
138
- io.output.write(`${paint("green", "Git")} repository created (files staged; no commit: set git user.name / user.email)\n`);
232
+ io.output.write(`${paint("yellow", "warning: Git")} repository created (files staged; commit not created). Run git status and check git user.name / user.email.\n`);
139
233
  else if (result === "inside-repo")
140
234
  io.output.write(`${paint("dim", "Git: already inside a repository, skipped")}\n`);
235
+ else if (result === "failed") {
236
+ if (!io.json)
237
+ io.error.write(`error: Git initialization failed in ${dir}.\n fix: Run git init and git add in that folder to inspect the failure.\n`);
238
+ }
141
239
  else
142
- io.output.write(`${paint("dim", "Git: not available, skipped")}\n`);
240
+ io.output.write(`${paint("yellow", "warning: Git is not available, skipped; install Git to initialize later.")}\n`);
143
241
  }
144
242
  const cd = relative(io.cwd, dir) || ".";
145
243
  io.output.write(`\nNext:\n`);
@@ -148,20 +246,38 @@ export async function main(argv, io) {
148
246
  if (!installed)
149
247
  io.output.write(` ${run.install}\n`);
150
248
  io.output.write(` ${run.run("dev")}\n\n`);
151
- return 0;
249
+ const durationMs = Math.round(performance.now() - started);
250
+ if (io.json)
251
+ resultOutput.write(JSON.stringify({
252
+ directory: dir,
253
+ template,
254
+ installed,
255
+ errors,
256
+ diagnostics,
257
+ warnings,
258
+ durationMs,
259
+ ...(errors ? { fix: diagnostics.find((diagnostic) => diagnostic.severity === "error").fix } : {}),
260
+ }) + "\n");
261
+ else if (!io.quiet)
262
+ io.output.write(`create-rivet: ${summary(errors, warnings)} in ${(durationMs / 1000).toFixed(1)} s\n`);
263
+ return errors ? 1 : 0;
152
264
  }
153
265
  catch (error) {
154
266
  if (error instanceof PromptCancelled)
155
267
  return cancelled(io);
156
- return fail(io, error.message);
268
+ return fail(diagnosticsIO, error.message, 1, error);
157
269
  }
158
270
  finally {
159
271
  closePrompts(io);
160
272
  }
161
273
  }
162
- function fail(io, message) {
163
- io.error.write(styleText("red", "Error: ", { stream: io.error }) + message + "\n");
164
- return 1;
274
+ function fail(io, message, exitCode = 1, error) {
275
+ const fix = exitCode === 2 ? "Run create-rivet --help and correct the argument." : "Check the target directory and permissions, then retry.";
276
+ if (io.json)
277
+ io.output.write(JSON.stringify({ error: message, fix, exitCode, ...(io.verbose && error instanceof Error ? { stack: error.stack } : {}) }) + "\n");
278
+ else
279
+ io.error.write(color("red", "Error: ", io.error, io.env) + message + `\n fix: ${fix}\n` + (io.verbose && error instanceof Error ? error.stack + "\n" : ""));
280
+ return exitCode;
165
281
  }
166
282
  function cancelled(io) {
167
283
  io.error.write("Cancelled.\n");
package/output.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { styleText } from "node:util";
2
+ export interface TerminalStream {
3
+ isTTY?: boolean;
4
+ columns?: number;
5
+ write(text: string): unknown;
6
+ }
7
+ export type TerminalEnv = Record<string, string | undefined>;
8
+ /** CI stays plain even when a wrapper sets FORCE_COLOR. NO_COLOR always wins. */
9
+ export declare function colorEnabled(stream: Pick<TerminalStream, "isTTY">, env?: TerminalEnv): boolean;
10
+ export declare function paint(format: Parameters<typeof styleText>[0], text: string, stream?: TerminalStream, env?: TerminalEnv): string;
11
+ export declare function debugEnabled(env?: TerminalEnv): boolean;
12
+ /** Conservative edit-distance suggestion; never guesses an unrelated command. */
13
+ export declare function suggestion(value: string, choices: readonly string[]): string;
14
+ export declare function summary(errors: number, warnings: number): string;
15
+ /** One updating line on a terminal; periodic plain lines in pipes/CI. Always stop in a finally block. */
16
+ export declare function progress(label: string, { stream, env, quiet, interval, }?: {
17
+ stream?: TerminalStream;
18
+ env?: TerminalEnv;
19
+ quiet?: boolean;
20
+ interval?: number;
21
+ }): {
22
+ update(value: string): void;
23
+ stop(message?: string): void;
24
+ };
package/output.js ADDED
@@ -0,0 +1,66 @@
1
+ // Standalone create-rivet presentation: no dependency on the framework/compiler packages.
2
+ import { styleText } from "node:util";
3
+ /** CI stays plain even when a wrapper sets FORCE_COLOR. NO_COLOR always wins. */
4
+ export function colorEnabled(stream, env = process.env) {
5
+ if (env.NO_COLOR !== undefined || (env.CI !== undefined && env.CI !== "false" && env.CI !== "0") || env.FORCE_COLOR === "0")
6
+ return false;
7
+ return env.FORCE_COLOR !== undefined || !!stream.isTTY;
8
+ }
9
+ export function paint(format, text, stream = process.stderr, env = process.env) {
10
+ return colorEnabled(stream, env) ? styleText(format, text, { validateStream: false }) : text;
11
+ }
12
+ export function debugEnabled(env = process.env) {
13
+ return (env.DEBUG ?? "").split(/[ ,]+/).some((part) => part === "rivet" || part === "rivet:*" || part === "*");
14
+ }
15
+ /** Conservative edit-distance suggestion; never guesses an unrelated command. */
16
+ export function suggestion(value, choices) {
17
+ const distance = (a, b) => {
18
+ let row = Array.from({ length: b.length + 1 }, (_, i) => i);
19
+ for (let i = 1; i <= a.length; i++) {
20
+ const next = [i];
21
+ for (let j = 1; j <= b.length; j++)
22
+ next[j] = Math.min(next[j - 1] + 1, row[j] + 1, row[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
23
+ row = next;
24
+ }
25
+ return row[b.length];
26
+ };
27
+ const best = choices.map((choice) => ({ choice, score: distance(value, choice) })).sort((a, b) => a.score - b.score)[0];
28
+ return best && best.score <= Math.min(3, Math.max(1, Math.floor(value.length / 3))) ? ` Did you mean ${best.choice}?` : "";
29
+ }
30
+ export function summary(errors, warnings) {
31
+ return `${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`;
32
+ }
33
+ /** One updating line on a terminal; periodic plain lines in pipes/CI. Always stop in a finally block. */
34
+ export function progress(label, { stream = process.stderr, env = process.env, quiet = false, interval = 5000, } = {}) {
35
+ const start = performance.now();
36
+ const tty = !!stream.isTTY && !env.CI && env.NO_COLOR === undefined;
37
+ let detail = "", closed = false;
38
+ const elapsed = () => `${((performance.now() - start) / 1000).toFixed(1)} s`;
39
+ const write = () => {
40
+ if (quiet || closed)
41
+ return;
42
+ const line = `${label}${detail ? ": " + detail : ""} (${elapsed()})`.replace(/[\r\n\x1b]/g, " ");
43
+ stream.write(tty ? `\r\x1b[2K${line.slice(0, Math.max(10, (stream.columns ?? 80) - 1))}` : line + "\n");
44
+ };
45
+ write();
46
+ const timer = quiet ? undefined : setInterval(write, tty ? 100 : interval);
47
+ timer?.unref();
48
+ return {
49
+ update(value) {
50
+ detail = value;
51
+ },
52
+ stop(message) {
53
+ if (closed)
54
+ return;
55
+ closed = true;
56
+ if (timer)
57
+ clearInterval(timer);
58
+ if (!quiet) {
59
+ if (tty)
60
+ stream.write("\r\x1b[2K");
61
+ if (message)
62
+ stream.write(`${message} in ${elapsed()}\n`);
63
+ }
64
+ },
65
+ };
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rivet",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Create a Rivet app: npm create rivet@latest",
@@ -52,6 +52,8 @@
52
52
  "index.js",
53
53
  "main.d.ts",
54
54
  "main.js",
55
+ "output.d.ts",
56
+ "output.js",
55
57
  "prompt.d.ts",
56
58
  "prompt.js",
57
59
  "templates"
package/prompt.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface PromptIO {
4
4
  setRawMode?: (mode: boolean) => unknown;
5
5
  };
6
6
  output: NodeJS.WritableStream;
7
+ env?: Record<string, string | undefined>;
7
8
  }
8
9
  /** Thrown when the user cancels (Ctrl+C, Ctrl+D / end of input). */
9
10
  export declare class PromptCancelled extends Error {
package/prompt.js CHANGED
@@ -2,14 +2,14 @@
2
2
  // TTY, a numbered answer otherwise). Every prompt takes its streams, so tests drive them with PassThrough.
3
3
  import { emitKeypressEvents } from "node:readline";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { styleText } from "node:util";
5
+ import { paint } from "./output.js";
6
6
  /** Thrown when the user cancels (Ctrl+C, Ctrl+D / end of input). */
7
7
  export class PromptCancelled extends Error {
8
8
  constructor() {
9
9
  super("cancelled");
10
10
  }
11
11
  }
12
- const color = (format, text, io) => styleText(format, text, { stream: io.output });
12
+ const color = (format, text, io) => paint(format, text, io.output, io.env);
13
13
  const readers = new WeakMap();
14
14
  /** One line reader per input stream (a reader per question would drop lines buffered after the first). */
15
15
  function reader(io) {
@@ -1,23 +1,23 @@
1
1
  ---
2
2
  name: rivet-add-route
3
- description: "Add a page or endpoint to a Rivet app with file routes, server data (serverLoad), actions, head and layouts. Use when the user asks for a new page, URL, route, endpoint or loader in a Rivet project."
3
+ description: "Add a page or endpoint to a Rivet app with file routes, server data (load(…)), actions, head and layouts. Use when the user asks for a new page, URL, route, endpoint or loader in a Rivet project."
4
4
  ---
5
5
 
6
6
  <!-- Generated by libs/mcp/ai/sync.ts from libs/mcp/ai/skills/add-route.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
  # Add a route to a Rivet app
9
9
 
10
- 1. Pick the file under `src/routes/` (URL = path): `about.tsx` → `/about`, `blog/[slug].tsx` → `/blog/:slug`, `docs/[[lang]]/x.tsx` (optional param), `files/[...path].tsx` (rest), `(group)/p.tsx` → `/p` (shares the group's `_layout.tsx`), `api/time.ts` → endpoint (`export const onGet = () => Response.json(…)`). `_layout.tsx` wraps its folder; `_404.tsx` / `_error.tsx` handle misses / errors. Look up details with `get_doc("docs:/guides/routing/file-routes/")`.
11
- 2. Write the page. Server code lives in `server…` exports (stripped from the browser build):
10
+ 1. Pick the file under `src/routes/` (URL = path): `about.tsx` → `/about`, `blog/[slug].tsx` → `/blog/:slug`, `docs/[[lang]]/x.tsx` (optional param), `files/[...path].tsx` (rest), `(group)/p.tsx` → `/p` (shares the group's `_layout.tsx`), `api/time.ts` → endpoint (`export const GET = onGet(() => Response.json(…))`, `onGet` from `@rivetjs/router`). `_layout.tsx` wraps its folder; `_404.tsx` / `_error.tsx` handle misses / errors. Look up details with `get_doc("docs:/guides/routing/file-routes/")`.
11
+ 2. Write the page. Server code lives in exports wrapped in a `@rivetjs/router` call — `load(…)`, `actions(…)`, `guard(…)`, `onGet(…)` … (stripped from the browser build):
12
12
 
13
13
  ```tsx
14
- import { notFound, type PageProps, type RequestEvent } from "@rivetjs/router";
14
+ import { notFound, type PageProps, type RequestEvent, load } from "@rivetjs/router";
15
15
 
16
- export const serverLoad = async ({ params }: RequestEvent<"blog/[slug]">) => {
16
+ export const loader = load(async ({ params }: RequestEvent<"blog/[slug]">) => {
17
17
  const post = await findPost(params.slug); // server-only: database, files, secrets
18
18
  if (!post) throw notFound();
19
19
  return { post };
20
- };
20
+ });
21
21
 
22
22
  export const head = { title: "Post" };
23
23
 
@@ -27,10 +27,10 @@ export default function Post({ data }: PageProps<"blog/[slug]">) {
27
27
  ```
28
28
 
29
29
  - Page props `data`, `params`, `url` are plain values; in `_layout.tsx` (`LayoutProps`) they are accessors: `data()`, `params()`, `url()`, plus `children`.
30
- - Mutations: `export const serverActions = { name: async ({ request }) => … }` + `<form method="post" action="?/name">`; with validation use `@rivetjs/forms` (`action(def, fn)` from `@rivetjs/forms/server`).
31
- - Access checks: `serverGuard` (throw / return `redirect()`, `deny()`, `notFound()`).
32
- - Static mode (`rivetRouter({ mode: "static" })`): `serverLoad` runs at build time; dynamic `[param]` pages list their params in `serverEntries`.
30
+ - Mutations: `export const forms = actions({ name: async ({ request }) => … })` + `<form method="post" action="?/name">`; with validation use `@rivetjs/forms` (`action(def, fn)` from `@rivetjs/forms/server`).
31
+ - Access checks: `guard(…)` (throw / return `redirect()`, `deny()`, `notFound()`).
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.