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.
- package/README.md +8 -1
- 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 +1 -1
- package/templates/starter-minimal/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-minimal/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-minimal/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-minimal/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-minimal/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-minimal/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-minimal/.cursor/rules/rivet.mdc +6 -6
- package/templates/starter-minimal/.github/copilot-instructions.md +6 -6
- package/templates/starter-minimal/AGENTS.md +56 -78
- package/templates/starter-minimal/CLAUDE.md +56 -78
- package/templates/starter-minimal/README.md +3 -3
- 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 +1 -1
- package/templates/starter-server/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-server/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-server/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-server/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-server/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-server/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-server/.cursor/rules/rivet.mdc +6 -6
- package/templates/starter-server/.github/copilot-instructions.md +6 -6
- package/templates/starter-server/AGENTS.md +56 -78
- package/templates/starter-server/CLAUDE.md +56 -78
- package/templates/starter-server/README.md +5 -4
- 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-static/.agents/skills/rivet-add-route/SKILL.md +1 -1
- package/templates/starter-static/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-static/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-static/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-static/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-static/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-static/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-static/.cursor/rules/rivet.mdc +6 -6
- package/templates/starter-static/.github/copilot-instructions.md +6 -6
- package/templates/starter-static/AGENTS.md +56 -78
- package/templates/starter-static/CLAUDE.md +56 -78
- package/templates/starter-static/README.md +6 -5
- 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 +1 -1
- package/templates/starter-ui/.agents/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-ui/.agents/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-ui/.claude/skills/rivet-fix-resumability-error/SKILL.md +7 -12
- package/templates/starter-ui/.claude/skills/rivet-port-react-component/SKILL.md +2 -2
- 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 +1 -1
- package/templates/starter-ui/.cursor/rules/rivet-fix-resumability-error.mdc +7 -12
- package/templates/starter-ui/.cursor/rules/rivet-port-react-component.mdc +3 -3
- package/templates/starter-ui/.cursor/rules/rivet.mdc +6 -6
- package/templates/starter-ui/.github/copilot-instructions.md +6 -6
- package/templates/starter-ui/AGENTS.md +56 -78
- package/templates/starter-ui/CLAUDE.md +56 -78
- package/templates/starter-ui/README.md +5 -4
- 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
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ cd my-app
|
|
|
8
8
|
npm run dev
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
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
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\`;
|
|
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
|
|
112
|
-
export declare function installDependencies(dir: string, pm: PackageManager, { stdio }?: {
|
|
113
|
-
stdio?: "inherit" | "pipe"
|
|
114
|
-
|
|
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
|
|
163
|
-
export function installDependencies(dir, pm, { stdio = "inherit" } = {}) {
|
|
164
|
-
const
|
|
165
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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(
|
|
141
|
+
return fail(diagnosticsIO, `${folder} is a file`);
|
|
90
142
|
if (!isEmptyDir(dir) && !values.force) {
|
|
91
143
|
if (!ask)
|
|
92
|
-
return fail(
|
|
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(
|
|
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.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
164
|
-
|
|
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.
|
|
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 {
|
|
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) =>
|
|
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) {
|
|
@@ -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:
|
|
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:
|
|
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
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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,
|
|
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
|
|
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`.
|
package/templates/starter-minimal/.agents/skills/rivet-ui-design/references/component-composition.md
CHANGED
|
@@ -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:**
|
|
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
|
|