@rivetjs/router 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +20 -0
- package/adapters/files.js +155 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +180 -0
- package/adapters/node.js +986 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +95 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +35 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +97 -0
- package/build/codegen.js +1037 -0
- package/build/dev-errors.d.ts +23 -0
- package/build/dev-errors.js +247 -0
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +31 -0
- package/build/migrate.js +385 -0
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +55 -0
- package/build/routes.js +219 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +208 -0
- package/build/vite.js +861 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +101 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +91 -0
- package/client/index.d.ts +156 -0
- package/client/index.js +714 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +135 -0
- package/index.js +8 -0
- package/package.json +156 -4
- package/server/app.d.ts +175 -0
- package/server/app.js +335 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +240 -0
- package/server/handler.js +661 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +96 -0
- package/server/render.d.ts +96 -0
- package/server/render.js +492 -0
- package/server/route-cache.d.ts +245 -0
- package/server/route-cache.js +813 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denys Severyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rivetjs/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
File routing for Rivet: loaders, actions, guards, a web-standard request handler, Node / static / edge adapters.
|
|
4
|
+
|
|
5
|
+
Part of Rivet, a compiled, resumable JSX framework. Start a project with `npm create rivet@latest`.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @rivetjs/router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Source: https://github.com/N1XUS/rivetjs/tree/main/libs/router · Docs: https://github.com/N1XUS/rivetjs/tree/main/apps/docs/content · Issues: https://github.com/N1XUS/rivetjs/issues
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResumeManifest } from "@rivetjs/runtime/server-resume";
|
|
2
|
+
import type { InlineStyleManifest } from "../server/inline-styles.js";
|
|
3
|
+
import type { AppConfig, GeneratedEntry } from "../server/app.js";
|
|
4
|
+
import type { AppHandler } from "../server/handler.js";
|
|
5
|
+
export interface ViteManifestRecord {
|
|
6
|
+
file: string;
|
|
7
|
+
src?: string;
|
|
8
|
+
isEntry?: boolean;
|
|
9
|
+
css?: string[];
|
|
10
|
+
}
|
|
11
|
+
/** One client build: URL base, resume manifest and Vite manifest. */
|
|
12
|
+
export interface ClientBuild {
|
|
13
|
+
readonly base: string;
|
|
14
|
+
readonly manifest: ResumeManifest;
|
|
15
|
+
readonly vite: Readonly<Record<string, ViteManifestRecord>>;
|
|
16
|
+
}
|
|
17
|
+
/** A client build's manifests (one per locale with i18n), as `loadBuild` reads and `fetch.js` inlines them. */
|
|
18
|
+
export interface BuildManifests {
|
|
19
|
+
readonly clients: readonly ClientBuild[];
|
|
20
|
+
/** i18n locales in compiler variant order, one client build each. */
|
|
21
|
+
readonly locales?: readonly string[];
|
|
22
|
+
/** `.vite/rivet-route-css.json` of the first client build, when written. */
|
|
23
|
+
readonly routeStyles?: Readonly<Record<string, string[]>>;
|
|
24
|
+
/** `.vite/rivet-inline-css.json` of the first client build (the sheets pages may write inline), when written. */
|
|
25
|
+
readonly inlineStyles?: InlineStyleManifest;
|
|
26
|
+
}
|
|
27
|
+
export type BuildConfig = Pick<AppConfig, "stream" | "onError" | "maxBody" | "csp" | "preload" | "routeRules" | "routeCache" | "renderCache" | "earlyHints"> & {
|
|
28
|
+
/** i18n: error-page locale when the request has none settled (`createApp` `i18n.resolve`). */
|
|
29
|
+
readonly resolveLocale?: (request: Request) => string | undefined | Promise<string | undefined>;
|
|
30
|
+
};
|
|
31
|
+
/** Create the app handler from the server bundle's own `createApp` (one runtime instance). */
|
|
32
|
+
export declare function assemble(entry: GeneratedEntry, build: BuildManifests, config?: BuildConfig, where?: string): AppHandler;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Create the app handler from the server bundle's own `createApp` (one runtime instance). */
|
|
2
|
+
export function assemble(entry, build, config = {}, where = "server entry") {
|
|
3
|
+
const { clients, locales, routeStyles, inlineStyles } = build;
|
|
4
|
+
const { resolveLocale, ...app } = config;
|
|
5
|
+
const first = clients[0];
|
|
6
|
+
const styles = Object.values(first.vite)
|
|
7
|
+
// App entries only (router `styles`, CSR app): resume chunks are entries too, their CSS is linked per route.
|
|
8
|
+
.filter((record) => record.isEntry && !record.src?.includes("rivet-resume-chunk:"))
|
|
9
|
+
.flatMap((record) => (record.file.endsWith(".css") ? [record.file] : (record.css ?? [])))
|
|
10
|
+
.map((file) => first.base + file);
|
|
11
|
+
const createApp = entry.createApp;
|
|
12
|
+
if (!createApp)
|
|
13
|
+
throw new TypeError(`${where} does not export createApp`);
|
|
14
|
+
const i18n = locales?.length
|
|
15
|
+
? {
|
|
16
|
+
i18n: {
|
|
17
|
+
locales,
|
|
18
|
+
manifests: Object.fromEntries(locales.map((locale, i) => [locale, clients[i].manifest])),
|
|
19
|
+
...(resolveLocale ? { resolve: resolveLocale } : {}),
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
: {};
|
|
23
|
+
return createApp({
|
|
24
|
+
...app,
|
|
25
|
+
entry,
|
|
26
|
+
manifest: first.manifest,
|
|
27
|
+
styles: [...new Set(styles)],
|
|
28
|
+
...(routeStyles ? { routeStyles } : {}),
|
|
29
|
+
...(inlineStyles ? { inlineStyles } : {}),
|
|
30
|
+
...i18n,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AppConfig, GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { AppHandler } from "../server/handler.js";
|
|
3
|
+
import { type BuildManifests } from "./assemble.js";
|
|
4
|
+
export interface LoadedBuild {
|
|
5
|
+
readonly entry: GeneratedEntry;
|
|
6
|
+
readonly handler: AppHandler;
|
|
7
|
+
/** Built client directory (`<dist>/client`). */
|
|
8
|
+
readonly client: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LoadBuildOptions {
|
|
11
|
+
/**
|
|
12
|
+
* i18n: one client build per locale in `client/<locale>/` (base `/<locale>/`), in compiler variant
|
|
13
|
+
* order. Pages use their locale's resume manifest; stylesheets come from the first locale's build.
|
|
14
|
+
*/
|
|
15
|
+
readonly locales?: readonly string[];
|
|
16
|
+
/** i18n: error-page locale when the request has none settled (e.g. `errorLocale(config)` from `@rivetjs/i18n/router`). */
|
|
17
|
+
readonly resolveLocale?: (request: Request) => string | undefined | Promise<string | undefined>;
|
|
18
|
+
/** URL base path of the client build (`/app`); `loadBuild` takes it from the server entry. */
|
|
19
|
+
readonly base?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Read the client build manifests of `<dist>/client` (per locale with i18n). */
|
|
22
|
+
export declare function readManifests(dist: string, { locales, base }?: LoadBuildOptions): Promise<BuildManifests>;
|
|
23
|
+
/** Import `<dist>/server/entry.server.js` and wire it to the client build's resume manifest and stylesheets. */
|
|
24
|
+
export declare function loadBuild(dist: string, config?: Pick<AppConfig, "stream" | "onError" | "maxBody" | "csp" | "preload" | "routeRules" | "routeCache" | "renderCache" | "earlyHints">, options?: LoadBuildOptions): Promise<LoadedBuild>;
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
// Load a production build (`vite build` + `vite build --ssr`) into an app handler.
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { assemble } from "./assemble.js";
|
|
14
|
+
/** Read the client build manifests of `<dist>/client` (per locale with i18n). */
|
|
15
|
+
export async function readManifests(dist, { locales, base = "" } = {}) {
|
|
16
|
+
const root = resolve(dist);
|
|
17
|
+
const read = async (path) => JSON.parse(await readFile(resolve(root, path), "utf8"));
|
|
18
|
+
const dirs = locales?.length
|
|
19
|
+
? locales.map((locale) => ({ dir: "client/" + locale + "/", base: base + "/" + locale + "/" }))
|
|
20
|
+
: [{ dir: "client/", base: base + "/" }];
|
|
21
|
+
const clients = await Promise.all(dirs.map(async ({ dir, base }) => {
|
|
22
|
+
const [manifest, vite] = await Promise.all([
|
|
23
|
+
read(dir + ".vite/rivet-resume.json"),
|
|
24
|
+
read(dir + ".vite/manifest.json"),
|
|
25
|
+
]);
|
|
26
|
+
return { base, manifest, vite };
|
|
27
|
+
}));
|
|
28
|
+
// Per-route CSS (route modules' stylesheets), when the client build wrote any.
|
|
29
|
+
const routeStyles = await read(dirs[0].dir + ".vite/rivet-route-css.json").catch(() => undefined);
|
|
30
|
+
// Small stylesheets written inline (`rivetRouter({ inlineStylesheets })`), when the client build wrote any.
|
|
31
|
+
const inlineStyles = await read(dirs[0].dir + ".vite/rivet-inline-css.json").catch(() => undefined);
|
|
32
|
+
return { clients, ...(locales?.length ? { locales } : {}), ...(routeStyles ? { routeStyles } : {}), ...(inlineStyles ? { inlineStyles } : {}) };
|
|
33
|
+
}
|
|
34
|
+
/** Import `<dist>/server/entry.server.js` and wire it to the client build's resume manifest and stylesheets. */
|
|
35
|
+
export async function loadBuild(dist, config = {}, options = {}) {
|
|
36
|
+
const root = resolve(dist);
|
|
37
|
+
const path = resolve(root, "server/entry.server.js");
|
|
38
|
+
const entry = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href, true)));
|
|
39
|
+
const manifests = await readManifests(root, { ...options, base: options.base ?? entry.base ?? "" });
|
|
40
|
+
// The server bundle carries its own `createApp`: the handler must share the app's runtime instance.
|
|
41
|
+
const handler = assemble(entry, manifests, { ...config, ...(options.resolveLocale ? { resolveLocale: options.resolveLocale } : {}) }, path);
|
|
42
|
+
return { entry, handler, client: resolve(root, "client") };
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { BuildManifests } from "./assemble.js";
|
|
3
|
+
import { type FetchOptions } from "./fetch.js";
|
|
4
|
+
import { type ServerTargetOptions } from "./server-target.js";
|
|
5
|
+
interface BunServer {
|
|
6
|
+
readonly port: number;
|
|
7
|
+
readonly hostname: string;
|
|
8
|
+
readonly url: URL;
|
|
9
|
+
requestIP(request: Request): {
|
|
10
|
+
address: string;
|
|
11
|
+
} | null;
|
|
12
|
+
stop(force?: boolean): Promise<void> | void;
|
|
13
|
+
}
|
|
14
|
+
export interface BunOptions extends Omit<FetchOptions, "serveAssets">, ServerTargetOptions {
|
|
15
|
+
}
|
|
16
|
+
/** Start `Bun.serve`; stops (in-flight requests finish) on SIGTERM / SIGINT. */
|
|
17
|
+
export declare function start(entry: GeneratedEntry, build: BuildManifests, options?: BunOptions): BunServer;
|
|
18
|
+
export {};
|
package/adapters/bun.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Bun server (`rivetRouter({ adapter: "bun" })` → `bun dist/server/bun.js`): `Bun.serve`
|
|
2
|
+
// (https://bun.com/docs/api/http) with the built client files first (`files.ts`, sent as `Bun.file`,
|
|
3
|
+
// zero-copy) and the app through the fetch adapter (web-standard Response bodies, Bun's native
|
|
4
|
+
// streams). The Bun `Server` reaches loaders as `event.platform.ctx`; environment: `process.env` /
|
|
5
|
+
// `Bun.env`. Bun.serve does not compress: precompressed `.br` / `.gz` siblings are served for files,
|
|
6
|
+
// pages go out uncompressed (put a compressing proxy / CDN in front for HTML).
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createFetch } from "./fetch.js";
|
|
9
|
+
import { staticFiles } from "./files.js";
|
|
10
|
+
import { immutableOf, listenOptions } from "./server-target.js";
|
|
11
|
+
/** Start `Bun.serve`; stops (in-flight requests finish) on SIGTERM / SIGINT. */
|
|
12
|
+
export function start(entry, build, options = {}) {
|
|
13
|
+
const Bun = globalThis.Bun;
|
|
14
|
+
if (!Bun)
|
|
15
|
+
throw new Error('rivetRouter({ adapter: "bun" }): dist/server/bun.js runs under Bun (bun dist/server/bun.js)');
|
|
16
|
+
const { client, port, hostname, signals, ...fetchOptions } = options;
|
|
17
|
+
const base = entry.base ?? "";
|
|
18
|
+
const lookup = client ? staticFiles(typeof client === "string" ? client : fileURLToPath(client), base, immutableOf(base, build)) : () => null;
|
|
19
|
+
const app = createFetch(entry, build, { address: (request, _env, server) => server.requestIP(request)?.address, ...fetchOptions });
|
|
20
|
+
const listen = listenOptions({ port, hostname });
|
|
21
|
+
const server = Bun.serve({
|
|
22
|
+
...listen,
|
|
23
|
+
fetch(request, server) {
|
|
24
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
25
|
+
const file = lookup(request, new URL(request.url).pathname);
|
|
26
|
+
if (file === 304)
|
|
27
|
+
return new Response(null, { status: 304 });
|
|
28
|
+
if (file)
|
|
29
|
+
return new Response(request.method === "HEAD" ? null : Bun.file(file.path), { headers: file.headers });
|
|
30
|
+
}
|
|
31
|
+
return app.fetch(request, undefined, server);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (signals !== false) {
|
|
35
|
+
const stop = async () => {
|
|
36
|
+
await server.stop();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
};
|
|
39
|
+
for (const signal of ["SIGTERM", "SIGINT"])
|
|
40
|
+
process.once(signal, stop);
|
|
41
|
+
}
|
|
42
|
+
console.log(`Listening on ${server.url.href}`);
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { BuildManifests } from "./assemble.js";
|
|
3
|
+
import { type FetchModule, type FetchOptions } from "./fetch.js";
|
|
4
|
+
export type CloudflareOptions = FetchOptions;
|
|
5
|
+
export declare function create(entry: GeneratedEntry, build: BuildManifests, options?: CloudflareOptions): FetchModule;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createFetch } from "./fetch.js";
|
|
2
|
+
export function create(entry, build, options = {}) {
|
|
3
|
+
return createFetch(entry, build, {
|
|
4
|
+
// Cloudflare sets it to the connecting client's address (clients cannot override it).
|
|
5
|
+
address: (request) => request.headers.get("cf-connecting-ip") ?? undefined,
|
|
6
|
+
...options,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PrimaryServing, ServerServing } from "./node.js";
|
|
2
|
+
/** In a worker: `run()` (one-worker `start`), then IPC: relay `revalidate()`, drain + exit on the primary's close. */
|
|
3
|
+
export declare function worker(run: () => Promise<ServerServing>): Promise<ServerServing>;
|
|
4
|
+
/** In the primary: fork `count` workers, restart crashed ones, relay revalidations; resolves once all listen. */
|
|
5
|
+
export declare function primary(count: number, grace: number, log: (error: unknown) => void): Promise<PrimaryServing>;
|
|
6
|
+
/** `cluster.isPrimary` (true outside a cluster). */
|
|
7
|
+
export declare const isPrimary: () => boolean;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Multi-core serving for the node adapter (`start({ workers })`, loaded only when `workers` > 1): the primary forks
|
|
2
|
+
// `workers` processes running the same script; each worker runs `start()` with one worker, and the primary hands out
|
|
3
|
+
// connections (node:cluster round robin) or, with `reusePort`, each worker binds its own socket and the kernel
|
|
4
|
+
// balances (Linux / FreeBSD). The primary loads no build and renders nothing. `revalidate()` reaches every worker:
|
|
5
|
+
// the worker that calls it drops its own pages and the primary relays the target to the others (IPC).
|
|
6
|
+
import cluster, {} from "node:cluster";
|
|
7
|
+
// `server/route-cache.ts`: the local drop (registered at module load) and the hook its `revalidate()` calls after it.
|
|
8
|
+
const LOCAL = Symbol.for("rivet.revalidate.local");
|
|
9
|
+
const BROADCAST = Symbol.for("rivet.revalidate.broadcast");
|
|
10
|
+
/** A `revalidate()` target from IPC: `{ path?: string, tag?: string }`, at least one. */
|
|
11
|
+
const targetOf = (value) => {
|
|
12
|
+
if (value === null || typeof value !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const { path, tag } = value;
|
|
15
|
+
if ((path !== undefined && typeof path !== "string") || (tag !== undefined && typeof tag !== "string") || (path === undefined && tag === undefined))
|
|
16
|
+
return undefined;
|
|
17
|
+
return { path: path, tag: tag };
|
|
18
|
+
};
|
|
19
|
+
const global = globalThis;
|
|
20
|
+
/** In a worker: `run()` (one-worker `start`), then IPC: relay `revalidate()`, drain + exit on the primary's close. */
|
|
21
|
+
export async function worker(run) {
|
|
22
|
+
const serving = await run();
|
|
23
|
+
// The local drop already happened: a closed IPC channel only means the other workers keep theirs.
|
|
24
|
+
global[BROADCAST] = (target) => {
|
|
25
|
+
if (process.connected)
|
|
26
|
+
process.send?.({ type: "rivet:revalidate", target }, undefined, undefined, () => { });
|
|
27
|
+
};
|
|
28
|
+
const stop = () => void serving.close().then(() => process.exit(0), () => process.exit(1));
|
|
29
|
+
process.on("message", (message) => {
|
|
30
|
+
if (message?.type === "rivet:revalidate") {
|
|
31
|
+
const target = targetOf(message.target);
|
|
32
|
+
if (target)
|
|
33
|
+
try {
|
|
34
|
+
global[LOCAL]?.(target);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// A malformed path: nothing to drop here.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (message?.type === "rivet:close")
|
|
41
|
+
stop();
|
|
42
|
+
});
|
|
43
|
+
// The primary went away (crash, kill -9): finish what is in flight, then exit.
|
|
44
|
+
process.once("disconnect", stop);
|
|
45
|
+
return serving;
|
|
46
|
+
}
|
|
47
|
+
/** In the primary: fork `count` workers, restart crashed ones, relay revalidations; resolves once all listen. */
|
|
48
|
+
export function primary(count, grace, log) {
|
|
49
|
+
let closing = false;
|
|
50
|
+
// Replacements only once every first worker listens: a worker failing at start rejects `start()` instead.
|
|
51
|
+
let started = false;
|
|
52
|
+
const crashes = [];
|
|
53
|
+
const live = new Set();
|
|
54
|
+
let url = "";
|
|
55
|
+
const fork = () => {
|
|
56
|
+
const child = cluster.fork();
|
|
57
|
+
live.add(child);
|
|
58
|
+
child.on("message", (message) => {
|
|
59
|
+
if (message?.type !== "rivet:revalidate")
|
|
60
|
+
return;
|
|
61
|
+
const target = targetOf(message.target);
|
|
62
|
+
if (!target)
|
|
63
|
+
return;
|
|
64
|
+
for (const other of live)
|
|
65
|
+
if (other !== child && other.isConnected())
|
|
66
|
+
other.send({ type: "rivet:revalidate", target }, undefined, () => { });
|
|
67
|
+
});
|
|
68
|
+
child.once("exit", (code, signal) => {
|
|
69
|
+
live.delete(child);
|
|
70
|
+
if (closing || !started)
|
|
71
|
+
return;
|
|
72
|
+
// A crashed worker is replaced, unless workers keep crashing (5 in 10 s): then the primary stops forking.
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
crashes.push(now);
|
|
75
|
+
while (crashes[0] < now - 10_000)
|
|
76
|
+
crashes.shift();
|
|
77
|
+
if (crashes.length > 5) {
|
|
78
|
+
log(new Error(`rivet: workers keep exiting (last: ${signal ?? code}); not restarting`));
|
|
79
|
+
// Nothing serves any more: exit, so the process manager / orchestrator restarts the whole server.
|
|
80
|
+
if (live.size === 0)
|
|
81
|
+
process.exit(1);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
log(new Error(`rivet: worker ${child.process.pid} exited (${signal ?? code}); starting another`));
|
|
85
|
+
void fork().catch(log);
|
|
86
|
+
});
|
|
87
|
+
return new Promise((done, fail) => {
|
|
88
|
+
child.once("listening", (address) => {
|
|
89
|
+
const host = address.address === undefined || address.address === null ? "127.0.0.1" : address.addressType === 6 ? `[${address.address}]` : address.address;
|
|
90
|
+
url ||= `http://${host}:${address.port}`;
|
|
91
|
+
done();
|
|
92
|
+
});
|
|
93
|
+
child.once("exit", (code) => fail(new Error(`rivet: worker exited (${code}) before listening`)));
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
const stopAll = () => {
|
|
97
|
+
closing = true;
|
|
98
|
+
for (const child of live)
|
|
99
|
+
child.process.kill("SIGTERM");
|
|
100
|
+
};
|
|
101
|
+
return Promise.all(Array.from({ length: count }, fork)).then(() => {
|
|
102
|
+
started = true;
|
|
103
|
+
let closed = null;
|
|
104
|
+
return {
|
|
105
|
+
url,
|
|
106
|
+
workers: count,
|
|
107
|
+
close: () => (closed ??= (async () => {
|
|
108
|
+
closing = true;
|
|
109
|
+
await Promise.all([...live].map((child) => new Promise((done) => {
|
|
110
|
+
if (child.isDead())
|
|
111
|
+
return done();
|
|
112
|
+
child.once("exit", () => done());
|
|
113
|
+
// Drain (the worker's own `grace`), then force.
|
|
114
|
+
const force = setTimeout(() => child.process.kill("SIGKILL"), grace + 2_000);
|
|
115
|
+
force.unref();
|
|
116
|
+
if (child.isConnected())
|
|
117
|
+
child.send({ type: "rivet:close" });
|
|
118
|
+
else
|
|
119
|
+
child.process.kill("SIGTERM");
|
|
120
|
+
})));
|
|
121
|
+
})()),
|
|
122
|
+
};
|
|
123
|
+
}, (error) => {
|
|
124
|
+
// A worker failed before listening (bad build, port taken): stop the others, `start()` rejects.
|
|
125
|
+
stopAll();
|
|
126
|
+
throw error;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** `cluster.isPrimary` (true outside a cluster). */
|
|
130
|
+
export const isPrimary = () => cluster.isPrimary;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DocumentShell } from "../server/render.js";
|
|
2
|
+
/** Options of the CSR `index.html` shell. */
|
|
3
|
+
export interface ShellOptions {
|
|
4
|
+
/** Module script URL of the client entry. */
|
|
5
|
+
script: string;
|
|
6
|
+
/** Stylesheet URLs. */
|
|
7
|
+
styles?: readonly string[];
|
|
8
|
+
/** URL the shell is built for (the document sees it; default `http://localhost/`). */
|
|
9
|
+
url?: URL;
|
|
10
|
+
}
|
|
11
|
+
/** `index.html` for a CSR app from its document shell: the app mounts into the shell's container. */
|
|
12
|
+
export declare function shellHTML(document: DocumentShell, { script, styles, url }: ShellOptions): string;
|
package/adapters/csr.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// CSR adapter: a single-page shell that mounts the app in the browser. No server, so route
|
|
2
|
+
// modules have no `.server.ts` (codegen rejects them) and pages render without loader data.
|
|
3
|
+
import { escapeAttribute } from "@rivetjs/runtime/escape";
|
|
4
|
+
/** `index.html` for a CSR app from its document shell: the app mounts into the shell's container. */
|
|
5
|
+
export function shellHTML(document, { script, styles = [], url = new URL("http://localhost/") }) {
|
|
6
|
+
const { start, end } = document({ head: undefined, nonce: undefined, url, status: 200, styles });
|
|
7
|
+
const tag = `<script type="module" src="${escapeAttribute(script)}"></script>`;
|
|
8
|
+
if (!start.includes("</head>"))
|
|
9
|
+
throw new Error("CSR shell: the document start has no </head>");
|
|
10
|
+
return start.replace("</head>", tag + "</head>") + end;
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { BuildManifests } from "./assemble.js";
|
|
3
|
+
import { type FetchOptions } from "./fetch.js";
|
|
4
|
+
import { type ServerTargetOptions } from "./server-target.js";
|
|
5
|
+
interface DenoServer {
|
|
6
|
+
readonly finished: Promise<void>;
|
|
7
|
+
shutdown(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface DenoOptions extends Omit<FetchOptions, "serveAssets">, ServerTargetOptions {
|
|
10
|
+
}
|
|
11
|
+
/** Start `Deno.serve`; shuts down (in-flight requests finish) on SIGTERM / SIGINT. */
|
|
12
|
+
export declare function start(entry: GeneratedEntry, build: BuildManifests, options?: DenoOptions): DenoServer;
|
|
13
|
+
export {};
|
package/adapters/deno.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Deno server (`rivetRouter({ adapter: "deno" })` → `deno run --allow-net --allow-read --allow-env dist/server/deno.js`):
|
|
2
|
+
// `Deno.serve` (https://docs.deno.com/runtime/fundamentals/http_server/) with the built client files
|
|
3
|
+
// first (`files.ts`, streamed from `Deno.open`) and the app through the fetch adapter (Uint8Array
|
|
4
|
+
// web streams). Deno compresses compressible responses itself (gzip / brotli by `Accept-Encoding`).
|
|
5
|
+
// The handler's `info` (`remoteAddr`) reaches loaders as `event.platform.ctx`; environment: `Deno.env.get()`.
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createFetch } from "./fetch.js";
|
|
8
|
+
import { staticFiles } from "./files.js";
|
|
9
|
+
import { immutableOf, listenOptions } from "./server-target.js";
|
|
10
|
+
/** Start `Deno.serve`; shuts down (in-flight requests finish) on SIGTERM / SIGINT. */
|
|
11
|
+
export function start(entry, build, options = {}) {
|
|
12
|
+
const Deno = globalThis.Deno;
|
|
13
|
+
if (!Deno)
|
|
14
|
+
throw new Error('rivetRouter({ adapter: "deno" }): dist/server/deno.js runs under Deno (deno run -A dist/server/deno.js)');
|
|
15
|
+
const { client, port, hostname, signals, ...fetchOptions } = options;
|
|
16
|
+
const base = entry.base ?? "";
|
|
17
|
+
const lookup = client ? staticFiles(typeof client === "string" ? client : fileURLToPath(client), base, immutableOf(base, build)) : () => null;
|
|
18
|
+
const app = createFetch(entry, build, { address: (_request, _env, info) => info.remoteAddr.hostname, ...fetchOptions });
|
|
19
|
+
const server = Deno.serve({ ...listenOptions({ port, hostname }), onListen: ({ hostname, port }) => console.log(`Listening on http://${hostname}:${port}/`) }, async (request, info) => {
|
|
20
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
21
|
+
const file = lookup(request, new URL(request.url).pathname);
|
|
22
|
+
if (file === 304)
|
|
23
|
+
return new Response(null, { status: 304 });
|
|
24
|
+
if (file)
|
|
25
|
+
return new Response(request.method === "HEAD" ? null : (await Deno.open(file.path)).readable, { headers: file.headers });
|
|
26
|
+
}
|
|
27
|
+
return app.fetch(request, undefined, info);
|
|
28
|
+
});
|
|
29
|
+
if (signals !== false) {
|
|
30
|
+
const stop = () => void server.shutdown().then(() => Deno.exit(0));
|
|
31
|
+
for (const signal of ["SIGTERM", "SIGINT"])
|
|
32
|
+
Deno.addSignalListener(signal, stop);
|
|
33
|
+
}
|
|
34
|
+
return server;
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AppConfig, GeneratedEntry } from "../server/app.js";
|
|
2
|
+
import type { BodyLimit } from "../server/security.js";
|
|
3
|
+
import type { ErrorInfo } from "../server/handler.js";
|
|
4
|
+
import { type BuildManifests } from "./assemble.js";
|
|
5
|
+
import { nonce } from "../shared/nonce.js";
|
|
6
|
+
/** Workers-style module: `export default { fetch }`. */
|
|
7
|
+
export interface FetchModule {
|
|
8
|
+
fetch(request: Request, env?: unknown, ctx?: unknown): Promise<Response>;
|
|
9
|
+
}
|
|
10
|
+
type Maybe<T> = T | undefined | Promise<T | undefined>;
|
|
11
|
+
export interface FetchOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Fixed public origin (`https://shop.example`) for request URLs: set it when the platform hands the
|
|
14
|
+
* handler an internal URL, so same-origin checks compare with what the browser sees.
|
|
15
|
+
*/
|
|
16
|
+
origin?: string;
|
|
17
|
+
/** Request body limit for unsafe methods (413 past it); default 1 MiB, multipart 10 MiB. */
|
|
18
|
+
maxBody?: BodyLimit;
|
|
19
|
+
/** Out-of-order streaming for pending `Async` boundaries. */
|
|
20
|
+
stream?: {
|
|
21
|
+
deadline?: number;
|
|
22
|
+
};
|
|
23
|
+
/** Extra headers on every handler response; defaults add nosniff + referrer policy (as node). */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Log unexpected failures (default console.error); app failures get `info` (`digest`, `phase`, `route`). */
|
|
26
|
+
onError?: (error: unknown, request?: Request, info?: ErrorInfo) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Static files before the handler (GET/HEAD): return a Response to answer, undefined to render.
|
|
29
|
+
* E.g. Workers `(request, env) => env.ASSETS.fetch(request)` filtered by path; most platforms serve
|
|
30
|
+
* `dist/client` before the function and need none.
|
|
31
|
+
*/
|
|
32
|
+
serveAssets?: (request: Request, env: unknown, ctx: unknown) => Maybe<Response>;
|
|
33
|
+
/** i18n: error-page locale when the request has none settled (e.g. `errorLocale(config)` from `@rivetjs/i18n/router`). */
|
|
34
|
+
resolveLocale?: (request: Request) => string | undefined | Promise<string | undefined>;
|
|
35
|
+
/** HTML pages' Content-Security-Policy (`createApp` `csp`): default `strictCSP`; `{ trustedTypes, directives }` (`strictCSP` with them), a `(nonce) => policy` function, or `false` (none). */
|
|
36
|
+
csp?: AppConfig["csp"];
|
|
37
|
+
/** Preload planner (`createApp` `preload`): `background` byte budget of idle-time visible prefetch, `lean` (nothing before an interaction); per page when a function. */
|
|
38
|
+
preload?: AppConfig["preload"];
|
|
39
|
+
/** Route rules (`createApp` `routeRules`; default: the entry's, `rivetRouter({ routeRules })`): headers + in-process page cache per path pattern. */
|
|
40
|
+
routeRules?: AppConfig["routeRules"];
|
|
41
|
+
/** The route rules' page store (`createApp` `routeCache`: bounds, `origin`; default `origin`: this adapter's). */
|
|
42
|
+
routeCache?: AppConfig["routeCache"];
|
|
43
|
+
/**
|
|
44
|
+
* Early hints (`createApp` `earlyHints`): a `Link` header on HTML pages (linked stylesheets, the head's build-asset
|
|
45
|
+
* font preloads, `links`, `runtime: true` the resume entry). Cloudflare / Fastly turn it into `103 Early Hints` for
|
|
46
|
+
* later requests; a fetch handler cannot write a 103 itself. Default off.
|
|
47
|
+
*/
|
|
48
|
+
earlyHints?: AppConfig["earlyHints"];
|
|
49
|
+
/** Client address for `event.platform.clientAddress` (e.g. `(r) => r.headers.get("cf-connecting-ip") ?? undefined`). */
|
|
50
|
+
address?: (request: Request, env: unknown, ctx: unknown) => string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Fresh 128-bit CSP nonce (base64), shared with the node adapter. */
|
|
53
|
+
export { nonce };
|
|
54
|
+
/** Wrap a server entry and its (inlined) client manifests into a fetch module. */
|
|
55
|
+
export declare function createFetch(entry: GeneratedEntry, build: BuildManifests, options?: FetchOptions): FetchModule;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { assemble } from "./assemble.js";
|
|
2
|
+
import { nonce } from "../shared/nonce.js";
|
|
3
|
+
/** Fresh 128-bit CSP nonce (base64), shared with the node adapter. */
|
|
4
|
+
export { nonce };
|
|
5
|
+
/** Wrap a server entry and its (inlined) client manifests into a fetch module. */
|
|
6
|
+
export function createFetch(entry, build, options = {}) {
|
|
7
|
+
const { onError = (error) => console.error(error), serveAssets, address } = options;
|
|
8
|
+
const handler = assemble(entry, build, {
|
|
9
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
10
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
11
|
+
...(options.maxBody !== undefined ? { maxBody: options.maxBody } : {}),
|
|
12
|
+
...(options.csp !== undefined ? { csp: options.csp } : {}),
|
|
13
|
+
...(options.preload ? { preload: options.preload } : {}),
|
|
14
|
+
...(options.routeRules ? { routeRules: options.routeRules } : {}),
|
|
15
|
+
// The fixed public origin is the one stored pages are for (`isr` rules need one).
|
|
16
|
+
...(options.routeCache || options.origin ? { routeCache: { ...(options.origin ? { origin: options.origin } : {}), ...options.routeCache } } : {}),
|
|
17
|
+
...(options.resolveLocale ? { resolveLocale: options.resolveLocale } : {}),
|
|
18
|
+
...(options.earlyHints ? { earlyHints: options.earlyHints } : {}),
|
|
19
|
+
}, "fetch entry");
|
|
20
|
+
const base = Object.entries({ "X-Content-Type-Options": "nosniff", "Referrer-Policy": "strict-origin-when-cross-origin", ...options.headers });
|
|
21
|
+
const fixedOrigin = options.origin ? new URL(options.origin).origin : null;
|
|
22
|
+
/** Add the base headers the response lacks (immutable headers, e.g. a proxied `fetch()`: copy). */
|
|
23
|
+
const finish = (response, head) => {
|
|
24
|
+
const missing = base.filter(([name]) => !response.headers.has(name));
|
|
25
|
+
if (head && response.body) {
|
|
26
|
+
void response.body.cancel().catch(() => { });
|
|
27
|
+
response = new Response(null, response);
|
|
28
|
+
}
|
|
29
|
+
if (!missing.length)
|
|
30
|
+
return response;
|
|
31
|
+
try {
|
|
32
|
+
for (const [name, value] of missing)
|
|
33
|
+
response.headers.set(name, value);
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
const copy = new Response(response.body, response);
|
|
38
|
+
for (const [name, value] of missing)
|
|
39
|
+
copy.headers.set(name, value);
|
|
40
|
+
return copy;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
async fetch(request, env, ctx) {
|
|
45
|
+
const head = request.method === "HEAD";
|
|
46
|
+
try {
|
|
47
|
+
if (serveAssets && (head || request.method === "GET")) {
|
|
48
|
+
const asset = await serveAssets(request, env, ctx);
|
|
49
|
+
if (asset)
|
|
50
|
+
return asset;
|
|
51
|
+
}
|
|
52
|
+
if (fixedOrigin) {
|
|
53
|
+
const url = new URL(request.url);
|
|
54
|
+
if (url.origin !== fixedOrigin)
|
|
55
|
+
request = new Request(fixedOrigin + url.pathname + url.search, request);
|
|
56
|
+
}
|
|
57
|
+
const clientAddress = address?.(request, env, ctx);
|
|
58
|
+
const platform = env !== undefined || ctx !== undefined || clientAddress
|
|
59
|
+
? { ...(env !== undefined ? { env } : {}), ...(ctx !== undefined ? { ctx } : {}) }
|
|
60
|
+
: undefined;
|
|
61
|
+
const response = await handler(request, { nonce: nonce(), ...(clientAddress ? { address: clientAddress } : {}), ...(platform ? { platform } : {}) });
|
|
62
|
+
return finish(response, head);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (request.signal.aborted)
|
|
66
|
+
throw error;
|
|
67
|
+
onError(error, request);
|
|
68
|
+
return finish(new Response("Internal Server Error", { status: 500, headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" } }), head);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface StaticFile {
|
|
2
|
+
/** Absolute file path to send (the precompressed sibling when `encoding` is set). */
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly headers: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
/** A response encoding the adapters produce. */
|
|
7
|
+
export type Encoding = "zstd" | "br" | "gzip" | null;
|
|
8
|
+
/**
|
|
9
|
+
* `q` per content coding of an `Accept-Encoding` header, parsed once per distinct string (browsers send a handful;
|
|
10
|
+
* bounded: client-controlled text). `weightOf` applies `*` to codings the header does not name.
|
|
11
|
+
*/
|
|
12
|
+
export declare function acceptWeights(header: string): Map<string, number>;
|
|
13
|
+
export declare const weightOf: (accepted: Map<string, number>, name: string) => number;
|
|
14
|
+
/**
|
|
15
|
+
* Pick `zstd`, `br` or `gzip` from an Accept-Encoding header: the highest `q` (a tie: `zstd`, then `br`); q=0 excludes.
|
|
16
|
+
* `zstd: false` never picks zstd (`serve({ compress: { zstd: false } })`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function negotiate(header: string | null | undefined, zstd?: boolean): Encoding;
|
|
19
|
+
/** Index `client` (served under `base`); returns the per-request lookup. */
|
|
20
|
+
export declare function staticFiles(client: string, base: string, immutable: readonly string[]): (request: Request, pathname: string) => StaticFile | 304 | null;
|