@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,231 @@
|
|
|
1
|
+
// Static adapter: prerender resumable pages (static routes + `entries()`) through the app handler,
|
|
2
|
+
// with `_data.json` beside each page for client navigation and a CSR shell for the rest.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
6
|
+
import { DATA_PARAM } from "../shared/data.js";
|
|
7
|
+
import { loadServer } from "../server/module.js";
|
|
8
|
+
import { hashPolicy, inlineSources } from "../shared/csp-hash.js";
|
|
9
|
+
import { normalizeBase } from "../shared/base.js";
|
|
10
|
+
import { slashPath } from "../shared/slash.js";
|
|
11
|
+
/** Fill a router pattern with params (`:name` encoded, `*name` encoded per segment). */
|
|
12
|
+
export function fillPattern(pattern, params) {
|
|
13
|
+
const out = pattern.replace(/([:*])([A-Za-z_$][\w$]*)/g, (_, kind, name) => {
|
|
14
|
+
const value = params[name];
|
|
15
|
+
if (value === undefined || value === "")
|
|
16
|
+
throw new Error(`Missing param "${name}" for ${pattern}`);
|
|
17
|
+
return kind === "*" ? value.split("/").map(encodeURIComponent).join("/") : encodeURIComponent(value);
|
|
18
|
+
});
|
|
19
|
+
return out.replace(/\/+$/, "") || "/";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Add a CSP `<meta>` to a static page: its inline scripts allowed by SHA-256 hash, script files from
|
|
23
|
+
* its own origin, no plugins, no `<base>`. (`'strict-dynamic'` would not do: browsers pass a nonce's
|
|
24
|
+
* trust on to `import()`, but not a hash's, and the resume loader imports its chunks.) Pages without
|
|
25
|
+
* `<head>` or without inline scripts (and no `options.directives`) are returned unchanged. The `<meta>` goes after a
|
|
26
|
+
* leading `<meta charset>` and `<meta name="viewport">` (the charset must stay in the first 1024 bytes, first).
|
|
27
|
+
* `options.directives` merge in (`cspPolicy`): e.g. `{ "script-src": "'wasm-unsafe-eval'", "worker-src": "'self' blob:" }`;
|
|
28
|
+
* header-only directives (`frame-ancestors`, `report-uri`, `report-to`, `sandbox`) throw.
|
|
29
|
+
*/
|
|
30
|
+
export function hashCSP(html, options) {
|
|
31
|
+
const sha = (text) => `'sha256-${createHash("sha256").update(text, "utf8").digest("base64")}'`;
|
|
32
|
+
// Inline `<style>` elements (inlined stylesheets, head styles): hashed into a `style-src` / `style-src-elem` the options set.
|
|
33
|
+
const found = inlineSources(html, !!options?.directives);
|
|
34
|
+
const head = /<head\b[^>]*>/i.exec(html);
|
|
35
|
+
const extra = options?.directives !== undefined && Object.keys(options.directives).length > 0;
|
|
36
|
+
if ((!found.scripts.length && !extra) || !head)
|
|
37
|
+
return html;
|
|
38
|
+
const policy = hashPolicy(found.scripts.map(sha), found.origins, found.styles.map(sha), options, true);
|
|
39
|
+
// After a leading charset / viewport meta (in that order), before everything else.
|
|
40
|
+
const lead = /^(?:\s*<meta\s+charset\b[^>]*>)?(?:\s*<meta\s+name\s*=\s*["']?viewport\b[^>]*>)?/i.exec(html.slice(head.index + head[0].length));
|
|
41
|
+
const at = head.index + head[0].length + lead[0].length;
|
|
42
|
+
return html.slice(0, at) + `<meta http-equiv="Content-Security-Policy" content="${policy}">` + html.slice(at);
|
|
43
|
+
}
|
|
44
|
+
/** Synthetic request origin when `origin` is unset. */
|
|
45
|
+
const PLACEHOLDER = "http://localhost";
|
|
46
|
+
const STATE = /<script type="rivet\/state"[^>]*>[^<]*/g;
|
|
47
|
+
// The placeholder origin as JSON / HTML text holds it (`http://localhost/…`, `http://localhost"`; `:port` is another origin).
|
|
48
|
+
const PLACEHELD = /http:(?:\\?\/){2}localhost(?![\w.:-])/;
|
|
49
|
+
const placeholder = (text) => PLACEHELD.test(text);
|
|
50
|
+
const ambiguous = (url, where, text) => {
|
|
51
|
+
const at = PLACEHELD.exec(text).index;
|
|
52
|
+
return `Prerender ${url}: the ${where} holds the placeholder origin http://localhost (a URL string built from the request URL: …${text.slice(Math.max(0, at - 40), at + 60)}…); pass prerender({ origin }) / prerenderBuild({ origin }) with the site's origin`;
|
|
53
|
+
};
|
|
54
|
+
// `<script src>` / `<link rel=modulepreload href>` (attribute order as rendered: rel before href).
|
|
55
|
+
const SCRIPT_URL = /<(script\b[^>]*?\ssrc|link\b[^>]*?\srel="?modulepreload"?[^>]*?\shref)="([^"]+)"/g;
|
|
56
|
+
/** Delete the client build's scripts outside the referenced ones' import closure, and every `*.deps.js` module. */
|
|
57
|
+
async function prune(outDir, base, scripts) {
|
|
58
|
+
let manifest;
|
|
59
|
+
try {
|
|
60
|
+
manifest = JSON.parse(await readFile(join(resolve(outDir), ".vite/manifest.json"), "utf8"));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const byFile = new Map(Object.values(manifest).map((chunk) => [chunk.file, chunk]));
|
|
66
|
+
const keep = new Set();
|
|
67
|
+
const visit = (file) => {
|
|
68
|
+
const chunk = file === undefined ? undefined : byFile.get(file);
|
|
69
|
+
if (!chunk || keep.has(chunk.file))
|
|
70
|
+
return;
|
|
71
|
+
keep.add(chunk.file);
|
|
72
|
+
for (const key of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])])
|
|
73
|
+
visit(manifest[key]?.file);
|
|
74
|
+
};
|
|
75
|
+
for (const src of scripts) {
|
|
76
|
+
if (!src.startsWith("/") || src.startsWith("//"))
|
|
77
|
+
continue;
|
|
78
|
+
const path = base && src.startsWith(base + "/") ? src.slice(base.length) : src;
|
|
79
|
+
visit(path.slice(1).split(/[?#]/)[0]);
|
|
80
|
+
}
|
|
81
|
+
const drop = [...byFile.keys()].filter((file) => file.endsWith(".js") && !keep.has(file));
|
|
82
|
+
// The loader's deps modules (`<chunk>.deps.js`): only resumed pages import them.
|
|
83
|
+
const deps = (await readdir(resolve(outDir), { recursive: true })).map((file) => file.split(sep).join("/")).filter((file) => file.endsWith(".deps.js"));
|
|
84
|
+
const out = [...new Set([...drop, ...deps])].sort();
|
|
85
|
+
await Promise.all(out.map((file) => rm(resolve(outDir, file), { force: true })));
|
|
86
|
+
// Directories left empty (`assets/`).
|
|
87
|
+
for (const dir of new Set(out.map((file) => dirname(resolve(outDir, file)))))
|
|
88
|
+
if (dir !== resolve(outDir))
|
|
89
|
+
await rmdir(dir).catch(() => undefined);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** Output file for a URL path, kept inside outDir. */
|
|
93
|
+
function target(outDir, path, file) {
|
|
94
|
+
const root = resolve(outDir);
|
|
95
|
+
const decoded = decodeURIComponent(path);
|
|
96
|
+
const out = resolve(root, "." + decoded, file);
|
|
97
|
+
if (out !== root && !out.startsWith(root + sep))
|
|
98
|
+
throw new Error(`Prerender path escapes outDir: ${path}`);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
async function write(path, body) {
|
|
102
|
+
await mkdir(dirname(path), { recursive: true });
|
|
103
|
+
await writeFile(path, body);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Prerender every eligible route. Non-prerenderable routes with server modules are errors, and so are two
|
|
107
|
+
* pages whose paths differ only in letter case (checked before the second one is written).
|
|
108
|
+
*/
|
|
109
|
+
export async function prerender(options) {
|
|
110
|
+
const { handler, routes, outDir, trailingSlash = "ignore" } = options;
|
|
111
|
+
const origin = options.origin ?? PLACEHOLDER;
|
|
112
|
+
// Directive mistakes throw before any page is rendered.
|
|
113
|
+
if (options.csp)
|
|
114
|
+
hashCSP("<head><script></script>", options.csp);
|
|
115
|
+
const base = normalizeBase(options.base);
|
|
116
|
+
// Output paths: the URL path below the base.
|
|
117
|
+
const local = (path) => (base && (path === base || path.startsWith(base + "/")) ? path.slice(base.length) || "/" : path);
|
|
118
|
+
// Some prerendered page resumes (state script): client navigation may run, so pages get `_data.json` files.
|
|
119
|
+
let resumes = false;
|
|
120
|
+
const html = async (response, url) => {
|
|
121
|
+
const text = await response.text();
|
|
122
|
+
const states = text.match(STATE);
|
|
123
|
+
if (states)
|
|
124
|
+
resumes = true;
|
|
125
|
+
if (options.prune)
|
|
126
|
+
for (const [, , src] of text.matchAll(SCRIPT_URL))
|
|
127
|
+
scripts.add(src);
|
|
128
|
+
const held = origin === PLACEHOLDER ? states?.find(placeholder) : undefined;
|
|
129
|
+
if (held)
|
|
130
|
+
throw new Error(ambiguous(url, "resume state", held));
|
|
131
|
+
return options.csp === false ? text : hashCSP(text, options.csp);
|
|
132
|
+
};
|
|
133
|
+
const report = { pages: [], fallback: [], resumes: false };
|
|
134
|
+
// Script URLs the pages reference (`prune`).
|
|
135
|
+
const scripts = new Set();
|
|
136
|
+
const done = new Set();
|
|
137
|
+
// Prerendered page paths (their `_data.json` files are written after every page rendered).
|
|
138
|
+
const written = [];
|
|
139
|
+
// Output paths folded to lower case → the page that took it.
|
|
140
|
+
const files = new Map();
|
|
141
|
+
const render = async (path, data) => {
|
|
142
|
+
const url = new URL(path, origin);
|
|
143
|
+
if (data)
|
|
144
|
+
url.searchParams.set(DATA_PARAM, "data");
|
|
145
|
+
return handler(new Request(url, { headers: { accept: data ? "application/json" : "text/html" } }));
|
|
146
|
+
};
|
|
147
|
+
for (const route of routes) {
|
|
148
|
+
if (!route.page)
|
|
149
|
+
continue;
|
|
150
|
+
const module = await loadServer(route.server);
|
|
151
|
+
const dynamic = /[:*]/.test(route.pattern);
|
|
152
|
+
const enabled = module?.prerender ?? (!dynamic || module?.entries !== undefined);
|
|
153
|
+
if (!enabled) {
|
|
154
|
+
// Layout loaders/guards run per request too: the CSR shell would skip them.
|
|
155
|
+
const layouts = await Promise.all(route.layouts.map(loadServer));
|
|
156
|
+
if ([module, ...layouts].some((m) => m?.load || m?.guard))
|
|
157
|
+
throw new Error(`Route ${route.id} needs a server (serverLoad/serverGuard on it or a layout, without prerender); use the node adapter or add serverEntries()`);
|
|
158
|
+
if (!report.fallback.includes(route.id))
|
|
159
|
+
report.fallback.push(route.id);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const sets = dynamic ? [...((await module?.entries?.()) ?? [])] : [{}];
|
|
163
|
+
for (const params of sets) {
|
|
164
|
+
const path = fillPattern(route.pattern, params);
|
|
165
|
+
if (done.has(path))
|
|
166
|
+
continue;
|
|
167
|
+
done.add(path);
|
|
168
|
+
// One file on case-insensitive file systems (macOS, Windows) and hosts: the later page would overwrite the earlier.
|
|
169
|
+
const folded = decodeURIComponent(local(path)).toLowerCase();
|
|
170
|
+
const clash = files.get(folded);
|
|
171
|
+
if (clash !== undefined)
|
|
172
|
+
throw new Error(`Prerender paths ${clash} and ${path} (${route.id}) differ only in letter case: one output file on case-insensitive file systems`);
|
|
173
|
+
files.set(folded, path);
|
|
174
|
+
// The canonical URL (`trailingSlash`): the other form would answer 308.
|
|
175
|
+
const url = slashPath(path, trailingSlash);
|
|
176
|
+
const page = await render(url, false);
|
|
177
|
+
if (page.status !== 200)
|
|
178
|
+
throw new Error(`Prerender ${url} (${route.id}) returned ${page.status}`);
|
|
179
|
+
const at = local(path);
|
|
180
|
+
await write(trailingSlash === "never" && at !== "/" ? target(outDir, at + ".html", "") : target(outDir, at, "index.html"), await html(page, url));
|
|
181
|
+
report.pages.push(url);
|
|
182
|
+
written.push(path);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const missing = await render(base + "/__rivet_not_found__", false);
|
|
186
|
+
if (missing.status === 404)
|
|
187
|
+
await write(join(resolve(outDir), "404.html"), await html(missing, "404.html"));
|
|
188
|
+
// `_data.json` serves client navigation only: none when no page resumes (zero-JS sites: links load documents)
|
|
189
|
+
// and no CSR shell route navigates. (Per page they stay: any resumed page's router may navigate to any route.)
|
|
190
|
+
if (resumes || report.fallback.length)
|
|
191
|
+
for (const path of written) {
|
|
192
|
+
const url = slashPath(path, trailingSlash);
|
|
193
|
+
const data = await render(url, true);
|
|
194
|
+
const body = new Uint8Array(await data.arrayBuffer());
|
|
195
|
+
const text = origin === PLACEHOLDER ? new TextDecoder().decode(body) : "";
|
|
196
|
+
if (placeholder(text))
|
|
197
|
+
throw new Error(ambiguous(url, "_data.json", text));
|
|
198
|
+
await write(target(outDir, local(path), "_data.json"), body);
|
|
199
|
+
}
|
|
200
|
+
report.resumes = resumes;
|
|
201
|
+
if (options.prune && !resumes && !report.fallback.length) {
|
|
202
|
+
const pruned = await prune(outDir, base, scripts);
|
|
203
|
+
if (pruned.length)
|
|
204
|
+
report.pruned = pruned;
|
|
205
|
+
}
|
|
206
|
+
if (report.fallback.length) {
|
|
207
|
+
if (!options.shell)
|
|
208
|
+
throw new Error(`Routes ${report.fallback.join(", ")} need a CSR shell (options.shell)`);
|
|
209
|
+
await write(join(resolve(outDir), "200.html"), await options.shell());
|
|
210
|
+
}
|
|
211
|
+
return report;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Prerender a production build (`vite build` + `vite build --ssr`, router `mode: "static"`) into its
|
|
215
|
+
* client directory, so `<dist>/client` is the deployable site.
|
|
216
|
+
*/
|
|
217
|
+
export async function prerenderBuild(options = {}) {
|
|
218
|
+
const { loadBuild } = await import("./build.js");
|
|
219
|
+
const { entry, handler, client } = await loadBuild(options.dist ?? "dist", options.preload ? { preload: options.preload } : {}, options.locales ? { locales: options.locales } : {});
|
|
220
|
+
return prerender({
|
|
221
|
+
handler,
|
|
222
|
+
routes: entry.routes,
|
|
223
|
+
outDir: client,
|
|
224
|
+
...(entry.base ? { base: entry.base } : {}),
|
|
225
|
+
...(entry.trailingSlash ? { trailingSlash: entry.trailingSlash } : {}),
|
|
226
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
227
|
+
...(options.shell ? { shell: options.shell } : {}),
|
|
228
|
+
...(options.csp !== undefined ? { csp: options.csp } : {}),
|
|
229
|
+
prune: options.prune ?? true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Plugin, UserConfig } from "vite";
|
|
2
|
+
import type { FetchOptions } from "./fetch.js";
|
|
3
|
+
import type { CSPOptions } from "../shared/csp.js";
|
|
4
|
+
/** Options every target shares: build-time defaults serialized into the generated entry. */
|
|
5
|
+
interface TargetOptions extends Pick<FetchOptions, "origin" | "maxBody" | "stream" | "headers"> {
|
|
6
|
+
/** Build-time CSP (serialized into the entry): `false` or `{ trustedTypes, directives }`; a policy function goes to `create({ csp })`. */
|
|
7
|
+
readonly csp?: false | CSPOptions;
|
|
8
|
+
/** i18n: per-locale client builds (`client/<locale>/`, see `loadBuild`). */
|
|
9
|
+
readonly locales?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** `"fetch"`: `dist/server/fetch.js` (`export default { fetch }`) for any web-standard runtime. */
|
|
12
|
+
export interface FetchAdapterOptions extends TargetOptions {
|
|
13
|
+
readonly name: "fetch";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Vercel, Build Output API v3 (https://vercel.com/docs/build-output-api/v3): `.vercel/output/`
|
|
17
|
+
* with `config.json`, `static/` (client files + prerendered pages) and one function.
|
|
18
|
+
*/
|
|
19
|
+
export interface VercelAdapterOptions extends TargetOptions {
|
|
20
|
+
readonly name: "vercel";
|
|
21
|
+
/**
|
|
22
|
+
* `"nodejs"` (default; Vercel's recommendation): a Node.js function running the node adapter's
|
|
23
|
+
* listener (direct page bodies, no web-stream reads). `"edge"`: an Edge Runtime function (fetch adapter).
|
|
24
|
+
*/
|
|
25
|
+
readonly runtime?: "nodejs" | "edge";
|
|
26
|
+
/** Node.js major for `runtime: "nodejs"` (`.vc-config.json` `runtime: "nodejs<major>.x"`); default 24. */
|
|
27
|
+
readonly nodeVersion?: 20 | 22 | 24;
|
|
28
|
+
/** Function regions (`.vc-config.json` `regions`). */
|
|
29
|
+
readonly regions?: readonly string[];
|
|
30
|
+
/** Node.js function limits (`.vc-config.json` `maxDuration` seconds, `memory` MB). */
|
|
31
|
+
readonly maxDuration?: number;
|
|
32
|
+
readonly memory?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Incremental Static Regeneration for routes that `export const prerender = true` (Node.js runtime
|
|
35
|
+
* only): instead of build-time files, prerender functions cached by Vercel's CDN and regenerated
|
|
36
|
+
* after `expiration` seconds (`false`: never). Cached pages share one CSP nonce: see the deploy guide.
|
|
37
|
+
*/
|
|
38
|
+
readonly isr?: {
|
|
39
|
+
readonly expiration: number | false;
|
|
40
|
+
};
|
|
41
|
+
/** Brotli/gzip in the Node.js function (default true; see `@rivetjs/router/vercel`). */
|
|
42
|
+
readonly compress?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Netlify, Frameworks API (https://docs.netlify.com/build/frameworks/frameworks-api/): a
|
|
46
|
+
* Functions v2 function (or an Edge Function) in `.netlify/v1/`, `config.json` headers, and the
|
|
47
|
+
* publish directory `dist/netlify` (client files + prerendered pages).
|
|
48
|
+
*/
|
|
49
|
+
export interface NetlifyAdapterOptions extends TargetOptions {
|
|
50
|
+
readonly name: "netlify";
|
|
51
|
+
/** Render in an Edge Function (Deno, at the edge) instead of a Node.js function. */
|
|
52
|
+
readonly edge?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cloudflare Workers with static assets (https://developers.cloudflare.com/workers/static-assets/):
|
|
56
|
+
* `dist/cloudflare/wrangler.json` (+ `.wrangler/deploy/config.json` pointing Wrangler at it) and the
|
|
57
|
+
* assets directory `dist/cloudflare/static`.
|
|
58
|
+
*/
|
|
59
|
+
export interface CloudflareAdapterOptions extends TargetOptions {
|
|
60
|
+
readonly name: "cloudflare";
|
|
61
|
+
/**
|
|
62
|
+
* Extra Wrangler configuration merged into the generated `wrangler.json` (bindings, `routes`,
|
|
63
|
+
* `vars`, …). A `wrangler.json` / `wrangler.jsonc` in the project root is merged first.
|
|
64
|
+
*/
|
|
65
|
+
readonly wrangler?: Readonly<Record<string, unknown>>;
|
|
66
|
+
}
|
|
67
|
+
/** Bun: `bun dist/server/bun.js` serves `dist/client` and the app with `Bun.serve`. */
|
|
68
|
+
export interface BunAdapterOptions extends TargetOptions {
|
|
69
|
+
readonly name: "bun";
|
|
70
|
+
}
|
|
71
|
+
/** Deno: `deno run -A dist/server/deno.js` serves `dist/client` and the app with `Deno.serve`. */
|
|
72
|
+
export interface DenoAdapterOptions extends TargetOptions {
|
|
73
|
+
readonly name: "deno";
|
|
74
|
+
}
|
|
75
|
+
export type AdapterOptions = FetchAdapterOptions | VercelAdapterOptions | NetlifyAdapterOptions | CloudflareAdapterOptions | BunAdapterOptions | DenoAdapterOptions;
|
|
76
|
+
export type AdapterName = AdapterOptions["name"];
|
|
77
|
+
/** Generated entry file name (`dist/server/<file>.js`) per target. */
|
|
78
|
+
export declare const ENTRY_FILE: Readonly<Record<AdapterName, string>>;
|
|
79
|
+
/** `rivetRouter({ adapter })` value → options (a bare name = defaults). */
|
|
80
|
+
export declare function adapterOptions(adapter: AdapterName | AdapterOptions | undefined): AdapterOptions | undefined;
|
|
81
|
+
/** SSR build config: both `entry.server` and the target's entry; dependencies bundled (functions and edge runtimes get no node_modules). */
|
|
82
|
+
export declare function targetBuildConfig(options: AdapterOptions, entry: string, outDir: string): UserConfig;
|
|
83
|
+
/** Build facts the plugin needs from the router plugin. */
|
|
84
|
+
export interface TargetContext {
|
|
85
|
+
/** Generated `entry.server.tsx`. */
|
|
86
|
+
serverEntry(): string;
|
|
87
|
+
/** Router `mode` (`server` / `static`). */
|
|
88
|
+
mode(): "server" | "static" | "csr";
|
|
89
|
+
}
|
|
90
|
+
/** Plugin generating the target entry and, for platform targets, packaging the build after it is written. */
|
|
91
|
+
export declare function targetEntryPlugin(options: AdapterOptions, context: TargetContext): Plugin;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Build side of the deploy targets (`rivetRouter({ adapter })`): the server build gets a second
|
|
2
|
+
// entry, a generated module that imports the server entry, inlines the client build's manifests
|
|
3
|
+
// (read here, at build time, so the bundle needs no file system at run time) and hands both to the
|
|
4
|
+
// target's runtime module (`fetch.ts`, `vercel.ts`, `netlify.ts`, …). Platform targets then package
|
|
5
|
+
// the build into the layout their platform deploys (`pack.ts`, after the server bundle is written).
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { readManifests } from "./build.js";
|
|
9
|
+
import { normalizeBase } from "../shared/base.js";
|
|
10
|
+
/** Generated entry file name (`dist/server/<file>.js`) per target. */
|
|
11
|
+
export const ENTRY_FILE = {
|
|
12
|
+
fetch: "fetch",
|
|
13
|
+
vercel: "vercel",
|
|
14
|
+
netlify: "netlify",
|
|
15
|
+
cloudflare: "worker",
|
|
16
|
+
bun: "bun",
|
|
17
|
+
deno: "deno",
|
|
18
|
+
};
|
|
19
|
+
/** Options that only shape the build output (never serialized into the entry). */
|
|
20
|
+
const BUILD_ONLY = new Set(["name", "locales", "runtime", "nodeVersion", "regions", "maxDuration", "memory", "edge", "wrangler"]);
|
|
21
|
+
const TARGET = "\0rivet-target-entry";
|
|
22
|
+
// Paths in this form (`new URL("./x.js", import.meta.url)`) are rewritten to `.js` by the package build.
|
|
23
|
+
const runtimes = {
|
|
24
|
+
fetch: fileURLToPath(new URL("./fetch.js", import.meta.url)),
|
|
25
|
+
vercel: fileURLToPath(new URL("./vercel.js", import.meta.url)),
|
|
26
|
+
vercelEdge: fileURLToPath(new URL("./vercel-edge.js", import.meta.url)),
|
|
27
|
+
netlify: fileURLToPath(new URL("./netlify.js", import.meta.url)),
|
|
28
|
+
cloudflare: fileURLToPath(new URL("./cloudflare.js", import.meta.url)),
|
|
29
|
+
bun: fileURLToPath(new URL("./bun.js", import.meta.url)),
|
|
30
|
+
deno: fileURLToPath(new URL("./deno.js", import.meta.url)),
|
|
31
|
+
};
|
|
32
|
+
/** `rivetRouter({ adapter })` value → options (a bare name = defaults). */
|
|
33
|
+
export function adapterOptions(adapter) {
|
|
34
|
+
if (adapter === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const options = typeof adapter === "string" ? { name: adapter } : adapter;
|
|
37
|
+
if (!(options.name in ENTRY_FILE))
|
|
38
|
+
throw new Error(`rivetRouter: unknown adapter "${String(options.name)}" (${Object.keys(ENTRY_FILE).join(", ")})`);
|
|
39
|
+
if (options.name === "vercel" && options.isr && options.runtime === "edge")
|
|
40
|
+
throw new Error('rivetRouter: adapter "vercel": isr needs runtime "nodejs"');
|
|
41
|
+
return options;
|
|
42
|
+
}
|
|
43
|
+
/** SSR build config: both `entry.server` and the target's entry; dependencies bundled (functions and edge runtimes get no node_modules). */
|
|
44
|
+
export function targetBuildConfig(options, entry, outDir) {
|
|
45
|
+
return {
|
|
46
|
+
ssr: { noExternal: true },
|
|
47
|
+
build: { ssr: true, outDir, copyPublicDir: false, rollupOptions: { input: { "entry.server": entry, [ENTRY_FILE[options.name]]: TARGET } } },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Runtime module and the generated entry's tail per target. */
|
|
51
|
+
function runtimeOf(options) {
|
|
52
|
+
const created = ["export const create = (options = {}) => createTarget(entry, build, { ...defaults, ...options });", "export default create();"];
|
|
53
|
+
switch (options.name) {
|
|
54
|
+
case "fetch":
|
|
55
|
+
return { path: runtimes.fetch, name: "createFetch", tail: created };
|
|
56
|
+
case "vercel":
|
|
57
|
+
return { path: options.runtime === "edge" ? runtimes.vercelEdge : runtimes.vercel, name: "create", tail: created };
|
|
58
|
+
case "netlify":
|
|
59
|
+
return { path: runtimes.netlify, name: "create", tail: created };
|
|
60
|
+
case "cloudflare":
|
|
61
|
+
return { path: runtimes.cloudflare, name: "create", tail: created };
|
|
62
|
+
case "bun":
|
|
63
|
+
case "deno":
|
|
64
|
+
// Runs when started directly (`bun dist/server/bun.js`); importing it (a custom server) does not listen.
|
|
65
|
+
return {
|
|
66
|
+
path: runtimes[options.name],
|
|
67
|
+
name: "start",
|
|
68
|
+
tail: [
|
|
69
|
+
'const client = "../client/";',
|
|
70
|
+
"export const start = (options = {}) => createTarget(entry, build, { client: new URL(client, import.meta.url), ...defaults, ...options });",
|
|
71
|
+
"if (import.meta.main) start();",
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Plugin generating the target entry and, for platform targets, packaging the build after it is written. */
|
|
77
|
+
export function targetEntryPlugin(options, context) {
|
|
78
|
+
let root = "";
|
|
79
|
+
let outDir = "";
|
|
80
|
+
let base = "";
|
|
81
|
+
return {
|
|
82
|
+
name: "vite-plugin-rivet-target",
|
|
83
|
+
applyToEnvironment: (environment) => environment.config.consumer === "server",
|
|
84
|
+
configResolved(config) {
|
|
85
|
+
root = config.root;
|
|
86
|
+
outDir = resolve(config.root, config.build.outDir);
|
|
87
|
+
base = normalizeBase(config.base);
|
|
88
|
+
},
|
|
89
|
+
resolveId: (id) => (id === TARGET ? id : null),
|
|
90
|
+
async load(id) {
|
|
91
|
+
if (id !== TARGET)
|
|
92
|
+
return null;
|
|
93
|
+
const { locales } = options;
|
|
94
|
+
const defaults = Object.fromEntries(Object.entries(options).filter(([key]) => !BUILD_ONLY.has(key)));
|
|
95
|
+
const manifests = await readManifests(resolve(outDir, ".."), { ...(locales?.length ? { locales } : {}), base }).catch((error) => {
|
|
96
|
+
throw new Error(`rivetRouter({ adapter: "${options.name}" }): build the client first (vite build, then vite build --ssr): ${error.message}`);
|
|
97
|
+
});
|
|
98
|
+
const json = (value) => `JSON.parse(${JSON.stringify(JSON.stringify(value))})`;
|
|
99
|
+
const runtime = runtimeOf(options);
|
|
100
|
+
return [
|
|
101
|
+
`import * as entry from ${JSON.stringify(context.serverEntry())};`,
|
|
102
|
+
`import { ${runtime.name} as createTarget } from ${JSON.stringify(runtime.path)};`,
|
|
103
|
+
`const build = ${json(manifests)};`,
|
|
104
|
+
`const defaults = ${JSON.stringify(defaults)};`,
|
|
105
|
+
"/** Runtime options (`onError`, `address`, `csp` function, …) over the build-time defaults. */",
|
|
106
|
+
...runtime.tail,
|
|
107
|
+
"",
|
|
108
|
+
].join("\n");
|
|
109
|
+
},
|
|
110
|
+
async closeBundle() {
|
|
111
|
+
// `vite build --ssr` only (the client build has no target entry), platform targets only.
|
|
112
|
+
if (this.environment?.config.consumer !== "server" || !["vercel", "netlify", "cloudflare"].includes(options.name))
|
|
113
|
+
return;
|
|
114
|
+
const { pack } = await import("./pack.js");
|
|
115
|
+
await pack(options, { root, dist: resolve(outDir, ".."), server: outDir, base, mode: context.mode() === "static" ? "static" : "server" });
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { BuildManifests } from "./assemble.js";
|
|
3
|
+
import { type FetchOptions } from "./fetch.js";
|
|
4
|
+
export type VercelEdgeOptions = Omit<FetchOptions, "serveAssets">;
|
|
5
|
+
/** Edge function handler: `context` (`waitUntil`) reaches loaders as `event.platform.ctx`. */
|
|
6
|
+
export type VercelEdgeHandler = (request: Request, context?: unknown) => Promise<Response>;
|
|
7
|
+
export declare function create(entry: GeneratedEntry, build: BuildManifests, options?: VercelEdgeOptions): VercelEdgeHandler;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createFetch } from "./fetch.js";
|
|
2
|
+
export function create(entry, build, options = {}) {
|
|
3
|
+
const app = createFetch(entry, build, {
|
|
4
|
+
// Vercel sets `X-Real-IP` / `X-Forwarded-For` to the client address (not client-controlled).
|
|
5
|
+
address: (request) => request.headers.get("x-real-ip") ?? undefined,
|
|
6
|
+
...options,
|
|
7
|
+
});
|
|
8
|
+
return (request, context) => app.fetch(request, undefined, context);
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { GeneratedEntry } from "../server/app.js";
|
|
3
|
+
import type { ErrorInfo } from "../server/handler.js";
|
|
4
|
+
import type { BodyLimit } from "../server/security.js";
|
|
5
|
+
import type { AppConfig } from "../server/app.js";
|
|
6
|
+
import { type BuildManifests } from "./assemble.js";
|
|
7
|
+
export interface VercelOptions {
|
|
8
|
+
/** Fixed public origin; default: the request's `Host` + `X-Forwarded-Proto` (Vercel's proxy sets both). */
|
|
9
|
+
origin?: string;
|
|
10
|
+
maxBody?: BodyLimit;
|
|
11
|
+
stream?: {
|
|
12
|
+
deadline?: number;
|
|
13
|
+
};
|
|
14
|
+
/** Extra headers on every response; defaults add nosniff + referrer policy. */
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
onError?: (error: unknown, request?: Request, info?: ErrorInfo) => void;
|
|
17
|
+
resolveLocale?: (request: Request) => string | undefined | Promise<string | undefined>;
|
|
18
|
+
csp?: AppConfig["csp"];
|
|
19
|
+
/** Preload planner (`createApp` `preload`): `background` byte budget of idle-time visible prefetch, `lean` (nothing before an interaction); per page when a function. */
|
|
20
|
+
preload?: AppConfig["preload"];
|
|
21
|
+
/**
|
|
22
|
+
* Brotli/gzip in the function (default true, as the node adapter). Vercel's CDN documents its
|
|
23
|
+
* automatic compression by MIME allowlist (https://vercel.com/docs/compression), which does not list
|
|
24
|
+
* `text/html`; an encoded response passes through as is.
|
|
25
|
+
*/
|
|
26
|
+
compress?: boolean;
|
|
27
|
+
/** Set when the build has ISR pages: their prerender functions get the page path as `__rivet_path`. */
|
|
28
|
+
isr?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/** `/_isr/a?__rivet_path=%2Fa&x=1` → `/a?x=1` (the ISR route's rewrite, undone). */
|
|
31
|
+
export declare function isrURL(url: string): string;
|
|
32
|
+
/** The function's request listener over a server entry and its inlined client manifests. */
|
|
33
|
+
export declare function create(entry: GeneratedEntry, build: BuildManifests, options?: VercelOptions): (req: IncomingMessage, res: ServerResponse) => void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { assemble } from "./assemble.js";
|
|
2
|
+
import { createListener } from "./node.js";
|
|
3
|
+
const ISR_PATH = "__rivet_path";
|
|
4
|
+
/** `/_isr/a?__rivet_path=%2Fa&x=1` → `/a?x=1` (the ISR route's rewrite, undone). */
|
|
5
|
+
export function isrURL(url) {
|
|
6
|
+
const at = url.indexOf("?");
|
|
7
|
+
if (at < 0)
|
|
8
|
+
return url;
|
|
9
|
+
const query = new URLSearchParams(url.slice(at + 1));
|
|
10
|
+
const path = query.get(ISR_PATH);
|
|
11
|
+
if (path === null || !path.startsWith("/") || path.startsWith("//"))
|
|
12
|
+
return url;
|
|
13
|
+
query.delete(ISR_PATH);
|
|
14
|
+
const rest = query.toString();
|
|
15
|
+
return path + (rest ? "?" + rest : "");
|
|
16
|
+
}
|
|
17
|
+
/** The function's request listener over a server entry and its inlined client manifests. */
|
|
18
|
+
export function create(entry, build, options = {}) {
|
|
19
|
+
const handler = assemble(entry, build, {
|
|
20
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
21
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
22
|
+
...(options.maxBody !== undefined ? { maxBody: options.maxBody } : {}),
|
|
23
|
+
...(options.csp !== undefined ? { csp: options.csp } : {}),
|
|
24
|
+
...(options.preload ? { preload: options.preload } : {}),
|
|
25
|
+
...(options.resolveLocale ? { resolveLocale: options.resolveLocale } : {}),
|
|
26
|
+
}, "vercel entry");
|
|
27
|
+
const { listener } = createListener({
|
|
28
|
+
handler,
|
|
29
|
+
// Only Vercel's proxy reaches the function: its `X-Forwarded-Proto` / `-Host` / `-For` (one hop) are the client's.
|
|
30
|
+
trustProxy: 1,
|
|
31
|
+
// A function instance has no idle phase worth shrinking the heap for.
|
|
32
|
+
idleMemory: false,
|
|
33
|
+
...(options.compress === false ? { compress: false } : {}),
|
|
34
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
35
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
36
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
37
|
+
});
|
|
38
|
+
if (!options.isr)
|
|
39
|
+
return listener;
|
|
40
|
+
return (req, res) => {
|
|
41
|
+
if (req.url?.includes(ISR_PATH))
|
|
42
|
+
req.url = isrURL(req.url);
|
|
43
|
+
listener(req, res);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { RouteTable } from "./routes.js";
|
|
2
|
+
import type { TransitionOptions } from "../client/transitions.js";
|
|
3
|
+
import type { TrailingSlash } from "../shared/slash.js";
|
|
4
|
+
import type { DocumentPrefetch, PrefetchEagerness } from "../shared/prefetch.js";
|
|
5
|
+
export interface CodegenOptions {
|
|
6
|
+
/** Routes directory relative to the project root, posix (e.g. `src/routes`). */
|
|
7
|
+
routesDir: string;
|
|
8
|
+
/** Output directory relative to the project root, posix (default `.rivet`). */
|
|
9
|
+
outDir?: string;
|
|
10
|
+
/** Root-relative module whose default export is the `DocumentShell`; re-exported by the server entry. */
|
|
11
|
+
document?: string;
|
|
12
|
+
/**
|
|
13
|
+
* `server` (default): client navigation uses the data endpoint. `static`: prerendered `_data.json`
|
|
14
|
+
* files. `csr`: no server; pages mount in the browser without loader data (`.server.ts` is an error).
|
|
15
|
+
*/
|
|
16
|
+
mode?: "server" | "static" | "csr";
|
|
17
|
+
/**
|
|
18
|
+
* `components`: component head (`@rivetjs/kit`): layouts and pages render under their `HeadLevel`,
|
|
19
|
+
* client navigation applies route heads, and the server entry hands the request's head sink to the
|
|
20
|
+
* app (the handler holds the document start until the shell rendered).
|
|
21
|
+
*/
|
|
22
|
+
head?: "components";
|
|
23
|
+
/** Route transitions + pending UI (`@rivetjs/router/transitions`, loaded on first navigation intent). */
|
|
24
|
+
transitions?: boolean | TransitionOptions;
|
|
25
|
+
/** Route announcer (a live region given each new page's title after client navigation). Default true. */
|
|
26
|
+
announce?: boolean;
|
|
27
|
+
/** URL base path (`/app`, Vite's `base`): route patterns of the generated files carry it (types do not). */
|
|
28
|
+
base?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The app reads search params (`useSearchParams()` / `SearchParams`): the server entry provides the
|
|
31
|
+
* request's and the resume entry the browser's. Off: none of it ships (the Vite plugin detects it).
|
|
32
|
+
*/
|
|
33
|
+
searchParams?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The app calls `navigate()` / `invalidate()` / `revalidate()` / `setSearchParams()` (or uses
|
|
36
|
+
* `@rivetjs/forms`): the App serves them (`rivet:go` listener, `data-rivet-go` on the routes element).
|
|
37
|
+
* Off: no listener, no resume state; calls load the document.
|
|
38
|
+
*/
|
|
39
|
+
navigate?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The app uses `@rivetjs/forms` (implies `navigate`): every router handler first installs the router brand
|
|
42
|
+
* (`_brand(_go)`, `Symbol.for("rivet.router")`) forms reach the router through. Off: no brand, no `_go`.
|
|
43
|
+
*/
|
|
44
|
+
forms?: boolean;
|
|
45
|
+
/** The app reads `useParams()`: pages and persisted layouts render under the params context. */
|
|
46
|
+
params?: boolean;
|
|
47
|
+
/** The app renders `<Link active>`: the App re-marks active links after each client navigation (`_markActive`). */
|
|
48
|
+
activeLinks?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Document prefetch (`@rivetjs/router/prefetch`): the server entry wraps the document shell so every
|
|
51
|
+
* page's head carries the Speculation Rules + inline fallback; a string is also the default for unmarked links.
|
|
52
|
+
*/
|
|
53
|
+
prefetch?: boolean | DocumentPrefetch;
|
|
54
|
+
/** Speculation Rules eagerness when `prefetch` is on (absent: `moderate`). */
|
|
55
|
+
prefetchEagerness?: PrefetchEagerness;
|
|
56
|
+
/** `false`: no route-path RegExp in the prefetch rules (only reload / external links take part). */
|
|
57
|
+
prefetchPaths?: boolean;
|
|
58
|
+
/** `"never"` / `"always"`: the server entry exports the policy (page requests in the other form answer 308). */
|
|
59
|
+
trailingSlash?: TrailingSlash;
|
|
60
|
+
/** `rivetRouter({ preload: { background } })`: the server entry exports it (the app's default background budget). */
|
|
61
|
+
preloadBackground?: number;
|
|
62
|
+
/** `rivetRouter({ routeRules })`: the server entry exports them (JSON data). */
|
|
63
|
+
routeRules?: Readonly<Record<string, unknown>>;
|
|
64
|
+
/**
|
|
65
|
+
* `rivetRouter({ clientRouter })`: `false` marks the App's router listeners `standalone: false` (alone
|
|
66
|
+
* they do not make a page resumable: inert pages ship zero JS). Default: `false` in `static` mode.
|
|
67
|
+
*/
|
|
68
|
+
clientRouter?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface Generated {
|
|
71
|
+
/** File name (relative to outDir) → contents. */
|
|
72
|
+
readonly files: Readonly<Record<string, string>>;
|
|
73
|
+
/** Root-relative modules that must compile in resume mode (generated app + route components). */
|
|
74
|
+
readonly resumeModules: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* RegExp source (no anchors, no leading `/`) over `URL.pathname.slice(1)` that matches every path one
|
|
78
|
+
* of `patterns` (route patterns: static segments, `:param`, terminal `*splat`; one optional trailing
|
|
79
|
+
* slash) matches. Shared prefixes are factored (a trie), so the source stays small for large apps.
|
|
80
|
+
* Valid in `u` / `v` mode and as a URLPattern regexp group (`/(source)`); `""`: only `/` matches.
|
|
81
|
+
*/
|
|
82
|
+
export declare function routePathRegExp(patterns: readonly string[]): string;
|
|
83
|
+
/** Generate the app, server entry, route table and route types for a scanned table. */
|
|
84
|
+
export declare function generate(table: RouteTable, options: CodegenOptions): Generated;
|