@rivetjs/router 0.0.0-stage → 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +20 -0
- package/adapters/files.js +155 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +180 -0
- package/adapters/node.js +986 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +95 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +35 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +97 -0
- package/build/codegen.js +1037 -0
- package/build/dev-errors.d.ts +23 -0
- package/build/dev-errors.js +247 -0
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +31 -0
- package/build/migrate.js +385 -0
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +55 -0
- package/build/routes.js +219 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +208 -0
- package/build/vite.js +861 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +101 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +91 -0
- package/client/index.d.ts +156 -0
- package/client/index.js +714 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +135 -0
- package/index.js +8 -0
- package/package.json +156 -4
- package/server/app.d.ts +175 -0
- package/server/app.js +335 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +240 -0
- package/server/handler.js +661 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +96 -0
- package/server/render.d.ts +96 -0
- package/server/render.js +492 -0
- package/server/route-cache.d.ts +245 -0
- package/server/route-cache.js +813 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
import type { DevSymbolInfo } from "@rivetjs/compiler/vite";
|
|
3
|
+
import type { RouteTable } from "./routes.js";
|
|
4
|
+
import type { AppHandler, ErrorInfo } from "../server/handler.js";
|
|
5
|
+
/** A resume symbol the compiler plugin compiled for dev SSR (`DevResume.describe`). */
|
|
6
|
+
type Describe = (id: string) => DevSymbolInfo | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Dev `onError`: source-map the stack, log it, send it to Vite's overlay (open pages, e.g. during
|
|
9
|
+
* client navigation) and remember it for the request's dev error page.
|
|
10
|
+
*/
|
|
11
|
+
export declare function devReport(server: ViteDevServer, table: () => RouteTable, routesPath: string, describe?: Describe): (error: unknown, _request: Request, info?: ErrorInfo) => void;
|
|
12
|
+
/** Browser errors forwarded by the dev entry (`import.meta.hot.send("rivet:client-error")`) open that page's overlay. */
|
|
13
|
+
export declare function listenClientErrors(server: ViteDevServer): void;
|
|
14
|
+
/**
|
|
15
|
+
* Wrap the dev app handler: a reported 5xx page gets the dev error page (no `_error` view) or the
|
|
16
|
+
* overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes. `reach`: the page's server
|
|
17
|
+
* reach reports (RV3011, `build/reach.ts`): an error opens the overlay over the rendered page.
|
|
18
|
+
*/
|
|
19
|
+
export declare function devHandler(handler: AppHandler, server: ViteDevServer, table: () => RouteTable, reach?: (url: URL) => Promise<readonly {
|
|
20
|
+
readonly severity: "error" | "warning";
|
|
21
|
+
readonly message: string;
|
|
22
|
+
}[]>): AppHandler;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// Dev server error surfaces (`vite serve` only; never part of a build): server failures reach Vite's
|
|
2
|
+
// error overlay and a dev error page with the source-mapped stack, unmatched URLs list the route
|
|
3
|
+
// table, and browser errors the dev entry forwards (`rivet:client-error`) open the overlay too.
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { compileRoutes } from "@rivetjs/runtime/router/server";
|
|
7
|
+
import { escapeAttribute, escapeText } from "@rivetjs/runtime/escape";
|
|
8
|
+
import { escapeJSON } from "@rivetjs/runtime/resume-format";
|
|
9
|
+
const failures = new AsyncLocalStorage();
|
|
10
|
+
/** JSON for an inline `<script>` (no `</script>` / `<!--`, no raw U+2028/U+2029), as resume state. */
|
|
11
|
+
const scriptJSON = (value) => escapeJSON(JSON.stringify(value));
|
|
12
|
+
const overlayOn = (server) => {
|
|
13
|
+
const hmr = server.config.server.hmr;
|
|
14
|
+
return !(hmr === false || (typeof hmr === "object" && hmr.overlay === false));
|
|
15
|
+
};
|
|
16
|
+
/** Diagnostic code (`RV1001`…) of an error that carries one, as an overlay message prefix. */
|
|
17
|
+
const codeOf = (error) => {
|
|
18
|
+
const code = error?.code;
|
|
19
|
+
return typeof code === "string" && /^RV\d{4}$/.test(code) ? `[${code}] ` : "";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Dev `onError`: source-map the stack, log it, send it to Vite's overlay (open pages, e.g. during
|
|
23
|
+
* client navigation) and remember it for the request's dev error page.
|
|
24
|
+
*/
|
|
25
|
+
export function devReport(server, table, routesPath, describe) {
|
|
26
|
+
return (error, _request, info) => {
|
|
27
|
+
if (error instanceof Error)
|
|
28
|
+
server.ssrFixStacktrace(error);
|
|
29
|
+
// Without info: a render failure reported by a custom renderer (app renders report `phase: "render"`).
|
|
30
|
+
const phase = info?.phase ?? "render";
|
|
31
|
+
const route = info?.route;
|
|
32
|
+
const file = route === undefined ? undefined : table().routes.find((entry) => entry.id === route)?.page;
|
|
33
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
34
|
+
const symbol = describe && resumeSymbol(text, describe);
|
|
35
|
+
const id = symbol?.file ?? (file ? join(routesPath, file) : undefined);
|
|
36
|
+
const note = symbol ? symbolNote(text, symbol) : describe ? missingNote(text) : "";
|
|
37
|
+
const message = codeOf(error) + text + note;
|
|
38
|
+
const stack = error instanceof Error ? (error.stack ?? "") : "";
|
|
39
|
+
server.config.logger.error(`[rivet] ${phase} failed${route === undefined ? "" : ` (route "${route}")`}: ${note ? message + "\n" : ""}${stack || message}`);
|
|
40
|
+
const failure = { message, stack, phase, route, id, ...(symbol ? { loc: { file: symbol.file, line: symbol.line, column: symbol.column } } : {}) };
|
|
41
|
+
failures.getStore()?.push(failure);
|
|
42
|
+
if (overlayOn(server))
|
|
43
|
+
server.ws.send({ type: "error", err: overlayError(failure) });
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const overlayError = (failure) => ({
|
|
47
|
+
message: `${failure.phase} failed${failure.route === undefined ? "" : ` in route "${failure.route}"`}: ${failure.message}`,
|
|
48
|
+
stack: failure.stack,
|
|
49
|
+
plugin: "rivet",
|
|
50
|
+
...(failure.id ? { id: failure.id } : {}),
|
|
51
|
+
...(failure.loc ? { loc: failure.loc } : {}),
|
|
52
|
+
});
|
|
53
|
+
// Symbol ids in resume recorder errors: `Cannot resume <id>: capture `<name>` …` (dev; `#<n>` without names), `… no chunk for <id>`.
|
|
54
|
+
const SYMBOL_ID = /(?:^|[\s(])((?:[\w.@-]+\/)*[\w.@-]+\.[cm]?[jt]sx?#[\w$]+)/;
|
|
55
|
+
/** The resume symbol a recorder error names, when the dev server compiled it. */
|
|
56
|
+
function resumeSymbol(message, describe) {
|
|
57
|
+
const id = SYMBOL_ID.exec(message)?.[1];
|
|
58
|
+
return id === undefined ? undefined : describe(id);
|
|
59
|
+
}
|
|
60
|
+
const KINDS = {
|
|
61
|
+
event: "event handler",
|
|
62
|
+
attr: "attribute binding",
|
|
63
|
+
bind: "text binding",
|
|
64
|
+
region: "child region",
|
|
65
|
+
model: "model:",
|
|
66
|
+
effect: "clientEffect",
|
|
67
|
+
clientEvent: "clientEvent",
|
|
68
|
+
computed: "computed",
|
|
69
|
+
equals: "equals comparator",
|
|
70
|
+
fn: "function",
|
|
71
|
+
slot: "slot",
|
|
72
|
+
spread: "spread",
|
|
73
|
+
};
|
|
74
|
+
/** `in <Component> (file:line:col), <kind>; capture `name` (file:line:col)` for an error naming a symbol. */
|
|
75
|
+
function symbolNote(message, symbol) {
|
|
76
|
+
const where = (line, column) => `${symbol.module}:${line}:${column}`;
|
|
77
|
+
let note = `\n in <${symbol.component === "default" ? "default export" : symbol.component}> (${where(symbol.line, symbol.column)}), ${KINDS[symbol.kind] ?? symbol.kind} ${symbol.id}`;
|
|
78
|
+
// `capture `name`` (dev manifest names, Function Bridge T19) or `capture #<n>` (no names).
|
|
79
|
+
const named = /capture `([^`]+)`/.exec(message)?.[1];
|
|
80
|
+
const index = named === undefined ? /capture #(\d+)/.exec(message)?.[1] : undefined;
|
|
81
|
+
const capture = named !== undefined ? symbol.captures.find((c) => c.name === named) : index === undefined ? undefined : symbol.captures[Number(index)];
|
|
82
|
+
if (capture)
|
|
83
|
+
note += `\n capture \`${capture.name}\` (${where(capture.line, capture.column)})`;
|
|
84
|
+
return note;
|
|
85
|
+
}
|
|
86
|
+
/** Dev note for a symbol the dev manifest has no chunk for (no SSR compile defined it). */
|
|
87
|
+
const missingNote = (message) => {
|
|
88
|
+
const id = /no chunk for (\S+)/.exec(message)?.[1];
|
|
89
|
+
return id === undefined
|
|
90
|
+
? ""
|
|
91
|
+
: `\n dev: no module the dev server compiled in resume mode defines ${id.replace(/[.,;]$/, "")} (is ${id.slice(0, id.indexOf("#"))} in resume.modules?)`;
|
|
92
|
+
};
|
|
93
|
+
/** Browser errors forwarded by the dev entry (`import.meta.hot.send("rivet:client-error")`) open that page's overlay. */
|
|
94
|
+
export function listenClientErrors(server) {
|
|
95
|
+
server.ws.on("rivet:client-error", (data, client) => {
|
|
96
|
+
const message = String(data?.message ?? "Unknown error");
|
|
97
|
+
// Benign ResizeObserver loop notice (older dev clients still forward it): no overlay, no error log.
|
|
98
|
+
if (/^ResizeObserver loop (completed with undelivered notifications|limit exceeded)/.test(message))
|
|
99
|
+
return;
|
|
100
|
+
const stack = typeof data?.stack === "string" ? data.stack : "";
|
|
101
|
+
server.config.logger.error(`[rivet] browser error: ${stack || message}`);
|
|
102
|
+
if (overlayOn(server))
|
|
103
|
+
client.send({ type: "error", err: { message: "Browser error: " + message, stack, plugin: "rivet" } });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
let matcher;
|
|
107
|
+
const matches = (table, url) => {
|
|
108
|
+
if (matcher?.table !== table)
|
|
109
|
+
matcher = { table, match: compileRoutes(table.routes.map((route) => ({ path: route.pattern, render: () => route }))) };
|
|
110
|
+
return matcher.match(url) != null;
|
|
111
|
+
};
|
|
112
|
+
/** The `_404` an unmatched URL renders (longest folder prefix, else the root one). */
|
|
113
|
+
const notFoundFor = (table, path) => table.notFounds?.find(({ prefix }) => path === prefix || path.startsWith(prefix + "/"))?.page ?? table.notFound;
|
|
114
|
+
/** Route patterns, once each, in table order: `[pattern, endpoint-only]`. */
|
|
115
|
+
const patterns = (table) => {
|
|
116
|
+
const seen = new Map();
|
|
117
|
+
for (const route of table.routes)
|
|
118
|
+
if (!seen.has(route.pattern))
|
|
119
|
+
seen.set(route.pattern, !route.page);
|
|
120
|
+
return [...seen];
|
|
121
|
+
};
|
|
122
|
+
const nonceOf = (response) => /'nonce-([^']+)'/.exec(response.headers.get("content-security-policy") ?? "")?.[1];
|
|
123
|
+
const nonceAttr = (nonce) => (nonce === undefined ? "" : ` nonce="${escapeAttribute(nonce)}"`);
|
|
124
|
+
const html = (response, body) => {
|
|
125
|
+
const headers = new Headers(response.headers);
|
|
126
|
+
headers.delete("content-length");
|
|
127
|
+
headers.set("content-type", "text/html; charset=utf-8");
|
|
128
|
+
return new Response(body, { status: response.status, headers });
|
|
129
|
+
};
|
|
130
|
+
/** Insert markup before the document's last `</body>` (else append). */
|
|
131
|
+
const inject = (page, markup) => {
|
|
132
|
+
const at = page.lastIndexOf("</body>");
|
|
133
|
+
return at < 0 ? page + markup : page.slice(0, at) + markup + page.slice(at);
|
|
134
|
+
};
|
|
135
|
+
/** Inline module script opening Vite's overlay on this page (the request's ws message reached only pages already open). */
|
|
136
|
+
const overlayScript = (server, failure, nonce) => overlayOf(server, overlayError(failure), nonce);
|
|
137
|
+
const overlayOf = (server, error, nonce) => `<script type="module"${nonceAttr(nonce)}>import { ErrorOverlay } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
138
|
+
`document.body.appendChild(new ErrorOverlay(${scriptJSON(error)}));</script>`;
|
|
139
|
+
const STYLE = "body{margin:0;font:15px/1.5 system-ui,sans-serif;background:#fff;color:#1b1b1f}main{max-width:72rem;margin:0 auto;padding:2rem 1rem}" +
|
|
140
|
+
"h1{font-size:1.4rem;margin:0 0 .5rem}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}pre{overflow:auto;padding:1rem;border-radius:6px;background:#f4f4f6;white-space:pre-wrap}" +
|
|
141
|
+
".message{color:#b00020;font-weight:600}ul{padding-left:1.2rem}li{margin:.15rem 0}small{color:#6b6b76}" +
|
|
142
|
+
"@media (prefers-color-scheme:dark){body{background:#151518;color:#e8e8ec}pre{background:#222228}.message{color:#ff8a8a}small{color:#9a9aa6}a{color:#8ab4ff}}";
|
|
143
|
+
const devPage = (title, body, scripts) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">` +
|
|
144
|
+
`<title>${escapeText(title)}</title><style>${STYLE}</style></head><body><main>${body}</main>${scripts}</body></html>`;
|
|
145
|
+
/** Vite's client (HMR, overlay) plus a reload when server code changes (`rivet:data`) or the route table does. */
|
|
146
|
+
const devClient = (server, nonce) => `<script type="module"${nonceAttr(nonce)}>import { createHotContext } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
147
|
+
`createHotContext("/@rivet/dev-page").on("rivet:data", () => location.reload());</script>`;
|
|
148
|
+
/** Route list markup: static patterns link to their page. */
|
|
149
|
+
const routeList = (server, table) => {
|
|
150
|
+
const items = patterns(table).map(([pattern, endpoint]) => {
|
|
151
|
+
const label = `<code>${escapeText(pattern)}</code>`;
|
|
152
|
+
const link = !endpoint && !/[:*]/.test(pattern) ? `<a href="${escapeAttribute(server.config.base + pattern.slice(1))}">${label}</a>` : label;
|
|
153
|
+
return `<li>${link}${endpoint ? " <small>endpoint</small>" : ""}</li>`;
|
|
154
|
+
});
|
|
155
|
+
return items.length ? `<ul id="rivet-dev-routes">${items.join("")}</ul>` : "<p>No routes yet: add a page under the routes directory.</p>";
|
|
156
|
+
};
|
|
157
|
+
/** HTML comment (`--` cannot appear inside one). */
|
|
158
|
+
const comment = (text) => `<!-- ${text.replace(/--/g, "- -")} -->`;
|
|
159
|
+
/**
|
|
160
|
+
* Wrap the dev app handler: a reported 5xx page gets the dev error page (no `_error` view) or the
|
|
161
|
+
* overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes. `reach`: the page's server
|
|
162
|
+
* reach reports (RV3011, `build/reach.ts`): an error opens the overlay over the rendered page.
|
|
163
|
+
*/
|
|
164
|
+
export function devHandler(handler, server, table, reach) {
|
|
165
|
+
return async (request, context) => {
|
|
166
|
+
// A page document (not a data request): its route's server reach errors first (the walk may regenerate the App).
|
|
167
|
+
const url = new URL(request.url);
|
|
168
|
+
const reachErrors = reach && request.method === "GET" && !url.searchParams.has("_rivet") && (request.headers.get("accept") ?? "text/html").includes("text/html")
|
|
169
|
+
? (await reach(url)).filter((problem) => problem.severity === "error")
|
|
170
|
+
: [];
|
|
171
|
+
const seen = [];
|
|
172
|
+
const response = await failures.run(seen, () => handler(request, context));
|
|
173
|
+
if (!(response.headers.get("content-type") ?? "").startsWith("text/html"))
|
|
174
|
+
return response;
|
|
175
|
+
const nonce = nonceOf(response);
|
|
176
|
+
const routes = table();
|
|
177
|
+
const failure = seen[0];
|
|
178
|
+
if (failure && response.status >= 500) {
|
|
179
|
+
const own = failure.route === undefined ? undefined : routes.routes.find((entry) => entry.id === failure.route);
|
|
180
|
+
const overlay = overlayOn(server) ? overlayScript(server, failure, nonce) : "";
|
|
181
|
+
const where = comment(`rivet dev: ${failure.phase} failed${failure.route === undefined ? "" : ` in route "${failure.route}"`}: ${failure.message}`);
|
|
182
|
+
// The app's `_error` view renders as in production; the overlay shows the details over it.
|
|
183
|
+
if (own?.error ?? routes.error)
|
|
184
|
+
return html(response, inject(await response.text(), where + overlay));
|
|
185
|
+
const body = `<h1>Server error</h1><p id="rivet-dev-where">${failure.route === undefined ? "" : `Route <code>${escapeText(failure.route)}</code>, `}phase <code>${escapeText(failure.phase)}</code></p>` +
|
|
186
|
+
`<pre class="message">${escapeText(failure.message)}</pre>${failure.stack ? `<pre class="stack">${escapeText(failure.stack)}</pre>` : ""}` +
|
|
187
|
+
"<p><small>Development only: production answers with a generic message and a digest. Add an <code>_error</code> page to render your own.</small></p>";
|
|
188
|
+
return html(response, devPage(`${response.status} · ${failure.message}`, body, devClient(server, nonce) + overlay));
|
|
189
|
+
}
|
|
190
|
+
if (response.status === 404 && request.method === "GET" && !matches(routes, new URL(request.url))) {
|
|
191
|
+
const path = new URL(request.url).pathname;
|
|
192
|
+
const list = patterns(routes)
|
|
193
|
+
.map(([pattern]) => pattern)
|
|
194
|
+
.join(" ");
|
|
195
|
+
if (notFoundFor(routes, path))
|
|
196
|
+
return html(response, inject(await response.text(), comment(`rivet dev: no route matches ${path}. Routes: ${list}`)));
|
|
197
|
+
const body = `<h1>No route matches <code>${escapeText(path)}</code></h1><p>Routes:</p>${routeList(server, routes)}` +
|
|
198
|
+
"<p><small>Development only: add a <code>_404</code> page to render your own.</small></p>";
|
|
199
|
+
return html(response, devPage("404 · " + path, body, devClient(server, nonce)));
|
|
200
|
+
}
|
|
201
|
+
if (reachErrors.length && response.status < 400 && overlayOn(server)) {
|
|
202
|
+
const error = { message: reachErrors.map((problem) => problem.message).join("\n\n"), stack: "", plugin: "vite-plugin-rivet-router" };
|
|
203
|
+
return html(response, inject(await response.text(), comment(error.message) + overlayOf(server, error, nonce)));
|
|
204
|
+
}
|
|
205
|
+
return response.body ? lateFailures(response, seen, server, nonce) : response;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* A render that fails after the response started (status sent; e.g. the resume state, encoded at the
|
|
210
|
+
* end of the shell) ends the document early (failure marker, or an errored body): the page then ends
|
|
211
|
+
* with the hint comment and opens the overlay itself (the ws message only reaches pages already
|
|
212
|
+
* open), and reloads on the next update.
|
|
213
|
+
*/
|
|
214
|
+
function lateFailures(response, seen, server, nonce) {
|
|
215
|
+
const reader = response.body.getReader();
|
|
216
|
+
const encoder = new TextEncoder();
|
|
217
|
+
const before = seen.length;
|
|
218
|
+
const hint = (failure) => {
|
|
219
|
+
const where = comment(`rivet dev: ${failure.phase} failed${failure.route === undefined ? "" : ` in route "${failure.route}"`}: ${failure.message}`);
|
|
220
|
+
const reload = `<script type="module"${nonceAttr(nonce)}>import { createHotContext } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
221
|
+
`const hot = createHotContext("/@rivet/dev-page");hot.on("rivet:data", () => location.reload());hot.on("vite:beforeUpdate", () => location.reload());</script>`;
|
|
222
|
+
return encoder.encode(where + (overlayOn(server) ? overlayScript(server, failure, nonce) : "") + reload);
|
|
223
|
+
};
|
|
224
|
+
const body = new ReadableStream({
|
|
225
|
+
async pull(controller) {
|
|
226
|
+
let chunk;
|
|
227
|
+
try {
|
|
228
|
+
chunk = await reader.read();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const failure = seen[0];
|
|
232
|
+
if (!failure)
|
|
233
|
+
return controller.error(error);
|
|
234
|
+
controller.enqueue(hint(failure));
|
|
235
|
+
return controller.close();
|
|
236
|
+
}
|
|
237
|
+
if (!chunk.done)
|
|
238
|
+
return controller.enqueue(chunk.value);
|
|
239
|
+
// A failure reported while the body streamed (the document closed with its failure marker).
|
|
240
|
+
if (seen.length > before)
|
|
241
|
+
controller.enqueue(hint(seen[before]));
|
|
242
|
+
controller.close();
|
|
243
|
+
},
|
|
244
|
+
cancel: (reason) => reader.cancel(reason),
|
|
245
|
+
});
|
|
246
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
247
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
/** The measurement mode env var: `swap` here; `on` / `off` belong to the runtime (server lazy row records, LAZY-ROWS T4, read at render). */
|
|
3
|
+
export declare const LAZY_ROWS_ENV = "RIVET_LAZY_ROWS";
|
|
4
|
+
/** Set by the measurement tools for their build children: production builds honour `RIVET_LAZY_ROWS` only with it. */
|
|
5
|
+
export declare const LAZY_ROWS_HARNESS_ENV = "RIVET_LAZY_ROWS_HARNESS";
|
|
6
|
+
/** Whether the swap harness applies to a Vite run (`command`): dev server, or a harness build. `on` / `off` → null (runtime
|
|
7
|
+
* values, no build change). Throws on unknown values. */
|
|
8
|
+
export declare function lazyRowsMode(command: "serve" | "build", env?: Readonly<Record<string, string | undefined>>): "swap" | null;
|
|
9
|
+
/**
|
|
10
|
+
* Lazy rows proper (LAZY-ROWS T5, `on`): a build whose resume runtime materialises them needs
|
|
11
|
+
* `rivetRouter({ resume: { lazyRows: true } })`. The measurement tools (docs-kit `check-examples`,
|
|
12
|
+
* `scripts/lazy-rows/probe.ts`) build app configs they do not own: with `RIVET_LAZY_ROWS=on` AND `LAZY_ROWS_HARNESS_ENV`
|
|
13
|
+
* (which only they set for their build children) `rivetRouter` turns the option on for that build (LAZY-T5B).
|
|
14
|
+
*/
|
|
15
|
+
export declare function lazyRowsBuild(env?: Readonly<Record<string, string | undefined>>): boolean;
|
|
16
|
+
/** The runtime module the server entry imports in swap mode (no package export: not public API). */
|
|
17
|
+
export declare function lazyRowsModule(): string;
|
|
18
|
+
/** Vite plugin: in swap mode the generated server entry (`entry`) installs the swap row hook before anything else. */
|
|
19
|
+
export declare function lazyRowsHarness(entry: () => string): Plugin;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Lazy collection rows, T1 measurement harness (spec docs/design/specs/2026-10-19-lazy-collection-rows-design.md §3.4, R2):
|
|
2
|
+
// `RIVET_LAZY_ROWS=swap` renders every For row inside a resumable region unrecorded and re-creates the region in the
|
|
3
|
+
// browser on the first interaction (create + swap). A bytes / server-CPU baseline only — it must never ship (the event
|
|
4
|
+
// target is replaced under the pointer, inputs lose their value, focus moves), so it is no option of `rivet.config` /
|
|
5
|
+
// `rivetRouter()`: only this environment variable turns it on, and only in the dev server or in a build whose environment
|
|
6
|
+
// also carries `LAZY_ROWS_HARNESS_ENV` (docs-kit `check-examples` / `scripts/lazy-rows/probe.ts` set it for their
|
|
7
|
+
// build children). Everything it adds is server code (`@rivetjs/runtime` `server-lazy-swap.ts`, installed first by the
|
|
8
|
+
// generated server entry): browser bundles are unchanged.
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
/** The measurement mode env var: `swap` here; `on` / `off` belong to the runtime (server lazy row records, LAZY-ROWS T4, read at render). */
|
|
12
|
+
export const LAZY_ROWS_ENV = "RIVET_LAZY_ROWS";
|
|
13
|
+
/** Set by the measurement tools for their build children: production builds honour `RIVET_LAZY_ROWS` only with it. */
|
|
14
|
+
export const LAZY_ROWS_HARNESS_ENV = "RIVET_LAZY_ROWS_HARNESS";
|
|
15
|
+
/** Whether the swap harness applies to a Vite run (`command`): dev server, or a harness build. `on` / `off` → null (runtime
|
|
16
|
+
* values, no build change). Throws on unknown values. */
|
|
17
|
+
export function lazyRowsMode(command, env = process.env) {
|
|
18
|
+
const value = env[LAZY_ROWS_ENV];
|
|
19
|
+
if (!value || value === "on" || value === "off")
|
|
20
|
+
return null;
|
|
21
|
+
if (value !== "swap")
|
|
22
|
+
throw new Error(`${LAZY_ROWS_ENV}=${value}: the only measurement mode is "swap" ("on" / "off": server lazy row records)`);
|
|
23
|
+
if (command === "serve" || env[LAZY_ROWS_HARNESS_ENV] === "1")
|
|
24
|
+
return "swap";
|
|
25
|
+
console.warn(`rivetRouter: ${LAZY_ROWS_ENV}=swap ignored: a measurement mode for the dev server and the lazy-rows tools only`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Lazy rows proper (LAZY-ROWS T5, `on`): a build whose resume runtime materialises them needs
|
|
30
|
+
* `rivetRouter({ resume: { lazyRows: true } })`. The measurement tools (docs-kit `check-examples`,
|
|
31
|
+
* `scripts/lazy-rows/probe.ts`) build app configs they do not own: with `RIVET_LAZY_ROWS=on` AND `LAZY_ROWS_HARNESS_ENV`
|
|
32
|
+
* (which only they set for their build children) `rivetRouter` turns the option on for that build (LAZY-T5B).
|
|
33
|
+
*/
|
|
34
|
+
export function lazyRowsBuild(env = process.env) {
|
|
35
|
+
return env[LAZY_ROWS_ENV] === "on" && env[LAZY_ROWS_HARNESS_ENV] === "1";
|
|
36
|
+
}
|
|
37
|
+
/** The runtime module the server entry imports in swap mode (no package export: not public API). */
|
|
38
|
+
export function lazyRowsModule() {
|
|
39
|
+
return join(dirname(createRequire(import.meta.url).resolve("@rivetjs/runtime/server-resume")), "server-lazy-swap.ts");
|
|
40
|
+
}
|
|
41
|
+
/** Vite plugin: in swap mode the generated server entry (`entry`) installs the swap row hook before anything else. */
|
|
42
|
+
export function lazyRowsHarness(entry) {
|
|
43
|
+
let active = false;
|
|
44
|
+
return {
|
|
45
|
+
name: "vite-plugin-rivet-lazy-rows",
|
|
46
|
+
enforce: "pre",
|
|
47
|
+
config(_config, env) {
|
|
48
|
+
active = lazyRowsMode(env.command) !== null;
|
|
49
|
+
if (active)
|
|
50
|
+
console.warn(`rivetRouter: ${LAZY_ROWS_ENV}=swap: For rows render unrecorded (measurement build, never ship it)`);
|
|
51
|
+
},
|
|
52
|
+
transform(code, id, options) {
|
|
53
|
+
if (!active || !options?.ssr || id.split("?")[0] !== entry())
|
|
54
|
+
return null;
|
|
55
|
+
// A call, not a bare import: the runtime package is `sideEffects: false`.
|
|
56
|
+
return { code: `import { _lazyRowsSwap } from ${JSON.stringify(lazyRowsModule())};\n_lazyRowsSwap();\n` + code, map: null };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type TS from "typescript";
|
|
2
|
+
/** Wrapper and conventional export name per former magic name. */
|
|
3
|
+
export declare const WRAPPERS: Readonly<Record<string, {
|
|
4
|
+
readonly wrapper: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
}>>;
|
|
7
|
+
/** Former sibling `*.server.ts` names → the in-file name they meant. */
|
|
8
|
+
export declare const SIBLING: Readonly<Record<string, string>>;
|
|
9
|
+
export interface MigrateReport {
|
|
10
|
+
/** Changed files (absolute) → new contents. */
|
|
11
|
+
readonly changes: ReadonlyMap<string, string>;
|
|
12
|
+
/** `file: old → new = wrapper(…)` per rewritten export. */
|
|
13
|
+
readonly renamed: readonly string[];
|
|
14
|
+
/** Exports left alone, with the reason. */
|
|
15
|
+
readonly skipped: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute the migration for route modules under `routesDir` (pages, layouts, plain `.ts` endpoints,
|
|
19
|
+
* sibling `*.server.ts`); references are searched in every source module under `projectDir`. Nothing is
|
|
20
|
+
* written (see `writeMigration`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function migrateServerExports(projectDir: string, routesDir: string): Promise<MigrateReport>;
|
|
23
|
+
/** Migrate one module's text on its own (no other modules: references elsewhere are not renamed). */
|
|
24
|
+
export declare function migrateSource(ts: typeof TS, text: string, fileName: string): {
|
|
25
|
+
text: string;
|
|
26
|
+
renamed: readonly string[];
|
|
27
|
+
skipped: readonly string[];
|
|
28
|
+
};
|
|
29
|
+
/** The migration over in-memory `texts` (absolute paths); `targets`: the route modules to rewrite. */
|
|
30
|
+
export declare function migrateTexts(ts: typeof TS, texts: ReadonlyMap<string, string>, targets: readonly string[], root: string): MigrateReport;
|
|
31
|
+
export declare function writeMigration(report: MigrateReport): Promise<void>;
|