@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,39 @@
|
|
|
1
|
+
import type { AdapterOptions } from "./target-build.js";
|
|
2
|
+
export interface PackContext {
|
|
3
|
+
/** Project (Vite) root. */
|
|
4
|
+
readonly root: string;
|
|
5
|
+
/** Build output with `client/` and `server/`. */
|
|
6
|
+
readonly dist: string;
|
|
7
|
+
/** Server build directory (`dist/server`). */
|
|
8
|
+
readonly server: string;
|
|
9
|
+
/** URL base path (`""` or `/app`). */
|
|
10
|
+
readonly base: string;
|
|
11
|
+
/** `server`: a function renders requests; `static`: files only. */
|
|
12
|
+
readonly mode: "server" | "static";
|
|
13
|
+
}
|
|
14
|
+
/** One prerendered page: its canonical URL path and the file written for it (relative to the static root). */
|
|
15
|
+
export interface PackedPage {
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly file: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PackResult {
|
|
20
|
+
/** Directories written (absolute). */
|
|
21
|
+
readonly outputs: readonly string[];
|
|
22
|
+
readonly pages: readonly PackedPage[];
|
|
23
|
+
}
|
|
24
|
+
export declare const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
25
|
+
export declare function pack(options: AdapterOptions, context: PackContext): Promise<PackResult | undefined>;
|
|
26
|
+
/** URL path prefixes of content-hashed files (`<base>/assets/`, per locale with i18n). */
|
|
27
|
+
export declare function immutablePrefixes(base: string, locales: readonly string[] | undefined): string[];
|
|
28
|
+
/** Files below `dir`, as `/`-joined relative paths. */
|
|
29
|
+
export declare function listFiles(dir: string): Promise<string[]>;
|
|
30
|
+
export declare const NETLIFY_FUNCTION = "rivet-server";
|
|
31
|
+
/** Compatibility date of the generated Wrangler configuration. */
|
|
32
|
+
export declare const COMPATIBILITY_DATE = "2026-10-01";
|
|
33
|
+
/**
|
|
34
|
+
* From 2026-08-04 `nodejs_compat` (+ `_v2`) is on by default; turned off explicitly, `nodejs_als` keeps
|
|
35
|
+
* `node:async_hooks` (https://developers.cloudflare.com/workers/configuration/compatibility-flags/).
|
|
36
|
+
*/
|
|
37
|
+
export declare const WORKER_FLAGS: readonly ["nodejs_als", "no_nodejs_compat", "no_nodejs_compat_v2"];
|
|
38
|
+
/** JSONC → JSON: comments and trailing commas removed outside strings. */
|
|
39
|
+
export declare function stripJSONC(text: string): string;
|
package/adapters/pack.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// Packaging for platform targets (Node-only, build time): after `vite build --ssr` wrote
|
|
10
|
+
// `dist/server` (with the target's generated entry), lay the build out the way the platform
|
|
11
|
+
// deploys it. Static files are copied (never the `.vite/` manifests, which the bundle inlines);
|
|
12
|
+
// routes that `export const prerender = true` (every prerenderable route in `mode: "static"`) are
|
|
13
|
+
// rendered to HTML files through the built handler, with a hash CSP (`static.ts`).
|
|
14
|
+
//
|
|
15
|
+
// Platform contracts (verified 2026-10-08):
|
|
16
|
+
// - Vercel Build Output API v3: https://vercel.com/docs/build-output-api/v3 (primitives, configuration)
|
|
17
|
+
// - Netlify Frameworks API: https://docs.netlify.com/build/frameworks/frameworks-api/
|
|
18
|
+
// - Cloudflare Workers static assets: https://developers.cloudflare.com/workers/static-assets/ (+ `_headers`:
|
|
19
|
+
// https://developers.cloudflare.com/workers/static-assets/headers/), redirected Wrangler configuration:
|
|
20
|
+
// https://developers.cloudflare.com/workers/wrangler/configuration/#generated-wrangler-configuration
|
|
21
|
+
import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import { readManifests } from "./build.js";
|
|
26
|
+
import { assemble } from "./assemble.js";
|
|
27
|
+
import { prerender } from "./static.js";
|
|
28
|
+
import { loadServer } from "../server/module.js";
|
|
29
|
+
export const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
30
|
+
const GENERATOR = "@rivetjs/router";
|
|
31
|
+
export async function pack(options, context) {
|
|
32
|
+
switch (options.name) {
|
|
33
|
+
case "vercel":
|
|
34
|
+
return packVercel(options, context);
|
|
35
|
+
case "netlify":
|
|
36
|
+
return packNetlify(options, context);
|
|
37
|
+
case "cloudflare":
|
|
38
|
+
return packCloudflare(options, context);
|
|
39
|
+
default:
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** URL path prefixes of content-hashed files (`<base>/assets/`, per locale with i18n). */
|
|
44
|
+
export function immutablePrefixes(base, locales) {
|
|
45
|
+
return locales?.length ? locales.map((locale) => `${base}/${locale}/assets/`) : [`${base}/assets/`];
|
|
46
|
+
}
|
|
47
|
+
/** Copy the client build into `to`, without dot-named build metadata (`.vite/`); `.well-known/` stays. */
|
|
48
|
+
async function copyClient(dist, to) {
|
|
49
|
+
await mkdir(to, { recursive: true });
|
|
50
|
+
const from = resolve(dist, "client");
|
|
51
|
+
await cp(from, to, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
dereference: true,
|
|
54
|
+
filter: (source) => {
|
|
55
|
+
const name = basename(source);
|
|
56
|
+
return source === from || !name.startsWith(".") || name === ".well-known";
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Files below `dir`, as `/`-joined relative paths. */
|
|
61
|
+
export async function listFiles(dir) {
|
|
62
|
+
const entries = await readdir(dir, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
63
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => relative(dir, join(entry.parentPath, entry.name)).split("\\").join("/"));
|
|
64
|
+
}
|
|
65
|
+
const exists = (path) => stat(path).then(() => true, () => false);
|
|
66
|
+
/**
|
|
67
|
+
* Prerender into `staticRoot` (the directory served at `/`): every prerenderable route in static mode,
|
|
68
|
+
* routes exporting `prerender = true` in server mode (no 404 page: the function answers misses).
|
|
69
|
+
*/
|
|
70
|
+
async function prerenderInto(options, context, staticRoot, facts = {}) {
|
|
71
|
+
// Import a private copy of the server build: Node's module cache is keyed by path, so a later build in
|
|
72
|
+
// the same process (tests, build scripts, watch) would get the first build's modules (chunks import the
|
|
73
|
+
// entry back by path) and its deleted chunks. The bundle imports only built-ins, so any directory does.
|
|
74
|
+
const copy = await mkdtemp(join(tmpdir(), "rivet-pack-"));
|
|
75
|
+
try {
|
|
76
|
+
await cp(context.server, copy, { recursive: true });
|
|
77
|
+
const path = pathToFileURL(join(copy, "entry.server.js")).href;
|
|
78
|
+
const entry = (await import(__rewriteRelativeImportExtension(path, true)));
|
|
79
|
+
const manifests = await readManifests(context.dist, { ...(options.locales?.length ? { locales: options.locales } : {}), base: entry.base ?? "" });
|
|
80
|
+
facts.trailingSlash = entry.trailingSlash ?? "ignore";
|
|
81
|
+
return await prerenderWith(entry, assemble(entry, manifests, {}, path), options, context, staticRoot);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await rm(copy, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function prerenderWith(entry, handler, options, context, staticRoot) {
|
|
88
|
+
let routes = entry.routes;
|
|
89
|
+
if (context.mode === "server") {
|
|
90
|
+
const marked = await Promise.all(routes.map(async (route) => route.page && (await loadServer(route.server))?.prerender === true));
|
|
91
|
+
routes = routes.filter((_, i) => marked[i]);
|
|
92
|
+
if (!routes.length)
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const outDir = staticRoot + context.base;
|
|
96
|
+
const notFound = join(outDir, "404.html");
|
|
97
|
+
const kept404 = await exists(notFound);
|
|
98
|
+
const trailingSlash = entry.trailingSlash ?? "ignore";
|
|
99
|
+
const report = await prerender({
|
|
100
|
+
handler,
|
|
101
|
+
routes,
|
|
102
|
+
outDir,
|
|
103
|
+
trailingSlash,
|
|
104
|
+
...(context.base ? { base: context.base } : {}),
|
|
105
|
+
...(options.csp !== undefined ? { csp: options.csp } : {}),
|
|
106
|
+
});
|
|
107
|
+
if (context.mode === "server" && !kept404)
|
|
108
|
+
await rm(notFound, { force: true });
|
|
109
|
+
return report.pages.map((url) => ({ url, file: pageFile(url, context.base, trailingSlash) }));
|
|
110
|
+
}
|
|
111
|
+
/** The file `prerender` writes for a canonical page URL, relative to the static root. */
|
|
112
|
+
function pageFile(url, base, trailingSlash) {
|
|
113
|
+
const local = (base && url.startsWith(base) ? url.slice(base.length) : url) || "/";
|
|
114
|
+
const path = decodeURIComponent(local).replace(/\/+$/, "");
|
|
115
|
+
const file = trailingSlash === "never" && path ? path + ".html" : path + "/index.html";
|
|
116
|
+
return (base + file).replace(/^\/+/, "");
|
|
117
|
+
}
|
|
118
|
+
const json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
119
|
+
async function write(path, contents) {
|
|
120
|
+
await mkdir(dirname(path), { recursive: true });
|
|
121
|
+
await writeFile(path, contents);
|
|
122
|
+
}
|
|
123
|
+
/** PCRE-safe literal. */
|
|
124
|
+
const literal = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
125
|
+
// ---------------------------------------------------------------------------------------------
|
|
126
|
+
// Vercel: `.vercel/output/{config.json, static/, functions/_render.func/}`.
|
|
127
|
+
const VERCEL_FUNCTION = "_render";
|
|
128
|
+
async function packVercel(options, context) {
|
|
129
|
+
const out = resolve(context.root, ".vercel/output");
|
|
130
|
+
await rm(out, { recursive: true, force: true });
|
|
131
|
+
const staticRoot = join(out, "static");
|
|
132
|
+
await copyClient(context.dist, staticRoot + context.base);
|
|
133
|
+
const pages = await prerenderInto(options, context, staticRoot);
|
|
134
|
+
const server = context.mode === "server";
|
|
135
|
+
const isr = server && options.isr ? options.isr : null;
|
|
136
|
+
const routes = immutablePrefixes(context.base, options.locales).map((prefix) => ({
|
|
137
|
+
src: `^${literal(prefix)}.+$`,
|
|
138
|
+
headers: { "cache-control": IMMUTABLE },
|
|
139
|
+
continue: true,
|
|
140
|
+
}));
|
|
141
|
+
// Prerendered `a/index.html` / `a.html` answer at `/a` (Vercel's file system matches exact paths).
|
|
142
|
+
const overrides = {};
|
|
143
|
+
const isrRoutes = [];
|
|
144
|
+
if (server) {
|
|
145
|
+
const fn = join(out, "functions", VERCEL_FUNCTION + ".func");
|
|
146
|
+
await cp(context.server, fn, { recursive: true });
|
|
147
|
+
await write(join(fn, "package.json"), json({ type: "module" }));
|
|
148
|
+
const config = options.runtime === "edge"
|
|
149
|
+
? { runtime: "edge", entrypoint: "vercel.js", ...(options.regions ? { regions: options.regions } : {}) }
|
|
150
|
+
: {
|
|
151
|
+
runtime: `nodejs${options.nodeVersion ?? 24}.x`,
|
|
152
|
+
handler: "vercel.js",
|
|
153
|
+
launcherType: "Nodejs",
|
|
154
|
+
shouldAddHelpers: false,
|
|
155
|
+
supportsResponseStreaming: true,
|
|
156
|
+
...(options.regions ? { regions: options.regions } : {}),
|
|
157
|
+
...(options.maxDuration !== undefined ? { maxDuration: options.maxDuration } : {}),
|
|
158
|
+
...(options.memory !== undefined ? { memory: options.memory } : {}),
|
|
159
|
+
};
|
|
160
|
+
await write(join(fn, ".vc-config.json"), json(config));
|
|
161
|
+
}
|
|
162
|
+
for (const page of pages) {
|
|
163
|
+
const path = page.url.replace(/\/+$/, "") || "/";
|
|
164
|
+
if (isr) {
|
|
165
|
+
// ISR: a prerender function per page (symlink to the render function) instead of a file;
|
|
166
|
+
// the page path travels as `__rivet_path` (a prerender function's own path is its name).
|
|
167
|
+
await rm(join(staticRoot, page.file), { force: true });
|
|
168
|
+
const name = "_isr" + (path === "/" ? "/index" : path);
|
|
169
|
+
const link = join(out, "functions", name + ".func");
|
|
170
|
+
await mkdir(dirname(link), { recursive: true });
|
|
171
|
+
await symlink(relative(dirname(link), join(out, "functions", VERCEL_FUNCTION + ".func")), link, "dir");
|
|
172
|
+
await write(join(out, "functions", name + ".prerender-config.json"), json({ expiration: isr.expiration, allowQuery: ["__rivet_path", "_rivet"], passQuery: true }));
|
|
173
|
+
isrRoutes.push({ src: `^${literal(path === "/" ? "" : path)}/?$`, dest: `/${name}?__rivet_path=${encodeURIComponent(path)}` });
|
|
174
|
+
}
|
|
175
|
+
else if (page.file !== "index.html") {
|
|
176
|
+
overrides[page.file] = { path: page.file.replace(/(\/index)?\.html$/, "") };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
routes.push({ handle: "filesystem" });
|
|
180
|
+
routes.push(...isrRoutes);
|
|
181
|
+
if (server)
|
|
182
|
+
routes.push({ src: "^/.*$", dest: "/" + VERCEL_FUNCTION });
|
|
183
|
+
else if (await exists(join(staticRoot, context.base, "404.html")))
|
|
184
|
+
routes.push({ handle: "error" }, { status: 404, src: "^/.*$", dest: `${context.base}/404.html` });
|
|
185
|
+
await write(join(out, "config.json"), json({ version: 3, routes, ...(Object.keys(overrides).length ? { overrides } : {}) }));
|
|
186
|
+
return { outputs: [out], pages };
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------------------------------
|
|
189
|
+
// Netlify: publish dir `dist/netlify`, `.netlify/v1/{config.json, functions|edge-functions/rivet-server/}`.
|
|
190
|
+
export const NETLIFY_FUNCTION = "rivet-server";
|
|
191
|
+
async function packNetlify(options, context) {
|
|
192
|
+
const publish = resolve(context.dist, "netlify");
|
|
193
|
+
const v1 = resolve(context.root, ".netlify/v1");
|
|
194
|
+
await rm(publish, { recursive: true, force: true });
|
|
195
|
+
for (const dir of ["functions", "edge-functions"])
|
|
196
|
+
await rm(join(v1, dir, NETLIFY_FUNCTION), { recursive: true, force: true });
|
|
197
|
+
await copyClient(context.dist, publish + context.base);
|
|
198
|
+
const pages = await prerenderInto(options, context, publish);
|
|
199
|
+
const prefixes = immutablePrefixes(context.base, options.locales);
|
|
200
|
+
const headers = prefixes.map((prefix) => ({ for: prefix + "*", values: { "cache-control": IMMUTABLE } }));
|
|
201
|
+
if (context.mode === "server") {
|
|
202
|
+
const name = `"${NETLIFY_FUNCTION}"`;
|
|
203
|
+
if (options.edge) {
|
|
204
|
+
// Edge Functions run before static files: answer only paths with no file (`undefined` = continue).
|
|
205
|
+
const dir = join(v1, "edge-functions", NETLIFY_FUNCTION);
|
|
206
|
+
await cp(context.server, dir, { recursive: true });
|
|
207
|
+
const files = await listFiles(publish);
|
|
208
|
+
const paths = new Set();
|
|
209
|
+
for (const file of files) {
|
|
210
|
+
if (prefixes.some((prefix) => ("/" + file).startsWith(prefix)))
|
|
211
|
+
continue;
|
|
212
|
+
paths.add("/" + file);
|
|
213
|
+
const page = /^(.*?)(?:\/index)?\.html$/.exec(file);
|
|
214
|
+
if (page)
|
|
215
|
+
for (const path of ["/" + page[1], "/" + page[1] + "/"])
|
|
216
|
+
paths.add(path.replace(/^\/\/$/, "/"));
|
|
217
|
+
}
|
|
218
|
+
await write(join(dir, NETLIFY_FUNCTION + ".js"), [
|
|
219
|
+
`// Generated by ${GENERATOR}: Netlify Edge Function (Frameworks API). Do not edit.`,
|
|
220
|
+
'import { create } from "./netlify.js";',
|
|
221
|
+
`export default create({ staticPaths: ${JSON.stringify([...paths].sort())} });`,
|
|
222
|
+
`export const config = ${JSON.stringify({ path: "/*", excludedPath: prefixes.map((prefix) => prefix + "*"), name: "Rivet", generator: GENERATOR })};`,
|
|
223
|
+
"",
|
|
224
|
+
].join("\n"));
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const dir = join(v1, "functions", NETLIFY_FUNCTION);
|
|
228
|
+
await cp(context.server, dir, { recursive: true });
|
|
229
|
+
await write(join(dir, "package.json"), json({ type: "module" }));
|
|
230
|
+
const config = {
|
|
231
|
+
path: "/*",
|
|
232
|
+
preferStatic: true,
|
|
233
|
+
name: "Rivet",
|
|
234
|
+
generator: GENERATOR,
|
|
235
|
+
// Already bundled (`ssr.noExternal`): ship the directory as is.
|
|
236
|
+
nodeBundler: "none",
|
|
237
|
+
includedFiles: [relative(context.root, dir).split("\\").join("/") + "/**"],
|
|
238
|
+
};
|
|
239
|
+
await write(join(dir, NETLIFY_FUNCTION + ".mjs"), [
|
|
240
|
+
`// Generated by ${GENERATOR}: Netlify Function (Functions v2, Frameworks API). Do not edit.`,
|
|
241
|
+
'export { default } from "./netlify.js";',
|
|
242
|
+
`export const config = ${JSON.stringify(config)};`,
|
|
243
|
+
"",
|
|
244
|
+
].join("\n"));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
await write(join(v1, "config.json"), json({ headers }));
|
|
248
|
+
return { outputs: [publish, v1], pages };
|
|
249
|
+
}
|
|
250
|
+
// ---------------------------------------------------------------------------------------------
|
|
251
|
+
// Cloudflare Workers: `dist/cloudflare/{wrangler.json, static/}` + `.wrangler/deploy/config.json`.
|
|
252
|
+
/** Compatibility date of the generated Wrangler configuration. */
|
|
253
|
+
export const COMPATIBILITY_DATE = "2026-10-01";
|
|
254
|
+
/**
|
|
255
|
+
* From 2026-08-04 `nodejs_compat` (+ `_v2`) is on by default; turned off explicitly, `nodejs_als` keeps
|
|
256
|
+
* `node:async_hooks` (https://developers.cloudflare.com/workers/configuration/compatibility-flags/).
|
|
257
|
+
*/
|
|
258
|
+
export const WORKER_FLAGS = ["nodejs_als", "no_nodejs_compat", "no_nodejs_compat_v2"];
|
|
259
|
+
async function packCloudflare(options, context) {
|
|
260
|
+
const out = resolve(context.dist, "cloudflare");
|
|
261
|
+
await rm(out, { recursive: true, force: true });
|
|
262
|
+
const staticRoot = join(out, "static");
|
|
263
|
+
await copyClient(context.dist, staticRoot + context.base);
|
|
264
|
+
const facts = {};
|
|
265
|
+
const pages = await prerenderInto(options, context, staticRoot, facts);
|
|
266
|
+
const prefixes = immutablePrefixes(context.base, options.locales);
|
|
267
|
+
await write(join(staticRoot, "_headers"), prefixes.map((prefix) => `${prefix}*\n Cache-Control: ${IMMUTABLE}\n`).join(""));
|
|
268
|
+
const user = await userWrangler(context.root);
|
|
269
|
+
const server = context.mode === "server";
|
|
270
|
+
const rel = (path) => relative(out, path).split("\\").join("/");
|
|
271
|
+
const config = {
|
|
272
|
+
name: (await packageName(context.root)) ?? "rivet-app",
|
|
273
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
274
|
+
// AsyncLocalStorage only (resume state); no Node.js compatibility layer, so the renderer writes
|
|
275
|
+
// plain Uint8Array slabs (workerd's `Buffer#utf8Write` rejects the in-place writes Node accepts).
|
|
276
|
+
compatibility_flags: [...WORKER_FLAGS],
|
|
277
|
+
...user,
|
|
278
|
+
...options.wrangler,
|
|
279
|
+
...(server
|
|
280
|
+
? {
|
|
281
|
+
// The server build is already bundled (route modules are relative chunks): upload it as is.
|
|
282
|
+
main: rel(join(context.server, "worker.js")),
|
|
283
|
+
base_dir: rel(context.server),
|
|
284
|
+
no_bundle: true,
|
|
285
|
+
find_additional_modules: true,
|
|
286
|
+
rules: [{ type: "ESModule", globs: ["**/*.js"] }],
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
assets: {
|
|
290
|
+
...(user.assets ?? {}),
|
|
291
|
+
...(options.wrangler?.assets ?? {}),
|
|
292
|
+
directory: "./static",
|
|
293
|
+
// Prerendered `a/index.html` answers at the router's canonical URL (`/a`, or `/a/` with `trailingSlash: "always"`).
|
|
294
|
+
html_handling: facts.trailingSlash === "always" ? "force-trailing-slash" : "drop-trailing-slash",
|
|
295
|
+
...(server ? {} : { not_found_handling: "404-page" }),
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
if (!server)
|
|
299
|
+
delete config.main;
|
|
300
|
+
await write(join(out, "wrangler.json"), json(config));
|
|
301
|
+
const redirect = resolve(context.root, ".wrangler/deploy/config.json");
|
|
302
|
+
await write(redirect, json({ configPath: relative(dirname(redirect), join(out, "wrangler.json")).split("\\").join("/") }));
|
|
303
|
+
return { outputs: [out, dirname(redirect)], pages };
|
|
304
|
+
}
|
|
305
|
+
/** The project's own `wrangler.json` / `wrangler.jsonc` (bindings, vars…), merged under the generated config. */
|
|
306
|
+
async function userWrangler(root) {
|
|
307
|
+
for (const name of ["wrangler.jsonc", "wrangler.json"]) {
|
|
308
|
+
const text = await readFile(join(root, name), "utf8").catch(() => null);
|
|
309
|
+
if (text === null)
|
|
310
|
+
continue;
|
|
311
|
+
const config = JSON.parse(stripJSONC(text));
|
|
312
|
+
// Paths in it are relative to the project root; the generated file sets its own.
|
|
313
|
+
for (const key of ["main", "base_dir", "rules", "no_bundle", "find_additional_modules", "$schema"])
|
|
314
|
+
delete config[key];
|
|
315
|
+
return config;
|
|
316
|
+
}
|
|
317
|
+
if (await exists(join(root, "wrangler.toml")))
|
|
318
|
+
console.warn('rivetRouter({ adapter: "cloudflare" }): wrangler.toml is not merged into the generated configuration; use wrangler.jsonc or the adapter\'s `wrangler` option');
|
|
319
|
+
return {};
|
|
320
|
+
}
|
|
321
|
+
/** JSONC → JSON: comments and trailing commas removed outside strings. */
|
|
322
|
+
export function stripJSONC(text) {
|
|
323
|
+
let out = "";
|
|
324
|
+
for (let i = 0; i < text.length; i++) {
|
|
325
|
+
const char = text[i];
|
|
326
|
+
if (char === '"') {
|
|
327
|
+
let end = i + 1;
|
|
328
|
+
while (end < text.length && text[end] !== '"')
|
|
329
|
+
end += text[end] === "\\" ? 2 : 1;
|
|
330
|
+
out += text.slice(i, end + 1);
|
|
331
|
+
i = end;
|
|
332
|
+
}
|
|
333
|
+
else if (char === "/" && text[i + 1] === "/") {
|
|
334
|
+
while (i < text.length && text[i] !== "\n")
|
|
335
|
+
i++;
|
|
336
|
+
out += "\n";
|
|
337
|
+
}
|
|
338
|
+
else if (char === "/" && text[i + 1] === "*") {
|
|
339
|
+
const end = text.indexOf("*/", i + 2);
|
|
340
|
+
i = end < 0 ? text.length : end + 1;
|
|
341
|
+
}
|
|
342
|
+
else
|
|
343
|
+
out += char;
|
|
344
|
+
}
|
|
345
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
346
|
+
}
|
|
347
|
+
/** `package.json` name as a Worker name (lower-case letters, digits, dashes). */
|
|
348
|
+
async function packageName(root) {
|
|
349
|
+
const text = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
350
|
+
const name = text ? JSON.parse(text).name : undefined;
|
|
351
|
+
if (typeof name !== "string")
|
|
352
|
+
return undefined;
|
|
353
|
+
const cleaned = name
|
|
354
|
+
.replace(/^@[^/]+\//, "")
|
|
355
|
+
.toLowerCase()
|
|
356
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
357
|
+
.replace(/^-+|-+$/g, "")
|
|
358
|
+
.slice(0, 63);
|
|
359
|
+
return cleaned || undefined;
|
|
360
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BuildManifests } from "./assemble.js";
|
|
2
|
+
export interface ServerTargetOptions {
|
|
3
|
+
/** Client build directory (default: `../client/` next to the generated entry). */
|
|
4
|
+
client?: string | URL;
|
|
5
|
+
/** Port (default `$PORT`, else 3000). */
|
|
6
|
+
port?: number;
|
|
7
|
+
/** Address (default `$HOST`, else `0.0.0.0`: the platform's container network). */
|
|
8
|
+
hostname?: string;
|
|
9
|
+
/** Stop gracefully on SIGTERM / SIGINT (default true). */
|
|
10
|
+
signals?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function listenOptions({ port, hostname }: Pick<ServerTargetOptions, "port" | "hostname">): {
|
|
13
|
+
port: number;
|
|
14
|
+
hostname: string;
|
|
15
|
+
};
|
|
16
|
+
/** URL prefixes of hashed files: each client build's `assets/` (one per locale with i18n). */
|
|
17
|
+
export declare function immutableOf(base: string, build: BuildManifests): string[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const env = (name) => globalThis.process?.env?.[name];
|
|
2
|
+
export function listenOptions({ port, hostname }) {
|
|
3
|
+
return { port: port ?? Number(env("PORT") ?? 3000), hostname: hostname ?? env("HOST") ?? "0.0.0.0" };
|
|
4
|
+
}
|
|
5
|
+
/** URL prefixes of hashed files: each client build's `assets/` (one per locale with i18n). */
|
|
6
|
+
export function immutableOf(base, build) {
|
|
7
|
+
return build.clients.map((client) => (client.base || base + "/") + "assets/");
|
|
8
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AppHandler, AppRoute } from "../server/handler.js";
|
|
2
|
+
import type { AppConfig } from "../server/app.js";
|
|
3
|
+
import type { CSPOptions } from "../shared/csp.js";
|
|
4
|
+
export type { CSPDirectives, CSPOptions } from "../shared/csp.js";
|
|
5
|
+
import { type TrailingSlash } from "../shared/slash.js";
|
|
6
|
+
/** Options of `prerender()`. */
|
|
7
|
+
export interface PrerenderOptions {
|
|
8
|
+
handler: AppHandler;
|
|
9
|
+
routes: readonly AppRoute[];
|
|
10
|
+
outDir: string;
|
|
11
|
+
/**
|
|
12
|
+
* The site's origin (`https://example.com`), used for the synthetic page requests: absolute URLs a page builds from
|
|
13
|
+
* its request URL (canonical links, `og:url`) carry it. Resume state and `_data.json` hold same-origin URL values
|
|
14
|
+
* relative either way. Unset: requests use the placeholder `http://localhost`, and a page whose state or data still
|
|
15
|
+
* holds it (a URL string built from the request URL) is an error.
|
|
16
|
+
*/
|
|
17
|
+
origin?: string;
|
|
18
|
+
/** CSR shell for routes that can't be prerendered and have no server module; written to `200.html`. */
|
|
19
|
+
shell?: () => string | Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Static pages have no per-request nonce: each page gets a `<meta>` CSP allowing its own inline
|
|
22
|
+
* scripts by hash (`hashCSP`, given these options: `trustedTypes`, extra `directives` such as
|
|
23
|
+
* `{ "script-src": "'wasm-unsafe-eval'", "worker-src": "'self' blob:" }`). `false` leaves the pages
|
|
24
|
+
* as rendered (the host sets headers).
|
|
25
|
+
*/
|
|
26
|
+
csp?: false | CSPOptions;
|
|
27
|
+
/** URL base path the route patterns carry (`/app`): files are written relative to it (the host serves `outDir` under it). */
|
|
28
|
+
base?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The app's `trailingSlash` policy (`prerenderBuild` reads it from the build): pages are requested in
|
|
31
|
+
* canonical form and written as `a.html` (`"never"`, hosts serve it at `/a`) or `a/index.html`
|
|
32
|
+
* (`"always"`, `"ignore"`). `_data.json` stays at `a/_data.json` either way.
|
|
33
|
+
*/
|
|
34
|
+
trailingSlash?: TrailingSlash;
|
|
35
|
+
/**
|
|
36
|
+
* Zero-JS sites (no page resumes, no CSR shell route): delete the client build's scripts no page references
|
|
37
|
+
* (`outDir/.vite/manifest.json` chunks outside the static / dynamic import closure of the pages' `<script src>` and
|
|
38
|
+
* modulepreload links, and the resume loader's `*.deps.js` modules). `prerenderBuild` turns it on (`prune: false` keeps them).
|
|
39
|
+
*/
|
|
40
|
+
prune?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** What `prerender()` wrote: page paths and the routes served by the CSR shell. */
|
|
43
|
+
export interface PrerenderReport {
|
|
44
|
+
pages: string[];
|
|
45
|
+
/** Route ids served by the CSR shell. */
|
|
46
|
+
fallback: string[];
|
|
47
|
+
/** Some page resumes (ships resume state + loader); `false`: every page is zero-JS (no `_data.json` written). */
|
|
48
|
+
resumes: boolean;
|
|
49
|
+
/** Script files `prune` deleted (outDir-relative). */
|
|
50
|
+
pruned?: string[];
|
|
51
|
+
}
|
|
52
|
+
/** Fill a router pattern with params (`:name` encoded, `*name` encoded per segment). */
|
|
53
|
+
export declare function fillPattern(pattern: string, params: Record<string, string>): string;
|
|
54
|
+
/**
|
|
55
|
+
* Add a CSP `<meta>` to a static page: its inline scripts allowed by SHA-256 hash, script files from
|
|
56
|
+
* its own origin, no plugins, no `<base>`. (`'strict-dynamic'` would not do: browsers pass a nonce's
|
|
57
|
+
* trust on to `import()`, but not a hash's, and the resume loader imports its chunks.) Pages without
|
|
58
|
+
* `<head>` or without inline scripts (and no `options.directives`) are returned unchanged. The `<meta>` goes after a
|
|
59
|
+
* leading `<meta charset>` and `<meta name="viewport">` (the charset must stay in the first 1024 bytes, first).
|
|
60
|
+
* `options.directives` merge in (`cspPolicy`): e.g. `{ "script-src": "'wasm-unsafe-eval'", "worker-src": "'self' blob:" }`;
|
|
61
|
+
* header-only directives (`frame-ancestors`, `report-uri`, `report-to`, `sandbox`) throw.
|
|
62
|
+
*/
|
|
63
|
+
export declare function hashCSP(html: string, options?: CSPOptions): string;
|
|
64
|
+
/**
|
|
65
|
+
* Prerender every eligible route. Non-prerenderable routes with server modules are errors, and so are two
|
|
66
|
+
* pages whose paths differ only in letter case (checked before the second one is written).
|
|
67
|
+
*/
|
|
68
|
+
export declare function prerender(options: PrerenderOptions): Promise<PrerenderReport>;
|
|
69
|
+
/**
|
|
70
|
+
* Prerender a production build (`vite build` + `vite build --ssr`, router `mode: "static"`) into its
|
|
71
|
+
* client directory, so `<dist>/client` is the deployable site.
|
|
72
|
+
*/
|
|
73
|
+
export declare function prerenderBuild(options?: {
|
|
74
|
+
dist?: string;
|
|
75
|
+
origin?: string;
|
|
76
|
+
shell?: PrerenderOptions["shell"];
|
|
77
|
+
csp?: PrerenderOptions["csp"];
|
|
78
|
+
/** Preload planner (`AppConfig.preload`): background prefetch byte budget, `lean` pages (nothing before an interaction). */
|
|
79
|
+
preload?: AppConfig["preload"];
|
|
80
|
+
/** i18n: per-locale client builds (see `loadBuild`). */
|
|
81
|
+
locales?: readonly string[];
|
|
82
|
+
/** `PrerenderOptions.prune` (default true): a zero-JS site keeps no unreferenced scripts. */
|
|
83
|
+
prune?: boolean;
|
|
84
|
+
}): Promise<PrerenderReport>;
|