@rivetjs/router 0.0.0-stage → 0.1.0-alpha.0
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 +7 -0
- package/adapters/files.js +103 -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 +163 -0
- package/adapters/node.js +940 -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 +92 -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 +33 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +84 -0
- package/build/codegen.js +920 -0
- package/build/dev-errors.d.ts +19 -0
- package/build/dev-errors.js +236 -0
- package/build/migrate.d.ts +15 -0
- package/build/migrate.js +146 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +50 -0
- package/build/routes.js +202 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +195 -0
- package/build/vite.js +703 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +100 -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 +74 -0
- package/client/index.d.ts +152 -0
- package/client/index.js +680 -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 +138 -0
- package/index.js +7 -0
- package/package.json +156 -4
- package/server/app.d.ts +169 -0
- package/server/app.js +302 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +196 -0
- package/server/handler.js +623 -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 +78 -0
- package/server/render.d.ts +94 -0
- package/server/render.js +489 -0
- package/server/route-cache.d.ts +175 -0
- package/server/route-cache.js +742 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -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/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
|
@@ -0,0 +1,19 @@
|
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
export declare function devHandler(handler: AppHandler, server: ViteDevServer, table: () => RouteTable): AppHandler;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,236 @@
|
|
|
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) => `<script type="module"${nonceAttr(nonce)}>import { ErrorOverlay } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
137
|
+
`document.body.appendChild(new ErrorOverlay(${scriptJSON(overlayError(failure))}));</script>`;
|
|
138
|
+
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}" +
|
|
139
|
+
"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}" +
|
|
140
|
+
".message{color:#b00020;font-weight:600}ul{padding-left:1.2rem}li{margin:.15rem 0}small{color:#6b6b76}" +
|
|
141
|
+
"@media (prefers-color-scheme:dark){body{background:#151518;color:#e8e8ec}pre{background:#222228}.message{color:#ff8a8a}small{color:#9a9aa6}a{color:#8ab4ff}}";
|
|
142
|
+
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">` +
|
|
143
|
+
`<title>${escapeText(title)}</title><style>${STYLE}</style></head><body><main>${body}</main>${scripts}</body></html>`;
|
|
144
|
+
/** Vite's client (HMR, overlay) plus a reload when server code changes (`rivet:data`) or the route table does. */
|
|
145
|
+
const devClient = (server, nonce) => `<script type="module"${nonceAttr(nonce)}>import { createHotContext } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
146
|
+
`createHotContext("/@rivet/dev-page").on("rivet:data", () => location.reload());</script>`;
|
|
147
|
+
/** Route list markup: static patterns link to their page. */
|
|
148
|
+
const routeList = (server, table) => {
|
|
149
|
+
const items = patterns(table).map(([pattern, endpoint]) => {
|
|
150
|
+
const label = `<code>${escapeText(pattern)}</code>`;
|
|
151
|
+
const link = !endpoint && !/[:*]/.test(pattern) ? `<a href="${escapeAttribute(server.config.base + pattern.slice(1))}">${label}</a>` : label;
|
|
152
|
+
return `<li>${link}${endpoint ? " <small>endpoint</small>" : ""}</li>`;
|
|
153
|
+
});
|
|
154
|
+
return items.length ? `<ul id="rivet-dev-routes">${items.join("")}</ul>` : "<p>No routes yet: add a page under the routes directory.</p>";
|
|
155
|
+
};
|
|
156
|
+
/** HTML comment (`--` cannot appear inside one). */
|
|
157
|
+
const comment = (text) => `<!-- ${text.replace(/--/g, "- -")} -->`;
|
|
158
|
+
/**
|
|
159
|
+
* Wrap the dev app handler: a reported 5xx page gets the dev error page (no `_error` view) or the
|
|
160
|
+
* overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes.
|
|
161
|
+
*/
|
|
162
|
+
export function devHandler(handler, server, table) {
|
|
163
|
+
return async (request, context) => {
|
|
164
|
+
const seen = [];
|
|
165
|
+
const response = await failures.run(seen, () => handler(request, context));
|
|
166
|
+
if (!(response.headers.get("content-type") ?? "").startsWith("text/html"))
|
|
167
|
+
return response;
|
|
168
|
+
const nonce = nonceOf(response);
|
|
169
|
+
const routes = table();
|
|
170
|
+
const failure = seen[0];
|
|
171
|
+
if (failure && response.status >= 500) {
|
|
172
|
+
const own = failure.route === undefined ? undefined : routes.routes.find((entry) => entry.id === failure.route);
|
|
173
|
+
const overlay = overlayOn(server) ? overlayScript(server, failure, nonce) : "";
|
|
174
|
+
const where = comment(`rivet dev: ${failure.phase} failed${failure.route === undefined ? "" : ` in route "${failure.route}"`}: ${failure.message}`);
|
|
175
|
+
// The app's `_error` view renders as in production; the overlay shows the details over it.
|
|
176
|
+
if (own?.error ?? routes.error)
|
|
177
|
+
return html(response, inject(await response.text(), where + overlay));
|
|
178
|
+
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>` +
|
|
179
|
+
`<pre class="message">${escapeText(failure.message)}</pre>${failure.stack ? `<pre class="stack">${escapeText(failure.stack)}</pre>` : ""}` +
|
|
180
|
+
"<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>";
|
|
181
|
+
return html(response, devPage(`${response.status} · ${failure.message}`, body, devClient(server, nonce) + overlay));
|
|
182
|
+
}
|
|
183
|
+
if (response.status === 404 && request.method === "GET" && !matches(routes, new URL(request.url))) {
|
|
184
|
+
const path = new URL(request.url).pathname;
|
|
185
|
+
const list = patterns(routes)
|
|
186
|
+
.map(([pattern]) => pattern)
|
|
187
|
+
.join(" ");
|
|
188
|
+
if (notFoundFor(routes, path))
|
|
189
|
+
return html(response, inject(await response.text(), comment(`rivet dev: no route matches ${path}. Routes: ${list}`)));
|
|
190
|
+
const body = `<h1>No route matches <code>${escapeText(path)}</code></h1><p>Routes:</p>${routeList(server, routes)}` +
|
|
191
|
+
"<p><small>Development only: add a <code>_404</code> page to render your own.</small></p>";
|
|
192
|
+
return html(response, devPage("404 · " + path, body, devClient(server, nonce)));
|
|
193
|
+
}
|
|
194
|
+
return response.body ? lateFailures(response, seen, server, nonce) : response;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* A render that fails after the response started (status sent; e.g. the resume state, encoded at the
|
|
199
|
+
* end of the shell) ends the document early (failure marker, or an errored body): the page then ends
|
|
200
|
+
* with the hint comment and opens the overlay itself (the ws message only reaches pages already
|
|
201
|
+
* open), and reloads on the next update.
|
|
202
|
+
*/
|
|
203
|
+
function lateFailures(response, seen, server, nonce) {
|
|
204
|
+
const reader = response.body.getReader();
|
|
205
|
+
const encoder = new TextEncoder();
|
|
206
|
+
const before = seen.length;
|
|
207
|
+
const hint = (failure) => {
|
|
208
|
+
const where = comment(`rivet dev: ${failure.phase} failed${failure.route === undefined ? "" : ` in route "${failure.route}"`}: ${failure.message}`);
|
|
209
|
+
const reload = `<script type="module"${nonceAttr(nonce)}>import { createHotContext } from ${scriptJSON(server.config.base + "@vite/client")};` +
|
|
210
|
+
`const hot = createHotContext("/@rivet/dev-page");hot.on("rivet:data", () => location.reload());hot.on("vite:beforeUpdate", () => location.reload());</script>`;
|
|
211
|
+
return encoder.encode(where + (overlayOn(server) ? overlayScript(server, failure, nonce) : "") + reload);
|
|
212
|
+
};
|
|
213
|
+
const body = new ReadableStream({
|
|
214
|
+
async pull(controller) {
|
|
215
|
+
let chunk;
|
|
216
|
+
try {
|
|
217
|
+
chunk = await reader.read();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const failure = seen[0];
|
|
221
|
+
if (!failure)
|
|
222
|
+
return controller.error(error);
|
|
223
|
+
controller.enqueue(hint(failure));
|
|
224
|
+
return controller.close();
|
|
225
|
+
}
|
|
226
|
+
if (!chunk.done)
|
|
227
|
+
return controller.enqueue(chunk.value);
|
|
228
|
+
// A failure reported while the body streamed (the document closed with its failure marker).
|
|
229
|
+
if (seen.length > before)
|
|
230
|
+
controller.enqueue(hint(seen[before]));
|
|
231
|
+
controller.close();
|
|
232
|
+
},
|
|
233
|
+
cancel: (reason) => reader.cancel(reason),
|
|
234
|
+
});
|
|
235
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
236
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const RENAMES: Readonly<Record<string, string>>;
|
|
2
|
+
export interface MigrateReport {
|
|
3
|
+
/** Changed files (absolute) → new contents. */
|
|
4
|
+
readonly changes: ReadonlyMap<string, string>;
|
|
5
|
+
/** `file: old → new` per renamed export. */
|
|
6
|
+
readonly renamed: readonly string[];
|
|
7
|
+
/** Exports left alone, with the reason. */
|
|
8
|
+
readonly skipped: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compute the migration for `*.server.ts` modules under `routesDir`; references are searched in every
|
|
12
|
+
* source module under `projectDir`. Nothing is written (see `writeMigration`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function migrateServerExports(projectDir: string, routesDir: string): Promise<MigrateReport>;
|
|
15
|
+
export declare function writeMigration(report: MigrateReport): Promise<void>;
|
package/build/migrate.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// `rivet migrate server-exports`: rename the old sibling `*.server.ts` export names (`load`, `guard`,
|
|
2
|
+
// `actions`, `entries`, `headers`, `GET`…) to the in-file names (`serverLoad`…, `onGet`…). Renames are
|
|
3
|
+
// scope-aware (TypeScript language service): references in other modules (`typeof import(…).load`)
|
|
4
|
+
// follow; unrelated locals of the same name do not.
|
|
5
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
7
|
+
export const RENAMES = {
|
|
8
|
+
load: "serverLoad",
|
|
9
|
+
guard: "serverGuard",
|
|
10
|
+
actions: "serverActions",
|
|
11
|
+
entries: "serverEntries",
|
|
12
|
+
headers: "serverHeaders",
|
|
13
|
+
GET: "onGet",
|
|
14
|
+
POST: "onPost",
|
|
15
|
+
PUT: "onPut",
|
|
16
|
+
PATCH: "onPatch",
|
|
17
|
+
DELETE: "onDelete",
|
|
18
|
+
};
|
|
19
|
+
const SOURCE = /\.(m|c)?[jt]sx?$/;
|
|
20
|
+
const SERVER = /\.server\.(m|c)?[jt]s$/;
|
|
21
|
+
const IGNORED = /(^|[/\\])(node_modules|dist|\.rivet|\.[^/\\]+)([/\\]|$)/;
|
|
22
|
+
async function sources(dir) {
|
|
23
|
+
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
24
|
+
return entries
|
|
25
|
+
.filter((e) => e.isFile() && SOURCE.test(e.name) && !e.name.endsWith(".d.ts"))
|
|
26
|
+
.map((e) => join(e.parentPath, e.name))
|
|
27
|
+
.filter((file) => !IGNORED.test(relative(dir, file)));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute the migration for `*.server.ts` modules under `routesDir`; references are searched in every
|
|
31
|
+
* source module under `projectDir`. Nothing is written (see `writeMigration`).
|
|
32
|
+
*/
|
|
33
|
+
export async function migrateServerExports(projectDir, routesDir) {
|
|
34
|
+
const ts = (await import("typescript")).default;
|
|
35
|
+
const files = await sources(resolve(projectDir));
|
|
36
|
+
const texts = new Map();
|
|
37
|
+
for (const file of files)
|
|
38
|
+
texts.set(file, await readFile(file, "utf8"));
|
|
39
|
+
const routesRoot = resolve(projectDir, routesDir) + sep;
|
|
40
|
+
const targets = files.filter((file) => file.startsWith(routesRoot) && SERVER.test(file));
|
|
41
|
+
const options = {
|
|
42
|
+
allowJs: true,
|
|
43
|
+
jsx: ts.JsxEmit.Preserve,
|
|
44
|
+
module: ts.ModuleKind.NodeNext,
|
|
45
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
46
|
+
target: ts.ScriptTarget.ES2022,
|
|
47
|
+
allowImportingTsExtensions: true,
|
|
48
|
+
noEmit: true,
|
|
49
|
+
};
|
|
50
|
+
const host = {
|
|
51
|
+
getScriptFileNames: () => files,
|
|
52
|
+
getScriptVersion: () => "1",
|
|
53
|
+
getScriptSnapshot: (file) => {
|
|
54
|
+
const text = texts.get(file) ?? ts.sys.readFile(file);
|
|
55
|
+
return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
|
|
56
|
+
},
|
|
57
|
+
getCurrentDirectory: () => resolve(projectDir),
|
|
58
|
+
getCompilationSettings: () => options,
|
|
59
|
+
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
60
|
+
fileExists: (file) => texts.has(file) || ts.sys.fileExists(file),
|
|
61
|
+
readFile: (file) => texts.get(file) ?? ts.sys.readFile(file),
|
|
62
|
+
readDirectory: ts.sys.readDirectory,
|
|
63
|
+
directoryExists: ts.sys.directoryExists,
|
|
64
|
+
getDirectories: ts.sys.getDirectories,
|
|
65
|
+
};
|
|
66
|
+
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
|
67
|
+
const edits = new Map();
|
|
68
|
+
const renamed = [];
|
|
69
|
+
const skipped = [];
|
|
70
|
+
const edit = (file, start, end, text) => {
|
|
71
|
+
let list = edits.get(file);
|
|
72
|
+
if (!list)
|
|
73
|
+
edits.set(file, (list = []));
|
|
74
|
+
if (!list.some((e) => e.start === start))
|
|
75
|
+
list.push({ start, end, text });
|
|
76
|
+
};
|
|
77
|
+
for (const file of targets) {
|
|
78
|
+
const program = service.getProgram();
|
|
79
|
+
const source = program.getSourceFile(file);
|
|
80
|
+
const name = relative(projectDir, file).split(sep).join("/");
|
|
81
|
+
// Top-level names (declared or exported) decide collisions.
|
|
82
|
+
const locals = new Set();
|
|
83
|
+
const exported = [];
|
|
84
|
+
for (const statement of source.statements) {
|
|
85
|
+
const isExport = ts.canHaveModifiers(statement) && ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
86
|
+
const names = [];
|
|
87
|
+
if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name)
|
|
88
|
+
names.push(statement.name);
|
|
89
|
+
if (ts.isVariableStatement(statement))
|
|
90
|
+
for (const d of statement.declarationList.declarations)
|
|
91
|
+
if (ts.isIdentifier(d.name))
|
|
92
|
+
names.push(d.name);
|
|
93
|
+
for (const id of names) {
|
|
94
|
+
locals.add(id.text);
|
|
95
|
+
if (isExport && id.text in RENAMES)
|
|
96
|
+
exported.push({ old: id.text, node: id });
|
|
97
|
+
}
|
|
98
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause))
|
|
99
|
+
for (const specifier of statement.exportClause.elements) {
|
|
100
|
+
locals.add(specifier.name.text);
|
|
101
|
+
if (specifier.name.text in RENAMES)
|
|
102
|
+
exported.push({ old: specifier.name.text, node: specifier.name, specifier });
|
|
103
|
+
}
|
|
104
|
+
if (ts.isImportDeclaration(statement) && statement.importClause) {
|
|
105
|
+
const clause = statement.importClause;
|
|
106
|
+
if (clause.name)
|
|
107
|
+
locals.add(clause.name.text);
|
|
108
|
+
const bindings = clause.namedBindings;
|
|
109
|
+
if (bindings && ts.isNamedImports(bindings))
|
|
110
|
+
for (const s of bindings.elements)
|
|
111
|
+
locals.add(s.name.text);
|
|
112
|
+
if (bindings && ts.isNamespaceImport(bindings))
|
|
113
|
+
locals.add(bindings.name.text);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const { old, node, specifier } of exported) {
|
|
117
|
+
const next = RENAMES[old];
|
|
118
|
+
if (locals.has(next)) {
|
|
119
|
+
skipped.push(`${name}: ${old} (\`${next}\` already exists)`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (specifier && specifier.propertyName) {
|
|
123
|
+
// `export { x as load }`: only the exported name changes.
|
|
124
|
+
edit(file, specifier.name.getStart(source), specifier.name.getEnd(), next);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const locations = service.findRenameLocations(file, node.getStart(source), false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
|
|
128
|
+
for (const location of locations)
|
|
129
|
+
edit(location.fileName, location.textSpan.start, location.textSpan.start + location.textSpan.length, (location.prefixText ?? "") + next + (location.suffixText ?? ""));
|
|
130
|
+
}
|
|
131
|
+
renamed.push(`${name}: ${old} → ${next}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const changes = new Map();
|
|
135
|
+
for (const [file, list] of edits) {
|
|
136
|
+
let text = texts.get(file) ?? (await readFile(file, "utf8"));
|
|
137
|
+
for (const e of list.sort((a, b) => b.start - a.start))
|
|
138
|
+
text = text.slice(0, e.start) + e.text + text.slice(e.end);
|
|
139
|
+
changes.set(file, text);
|
|
140
|
+
}
|
|
141
|
+
return { changes, renamed, skipped };
|
|
142
|
+
}
|
|
143
|
+
export async function writeMigration(report) {
|
|
144
|
+
for (const [file, text] of report.changes)
|
|
145
|
+
await writeFile(file, text);
|
|
146
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { InlineStyleManifest } from "../server/inline-styles.js";
|
|
2
|
+
import type { Rollup, ViteDevServer } from "vite";
|
|
3
|
+
import type { RouteTable } from "./routes.js";
|
|
4
|
+
/** Client build asset: route id → stylesheet URLs (written only when some route has CSS). */
|
|
5
|
+
export declare const ROUTE_CSS = ".vite/rivet-route-css.json";
|
|
6
|
+
/** Stylesheet URLs per route id from a client bundle (empty when no route module imports CSS). */
|
|
7
|
+
export declare function bundleRouteCSS(bundle: Rollup.OutputBundle, table: RouteTable, routesDir: string, base: string): Record<string, string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* One stylesheet per route: a route whose modules import several stylesheets (per-component CSS, `@rivetjs/ui`)
|
|
10
|
+
* links them as one merged asset (`assets/route-<hash>.css`, the files joined in link order; routes with the same
|
|
11
|
+
* list share it), so a page has one render-blocking route sheet next to the global styles. Lists of one file stay.
|
|
12
|
+
* Returns the merged assets to emit and the route map pointing at them. `prefix` names the asset (docs-kit example
|
|
13
|
+
* frames: `frame`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeRouteCSS(bundle: Rollup.OutputBundle, css: Record<string, string[]>, base: string, prefix?: string): {
|
|
16
|
+
routes: Record<string, string[]>;
|
|
17
|
+
assets: {
|
|
18
|
+
fileName: string;
|
|
19
|
+
source: string;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
22
|
+
/** Client build asset: the stylesheets the handler may write inline (`InlineStyleManifest`; written only when some are). */
|
|
23
|
+
export declare const INLINE_CSS = ".vite/rivet-inline-css.json";
|
|
24
|
+
/**
|
|
25
|
+
* `rivetRouter({ inlineStylesheets })` (Astro's `build.inlineStylesheets` model, decided per page on compressed bytes):
|
|
26
|
+
* `"auto"` (default) / `{ maxTotalBrotli }`, `"always"`, `"never"`.
|
|
27
|
+
*/
|
|
28
|
+
export type InlineStylesheets = "auto" | "always" | "never" | {
|
|
29
|
+
readonly maxTotalBrotli?: number;
|
|
30
|
+
};
|
|
31
|
+
/** `auto`: all of a page's sheets inline when their brotli sizes add up to at most this. */
|
|
32
|
+
export declare const MAX_TOTAL_BROTLI = 6144;
|
|
33
|
+
/**
|
|
34
|
+
* The inline stylesheet manifest (`.vite/rivet-inline-css.json`, `server/inline-styles.ts` decides per page), or
|
|
35
|
+
* `undefined` when no sheet qualifies (`"never"`, nothing listed). Listed: every client CSS asset and merged route sheet
|
|
36
|
+
* (keys = the URLs the handler links, `base` + file name) without relative `url()` / `@import` references (they resolve
|
|
37
|
+
* against the sheet's URL) that could ever go inline — `"always"`: all; `"auto"`: brotli size ≤ `maxTotalBrotli` (the
|
|
38
|
+
* per-page total) or raw size ≤ `limit` (Vite `build.assetsInlineLimit`, the per-sheet fallback). Brotli at quality 11.
|
|
39
|
+
*/
|
|
40
|
+
export declare function inlineCSS(bundle: Rollup.OutputBundle, extra: readonly {
|
|
41
|
+
fileName: string;
|
|
42
|
+
source: string;
|
|
43
|
+
}[], base: string, mode: InlineStylesheets, limit: number): InlineStyleManifest | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Drops repeated top-level `@property` / `@keyframes` blocks and `@layer a, b;` statements that are byte-identical to an
|
|
46
|
+
* earlier one (each per-component stylesheet carries the ones it uses; a repeat changes nothing). Strings and comments
|
|
47
|
+
* are skipped; anything else is kept as is.
|
|
48
|
+
*/
|
|
49
|
+
export declare function dedupeTopLevel(css: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Dev: stylesheets in the SSR module graph of a route's modules (loaded first), as `?direct` links
|
|
52
|
+
* (no flash before the client graph injects its `<style>`s; `entry.dev.tsx` then removes them).
|
|
53
|
+
*/
|
|
54
|
+
export declare function devRouteCSS(server: ViteDevServer, table: RouteTable, routesDir: string, id: string, exclude: ReadonlySet<string>): Promise<string[]>;
|