@rivetjs/router 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +20 -0
- package/adapters/files.js +155 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +180 -0
- package/adapters/node.js +986 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +95 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +35 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +97 -0
- package/build/codegen.js +1037 -0
- package/build/dev-errors.d.ts +23 -0
- package/build/dev-errors.js +247 -0
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +31 -0
- package/build/migrate.js +385 -0
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +55 -0
- package/build/routes.js +219 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +208 -0
- package/build/vite.js +861 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +101 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +91 -0
- package/client/index.d.ts +156 -0
- package/client/index.js +714 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +135 -0
- package/index.js +8 -0
- package/package.json +156 -4
- package/server/app.d.ts +175 -0
- package/server/app.js +335 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +240 -0
- package/server/handler.js +661 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +96 -0
- package/server/render.d.ts +96 -0
- package/server/render.js +492 -0
- package/server/route-cache.d.ts +245 -0
- package/server/route-cache.js +813 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CSPOptions } from "../shared/csp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Strict CSP: only nonce'd scripts and what they load run; no plugins, no `<base>` hijacking.
|
|
4
|
+
* `{ trustedTypes: true }` also enforces Trusted Types (`csp: (nonce) => strictCSP(nonce, { trustedTypes: true })`);
|
|
5
|
+
* `{ directives }` merges extra directives (`cspPolicy`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const strictCSP: (nonce: string, options?: CSPOptions) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// `strictCSP` with `directives`: own module so `createApp` loads the directive merge only when an app passes
|
|
2
|
+
// directives (dynamic import); the default policy is `nonceCSP` (shared/csp.ts).
|
|
3
|
+
import { nonceCSP } from "../shared/csp.js";
|
|
4
|
+
import { cspPolicy } from "../shared/csp-directives.js";
|
|
5
|
+
/**
|
|
6
|
+
* Strict CSP: only nonce'd scripts and what they load run; no plugins, no `<base>` hijacking.
|
|
7
|
+
* `{ trustedTypes: true }` also enforces Trusted Types (`csp: (nonce) => strictCSP(nonce, { trustedTypes: true })`);
|
|
8
|
+
* `{ directives }` merges extra directives (`cspPolicy`).
|
|
9
|
+
*/
|
|
10
|
+
export const strictCSP = (nonce, options) => options?.directives
|
|
11
|
+
? cspPolicy([
|
|
12
|
+
["script-src", [`'nonce-${nonce}'`, "'strict-dynamic'"]],
|
|
13
|
+
["object-src", ["'none'"]],
|
|
14
|
+
["base-uri", ["'none'"]],
|
|
15
|
+
], options, false,
|
|
16
|
+
// Inline `<style>`s (inlined stylesheets, head styles) carry the nonce: allowed by a `style-src` the app sets.
|
|
17
|
+
[`'nonce-${nonce}'`])
|
|
18
|
+
: nonceCSP(nonce, options);
|
package/server-only.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/server-only.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A failed action: HTTP `status` plus the data the page (or the enhanced submit) receives. */
|
|
2
|
+
export declare class ActionFailure<T = unknown> {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly data: T;
|
|
5
|
+
constructor(status: number, data: T);
|
|
6
|
+
}
|
|
7
|
+
/** Fail an action with a 4xx status (e.g. 400 for invalid input) and the data to render. */
|
|
8
|
+
export declare function fail<T>(status: number, data: T): ActionFailure<T>;
|
|
9
|
+
/** Whether a value is an `ActionFailure` returned by `fail()`. */
|
|
10
|
+
export declare const isActionFailure: (value: unknown) => value is ActionFailure;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Action results that are not success: returned by an action to answer with a 4xx status and data
|
|
2
|
+
// (validation failures). The page still renders (loaders run) and receives the data as the action
|
|
3
|
+
// result; enhanced (JSON) submits get `{ action: { name, result, status } }` with that status.
|
|
4
|
+
/** A failed action: HTTP `status` plus the data the page (or the enhanced submit) receives. */
|
|
5
|
+
export class ActionFailure {
|
|
6
|
+
constructor(status, data) {
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.data = data;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Fail an action with a 4xx status (e.g. 400 for invalid input) and the data to render. */
|
|
12
|
+
export function fail(status, data) {
|
|
13
|
+
if (!Number.isInteger(status) || status < 400 || status > 499)
|
|
14
|
+
throw new RangeError("fail() status must be 4xx");
|
|
15
|
+
return new ActionFailure(status, data);
|
|
16
|
+
}
|
|
17
|
+
/** Whether a value is an `ActionFailure` returned by `fail()`. */
|
|
18
|
+
export const isActionFailure = (value) => value instanceof ActionFailure;
|
package/shared/base.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Normalize a base (`/app/`, `/app`, `/`, `./`) to `""` (root) or `/app` (no trailing slash). */
|
|
2
|
+
export declare function normalizeBase(base: string | undefined): string;
|
|
3
|
+
/** The app's base path without trailing slash; `""` at the root. */
|
|
4
|
+
export declare const BASE: string;
|
|
5
|
+
/** An app path (`/items/1?q`) as a URL path under the base; other forms (full, relative URLs) unchanged. */
|
|
6
|
+
export declare function withBase(to: string, base?: string): string;
|
|
7
|
+
/** Whether a URL path lies under the base (the app's routes can serve it). */
|
|
8
|
+
export declare function underBase(pathname: string, base?: string): boolean;
|
package/shared/base.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// App base path (`rivetRouter({ base: "/app" })` or Vite's `base`): Vite replaces
|
|
2
|
+
// `import.meta.env.BASE_URL` in every module it builds (client and server bundles); outside Vite
|
|
3
|
+
// (plain Node) it is the root. Route patterns in the generated files already carry the base;
|
|
4
|
+
// these helpers serve code that takes app paths (`<Link to>`, `navigate()`).
|
|
5
|
+
/** Normalize a base (`/app/`, `/app`, `/`, `./`) to `""` (root) or `/app` (no trailing slash). */
|
|
6
|
+
export function normalizeBase(base) {
|
|
7
|
+
if (!base || !base.startsWith("/"))
|
|
8
|
+
return "";
|
|
9
|
+
return base.replace(/\/+$/, "");
|
|
10
|
+
}
|
|
11
|
+
/** The app's base path without trailing slash; `""` at the root. */
|
|
12
|
+
export const BASE = normalizeBase(import.meta.env?.BASE_URL);
|
|
13
|
+
/** An app path (`/items/1?q`) as a URL path under the base; other forms (full, relative URLs) unchanged. */
|
|
14
|
+
export function withBase(to, base = BASE) {
|
|
15
|
+
return base && to.startsWith("/") && !to.startsWith("//") ? base + to : to;
|
|
16
|
+
}
|
|
17
|
+
/** Whether a URL path lies under the base (the app's routes can serve it). */
|
|
18
|
+
export function underBase(pathname, base = BASE) {
|
|
19
|
+
return !base || pathname === base || pathname.startsWith(base + "/");
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a `Cookie` request header (RFC 6265 §5.4 shape): `name=value` pairs split on `;`, names and
|
|
3
|
+
* values trimmed, one pair of surrounding double quotes removed, values percent-decoded (a value that
|
|
4
|
+
* is not valid percent-encoding is kept as written). The first occurrence of a name wins (browsers
|
|
5
|
+
* send the most specific path first); pairs without `=` or with an empty name are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseCookies(header: string | null | undefined): Map<string, string>;
|
package/shared/cookie.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Cookie header parsing: the one implementation behind `event.cookies` (server/handler.ts) and
|
|
2
|
+
// `@rivetjs/i18n` locale resolution (`requestSource`).
|
|
3
|
+
/**
|
|
4
|
+
* Parse a `Cookie` request header (RFC 6265 §5.4 shape): `name=value` pairs split on `;`, names and
|
|
5
|
+
* values trimmed, one pair of surrounding double quotes removed, values percent-decoded (a value that
|
|
6
|
+
* is not valid percent-encoding is kept as written). The first occurrence of a name wins (browsers
|
|
7
|
+
* send the most specific path first); pairs without `=` or with an empty name are skipped.
|
|
8
|
+
*/
|
|
9
|
+
export function parseCookies(header) {
|
|
10
|
+
const out = new Map();
|
|
11
|
+
if (!header)
|
|
12
|
+
return out;
|
|
13
|
+
for (const part of header.split(";")) {
|
|
14
|
+
const at = part.indexOf("=");
|
|
15
|
+
if (at < 0)
|
|
16
|
+
continue;
|
|
17
|
+
const name = part.slice(0, at).trim();
|
|
18
|
+
if (!name || out.has(name))
|
|
19
|
+
continue;
|
|
20
|
+
const raw = part
|
|
21
|
+
.slice(at + 1)
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^"(.*)"$/, "$1");
|
|
24
|
+
try {
|
|
25
|
+
out.set(name, decodeURIComponent(raw));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
out.set(name, raw);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CSPOptions } from "./csp.js";
|
|
2
|
+
/**
|
|
3
|
+
* The policy string for `generated` (directive → sources, in order) merged with `options.directives`
|
|
4
|
+
* and the Trusted Types directives. `meta`: the policy goes into a `<meta>` (header-only directives throw).
|
|
5
|
+
* `styles`: the sources of the page's own `<style>` elements (nonce / hashes: inlined stylesheets, head styles), added
|
|
6
|
+
* to a `style-src` / `style-src-elem` the options set (none set: inline styles are not restricted). A list with
|
|
7
|
+
* `'unsafe-inline'` is left as given (a nonce or hash would turn it off).
|
|
8
|
+
*/
|
|
9
|
+
export declare function cspPolicy(generated: readonly (readonly [string, readonly string[]])[], options: CSPOptions | undefined, meta?: boolean, styles?: readonly string[]): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// CSP directive merge + validation (`directives` option): loaded only by apps that pass directives
|
|
2
|
+
// (`createApp` dynamic import, `strictCSP` / static `hashCSP`), so the default policy carries none of it.
|
|
3
|
+
import { trustedTypesDirectives } from "./csp.js";
|
|
4
|
+
/** CSP Level 3 directive names (+ Trusted Types, `upgrade-insecure-requests`, reporting). */
|
|
5
|
+
const DIRECTIVES = new Set([
|
|
6
|
+
"default-src",
|
|
7
|
+
"script-src",
|
|
8
|
+
"script-src-elem",
|
|
9
|
+
"script-src-attr",
|
|
10
|
+
"style-src",
|
|
11
|
+
"style-src-elem",
|
|
12
|
+
"style-src-attr",
|
|
13
|
+
"img-src",
|
|
14
|
+
"font-src",
|
|
15
|
+
"connect-src",
|
|
16
|
+
"media-src",
|
|
17
|
+
"object-src",
|
|
18
|
+
"child-src",
|
|
19
|
+
"frame-src",
|
|
20
|
+
"worker-src",
|
|
21
|
+
"manifest-src",
|
|
22
|
+
"fenced-frame-src",
|
|
23
|
+
"base-uri",
|
|
24
|
+
"form-action",
|
|
25
|
+
"frame-ancestors",
|
|
26
|
+
"sandbox",
|
|
27
|
+
"upgrade-insecure-requests",
|
|
28
|
+
"report-uri",
|
|
29
|
+
"report-to",
|
|
30
|
+
"webrtc",
|
|
31
|
+
]);
|
|
32
|
+
/** Ignored by browsers in a `<meta>` policy (CSP3 §3.3): header only. */
|
|
33
|
+
const HEADER_ONLY = new Set(["frame-ancestors", "report-uri", "report-to", "sandbox"]);
|
|
34
|
+
// One source expression: no whitespace, no directive / policy separators, nothing that breaks out of an attribute.
|
|
35
|
+
const SOURCE = /^[^\s;,"'<>\\]+$|^'[A-Za-z0-9+/=_-]+'$/;
|
|
36
|
+
/**
|
|
37
|
+
* The policy string for `generated` (directive → sources, in order) merged with `options.directives`
|
|
38
|
+
* and the Trusted Types directives. `meta`: the policy goes into a `<meta>` (header-only directives throw).
|
|
39
|
+
* `styles`: the sources of the page's own `<style>` elements (nonce / hashes: inlined stylesheets, head styles), added
|
|
40
|
+
* to a `style-src` / `style-src-elem` the options set (none set: inline styles are not restricted). A list with
|
|
41
|
+
* `'unsafe-inline'` is left as given (a nonce or hash would turn it off).
|
|
42
|
+
*/
|
|
43
|
+
export function cspPolicy(generated, options, meta = false, styles = []) {
|
|
44
|
+
const policy = new Map(generated.map(([name, sources]) => [name, [...sources]]));
|
|
45
|
+
const extra = options?.directives;
|
|
46
|
+
if (extra) {
|
|
47
|
+
for (const [raw, value] of Object.entries(extra)) {
|
|
48
|
+
const name = raw.toLowerCase();
|
|
49
|
+
if (name === "require-trusted-types-for" || name === "trusted-types")
|
|
50
|
+
throw new TypeError(`CSP directive "${raw}": use the trustedTypes option`);
|
|
51
|
+
if (!DIRECTIVES.has(name))
|
|
52
|
+
throw new TypeError(`Unknown CSP directive "${raw}"`);
|
|
53
|
+
if (meta && HEADER_ONLY.has(name))
|
|
54
|
+
throw new TypeError(`CSP directive "${name}" is ignored in a <meta> policy: send it as a header from your host`);
|
|
55
|
+
const sources = value === true ? [] : typeof value === "string" ? value.split(/\s+/).filter(Boolean) : [...value];
|
|
56
|
+
for (const source of sources)
|
|
57
|
+
if (typeof source !== "string" || !SOURCE.test(source))
|
|
58
|
+
throw new TypeError(`Invalid CSP source ${JSON.stringify(source)} in "${name}"`);
|
|
59
|
+
if (name === "script-src" || name === "script-src-elem") {
|
|
60
|
+
// Additive: the generated sources (nonce / hashes) are what lets the page's own scripts run.
|
|
61
|
+
const list = policy.get(name) ?? (name === "script-src-elem" ? [...(policy.get("script-src") ?? [])] : []);
|
|
62
|
+
for (const source of sources)
|
|
63
|
+
if (!list.includes(source))
|
|
64
|
+
list.push(source);
|
|
65
|
+
policy.set(name, list);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
policy.delete(name);
|
|
69
|
+
policy.set(name, sources);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (styles.length)
|
|
74
|
+
for (const name of ["style-src", "style-src-elem"]) {
|
|
75
|
+
const list = policy.get(name);
|
|
76
|
+
if (list && !list.includes("'unsafe-inline'"))
|
|
77
|
+
for (const source of styles)
|
|
78
|
+
if (!list.includes(source))
|
|
79
|
+
list.push(source);
|
|
80
|
+
}
|
|
81
|
+
let out = "";
|
|
82
|
+
for (const [name, sources] of policy)
|
|
83
|
+
out += (out ? "; " : "") + (sources.length ? `${name} ${sources.join(" ")}` : name);
|
|
84
|
+
return out + trustedTypesDirectives(options);
|
|
85
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CSPOptions } from "./csp.js";
|
|
2
|
+
/** A page's inline code a hash policy must allow. */
|
|
3
|
+
export interface InlineSources {
|
|
4
|
+
/** Bodies of the executable inline scripts, in page order (duplicates kept: callers dedupe the hashes). */
|
|
5
|
+
readonly scripts: string[];
|
|
6
|
+
/** Bodies of the `<style>` elements (only when asked: `styles`). */
|
|
7
|
+
readonly styles: string[];
|
|
8
|
+
/** Origins of third-party script files (`<script src>`, the kit `#rivet-scripts` bootstrap list). */
|
|
9
|
+
readonly origins: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
/** Collect what a hash policy for `html` allows; `styles`: also the `<style>` bodies (the options set a `style-src`). */
|
|
12
|
+
export declare function inlineSources(html: string, styles: boolean): InlineSources;
|
|
13
|
+
/** Byte ranges (`[start, end)`) of what a stored page's hash policy allows, and where its nonce attributes are. */
|
|
14
|
+
export interface NoncedSources {
|
|
15
|
+
/** Bodies of the executable inline scripts that carry the nonce. */
|
|
16
|
+
readonly scripts: [number, number][];
|
|
17
|
+
/** Bodies of the `<style>` elements that carry the nonce (only when asked: `styles`). */
|
|
18
|
+
readonly styles: [number, number][];
|
|
19
|
+
/** Origins of third-party script files whose `<script src>` carries the nonce (and of the kit `#rivet-scripts` list in a nonce'd bootstrap). */
|
|
20
|
+
readonly origins: Set<string>;
|
|
21
|
+
/** Start offsets of each ` nonce="…"` attribute (`' nonce="'.length + nonce.length + 1` long). */
|
|
22
|
+
readonly attrs: number[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Collect what the hash policy of a nonce-rendered page allows: only `<script>` / `<style>` / `<link>` start tags that
|
|
26
|
+
* carry ` nonce="<nonce>"` (the renderer's own elements). `text`: the page as one character per byte (latin1), so
|
|
27
|
+
* offsets are byte offsets; `utf8(start, end)`: those bytes as text (script URLs, the bootstrap list). Null — the page
|
|
28
|
+
* must not be stored — when the nonce appears anywhere else (script text, an attribute value, a malformed tag): a
|
|
29
|
+
* stored copy would hand one nonce to every visitor, or the tag is not one the renderer wrote.
|
|
30
|
+
*/
|
|
31
|
+
export declare function noncedSources(text: string, nonce: string, styles: boolean, utf8: (start: number, end: number) => string): NoncedSources | null;
|
|
32
|
+
/**
|
|
33
|
+
* The hash policy: `script-src` = the scripts' hashes + `'self'` + script origins, `object-src` / `base-uri` `'none'`,
|
|
34
|
+
* merged with `options` (`cspPolicy`; `meta`: header-only directives throw). Hashes are CSP sources (`'sha256-…'`).
|
|
35
|
+
* (`'strict-dynamic'` would not do: it trusts the scripts a trusted script inserts with `createElement("script")`, but
|
|
36
|
+
* an `import()` is fetched with the importing script's nonce, and a hash-allowed script has none — Chromium and Firefox
|
|
37
|
+
* block it (WebKit allows it); the resume loader imports its chunks. Pinned in `tests/route-cache-csp.test.ts`.)
|
|
38
|
+
*/
|
|
39
|
+
export declare function hashPolicy(scripts: Iterable<string>, origins: Iterable<string>, styles: readonly string[], options: CSPOptions | undefined, meta: boolean): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { cspPolicy } from "./csp-directives.js";
|
|
2
|
+
import { BOOT_LIST_END } from "./head.js";
|
|
3
|
+
const SCRIPT = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
|
|
4
|
+
const STYLE = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
5
|
+
// Inline scripts script-src governs: no `src`, and no type, a JavaScript one or `speculationrules` (router document prefetch).
|
|
6
|
+
const executable = (attrs) => {
|
|
7
|
+
if (/\ssrc\s*=/i.test(attrs))
|
|
8
|
+
return false;
|
|
9
|
+
const type = /\stype\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attrs);
|
|
10
|
+
const value = (type?.[1] ?? type?.[2] ?? type?.[3] ?? "").trim().toLowerCase();
|
|
11
|
+
return value === "" || value === "module" || value === "text/javascript" || value === "application/javascript" || value === "speculationrules";
|
|
12
|
+
};
|
|
13
|
+
/** Collect what a hash policy for `html` allows; `styles`: also the `<style>` bodies (the options set a `style-src`). */
|
|
14
|
+
export function inlineSources(html, styles) {
|
|
15
|
+
const scripts = [];
|
|
16
|
+
const origins = new Set();
|
|
17
|
+
for (const [, attrs = "", body = ""] of html.matchAll(SCRIPT)) {
|
|
18
|
+
if (executable(attrs))
|
|
19
|
+
scripts.push(body);
|
|
20
|
+
const src = /\ssrc\s*=\s*"([^"]*)"/i.exec(attrs)?.[1];
|
|
21
|
+
if (src)
|
|
22
|
+
addOrigin(origins, src.replaceAll("&", "&"));
|
|
23
|
+
else if (/\sid\s*=\s*"rivet-scripts"/i.test(attrs))
|
|
24
|
+
bootOrigins(body, origins);
|
|
25
|
+
}
|
|
26
|
+
const out = [];
|
|
27
|
+
if (styles)
|
|
28
|
+
for (const [, body = ""] of html.matchAll(STYLE))
|
|
29
|
+
out.push(body);
|
|
30
|
+
return { scripts, styles: out, origins };
|
|
31
|
+
}
|
|
32
|
+
const NONCE_LEAD = ' nonce="';
|
|
33
|
+
// A whole start tag the renderer writes: `<script|style|link` + attributes (quoted values without `<`) + `>`.
|
|
34
|
+
const NONCED_TAG = /^<(script|style|link)((?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"<]*"|'[^'<]*'|[^\s"'=<>`]+))?)*)\s*\/?>$/i;
|
|
35
|
+
/**
|
|
36
|
+
* Collect what the hash policy of a nonce-rendered page allows: only `<script>` / `<style>` / `<link>` start tags that
|
|
37
|
+
* carry ` nonce="<nonce>"` (the renderer's own elements). `text`: the page as one character per byte (latin1), so
|
|
38
|
+
* offsets are byte offsets; `utf8(start, end)`: those bytes as text (script URLs, the bootstrap list). Null — the page
|
|
39
|
+
* must not be stored — when the nonce appears anywhere else (script text, an attribute value, a malformed tag): a
|
|
40
|
+
* stored copy would hand one nonce to every visitor, or the tag is not one the renderer wrote.
|
|
41
|
+
*/
|
|
42
|
+
export function noncedSources(text, nonce, styles, utf8) {
|
|
43
|
+
const scripts = [];
|
|
44
|
+
const out = [];
|
|
45
|
+
const origins = new Set();
|
|
46
|
+
const attrs = [];
|
|
47
|
+
if (!nonce)
|
|
48
|
+
return null;
|
|
49
|
+
const attr = NONCE_LEAD + nonce + '"';
|
|
50
|
+
for (let at = text.indexOf(nonce); at >= 0; at = text.indexOf(nonce, at + nonce.length)) {
|
|
51
|
+
const start = at - NONCE_LEAD.length;
|
|
52
|
+
if (start < 0 || !text.startsWith(attr, start))
|
|
53
|
+
return null;
|
|
54
|
+
const open = text.lastIndexOf("<", start);
|
|
55
|
+
const close = text.indexOf(">", at);
|
|
56
|
+
const tag = open < 0 || close < 0 ? null : NONCED_TAG.exec(text.slice(open, close + 1));
|
|
57
|
+
if (!tag)
|
|
58
|
+
return null;
|
|
59
|
+
attrs.push(start);
|
|
60
|
+
const name = tag[1].toLowerCase();
|
|
61
|
+
if (name === "link")
|
|
62
|
+
continue;
|
|
63
|
+
const end = text.indexOf(name === "script" ? "</script" : "</style", close + 1);
|
|
64
|
+
if (end < 0)
|
|
65
|
+
return null;
|
|
66
|
+
if (name === "style") {
|
|
67
|
+
if (styles)
|
|
68
|
+
out.push([close + 1, end]);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const attributes = tag[2];
|
|
72
|
+
if (executable(attributes))
|
|
73
|
+
scripts.push([close + 1, end]);
|
|
74
|
+
const src = /\ssrc\s*=\s*"([^"]*)"/i.exec(attributes);
|
|
75
|
+
if (src) {
|
|
76
|
+
// The value's bytes (`"` excluded): the attributes start right after `<script`.
|
|
77
|
+
const end = open + 1 + name.length + src.index + src[0].length - 1;
|
|
78
|
+
addOrigin(origins, utf8(end - src[1].length, end).replaceAll("&", "&"));
|
|
79
|
+
}
|
|
80
|
+
else if (/\sid\s*=\s*"rivet-scripts"/i.test(attributes))
|
|
81
|
+
bootOrigins(utf8(close + 1, end), origins);
|
|
82
|
+
}
|
|
83
|
+
return { scripts, styles: out, origins, attrs };
|
|
84
|
+
}
|
|
85
|
+
function addOrigin(origins, src) {
|
|
86
|
+
const url = URL.canParse(src) ? new URL(src) : null;
|
|
87
|
+
if (url && (url.protocol === "https:" || url.protocol === "http:"))
|
|
88
|
+
origins.add(url.origin);
|
|
89
|
+
}
|
|
90
|
+
/** Kit script bootstrap: `{let l=<JSON list>,d=document,…`; the list ends at the first cut that parses. */
|
|
91
|
+
function bootOrigins(body, origins) {
|
|
92
|
+
if (!body.startsWith("{let l="))
|
|
93
|
+
return;
|
|
94
|
+
for (let at = body.indexOf(BOOT_LIST_END); at > 0; at = body.indexOf(BOOT_LIST_END, at + 1)) {
|
|
95
|
+
let list;
|
|
96
|
+
try {
|
|
97
|
+
list = JSON.parse(body.slice(7, at));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of list)
|
|
103
|
+
if (typeof entry.u === "string")
|
|
104
|
+
addOrigin(origins, entry.u);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The hash policy: `script-src` = the scripts' hashes + `'self'` + script origins, `object-src` / `base-uri` `'none'`,
|
|
110
|
+
* merged with `options` (`cspPolicy`; `meta`: header-only directives throw). Hashes are CSP sources (`'sha256-…'`).
|
|
111
|
+
* (`'strict-dynamic'` would not do: it trusts the scripts a trusted script inserts with `createElement("script")`, but
|
|
112
|
+
* an `import()` is fetched with the importing script's nonce, and a hash-allowed script has none — Chromium and Firefox
|
|
113
|
+
* block it (WebKit allows it); the resume loader imports its chunks. Pinned in `tests/route-cache-csp.test.ts`.)
|
|
114
|
+
*/
|
|
115
|
+
export function hashPolicy(scripts, origins, styles, options, meta) {
|
|
116
|
+
return cspPolicy([
|
|
117
|
+
["script-src", [...new Set(scripts), "'self'", ...origins]],
|
|
118
|
+
["object-src", ["'none'"]],
|
|
119
|
+
["base-uri", ["'none'"]],
|
|
120
|
+
], options, meta, [...new Set(styles)]);
|
|
121
|
+
}
|
package/shared/csp.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extra CSP directives, merged with the generated policy. A value is a source list (string split on
|
|
3
|
+
* whitespace, or an array); `true` / `[]` / `""` is a directive without sources (`upgrade-insecure-requests`).
|
|
4
|
+
*/
|
|
5
|
+
export type CSPDirectives = Readonly<Record<string, string | readonly string[] | true>>;
|
|
6
|
+
/** Content Security Policy options: directives, nonces and Trusted Types. */
|
|
7
|
+
export interface CSPOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Enforce Trusted Types: `require-trusted-types-for 'script'` with the runtime's `rivet` policy
|
|
10
|
+
* (template HTML) allowed. Pass policy names to allow your own as well.
|
|
11
|
+
*/
|
|
12
|
+
readonly trustedTypes?: boolean | readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Extra directives (`{ "script-src": "'wasm-unsafe-eval'", "worker-src": ["'self'", "blob:"] }`).
|
|
15
|
+
* `script-src` (and `script-src-elem`, when given) sources are added to the generated ones (nonce /
|
|
16
|
+
* hashes, `'self'`); `object-src` / `base-uri` replace the generated `'none'`; other directives are
|
|
17
|
+
* appended. Names are checked (unknown → `TypeError`); Trusted Types go through `trustedTypes`.
|
|
18
|
+
* Nothing is added implicitly (no `'unsafe-inline'`).
|
|
19
|
+
*/
|
|
20
|
+
readonly directives?: CSPDirectives;
|
|
21
|
+
}
|
|
22
|
+
export { cspPolicy } from "./csp-directives.js";
|
|
23
|
+
/** The default strict nonce policy (+ Trusted Types): what `strictCSP` returns without `directives`. */
|
|
24
|
+
export declare const nonceCSP: (nonce: string, options?: CSPOptions) => string;
|
|
25
|
+
/** The Trusted Types directives for `options` (empty when off), each prefixed with `; `. */
|
|
26
|
+
export declare function trustedTypesDirectives(options: CSPOptions | undefined): string;
|
package/shared/csp.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// CSP options shared by the server (nonce policy) and the static adapter (hash policy).
|
|
2
|
+
// The directive merge + validation lives in its own module (`csp-directives.ts`): apps without
|
|
3
|
+
// `directives` never load it (`createApp` imports it dynamically; `strictCSP` / `hashCSP` statically).
|
|
4
|
+
export { cspPolicy } from "./csp-directives.js";
|
|
5
|
+
/** The default strict nonce policy (+ Trusted Types): what `strictCSP` returns without `directives`. */
|
|
6
|
+
export const nonceCSP = (nonce, options) => `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'` + trustedTypesDirectives(options);
|
|
7
|
+
/** The Trusted Types directives for `options` (empty when off), each prefixed with `; `. */
|
|
8
|
+
export function trustedTypesDirectives(options) {
|
|
9
|
+
const names = options?.trustedTypes;
|
|
10
|
+
if (!names)
|
|
11
|
+
return "";
|
|
12
|
+
return `; require-trusted-types-for 'script'; trusted-types ${["rivet", ...(names === true ? [] : names)].join(" ")}`;
|
|
13
|
+
}
|
package/shared/data.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Query flag marking a client-navigation data request. */
|
|
2
|
+
export declare const DATA_PARAM = "_rivet";
|
|
3
|
+
/** Data requests: comma-separated ids of the layouts whose data the client still has (loaders skipped). */
|
|
4
|
+
export declare const KEEP_PARAM = "_rivet_keep";
|
|
5
|
+
/** `_rivet_keep` entry naming the page's own loader (`invalidate(key)` of layout keys only: same URL). */
|
|
6
|
+
export declare const KEEP_PAGE = ".";
|
|
7
|
+
/**
|
|
8
|
+
* `search` (`?a&b=c d`) without the router's own params (`_rivet`, `_rivet_keep`), every other byte
|
|
9
|
+
* as sent: loaders see the page URL's query exactly, so their keys match on HTML and data requests.
|
|
10
|
+
*/
|
|
11
|
+
export declare function withoutRivet(search: string): string;
|
|
12
|
+
/** Dependency keys (`event.depends()`) per loader, route data order; undefined: none. Omitted when no loader called it. */
|
|
13
|
+
export type LoaderDeps = readonly (readonly string[] | undefined)[];
|
|
14
|
+
/**
|
|
15
|
+
* Document head a route (or a component, with `@rivetjs/kit`) contributes. Merged by rank: inner
|
|
16
|
+
* route levels win; within a level components win over the route's `head` export.
|
|
17
|
+
*/
|
|
18
|
+
export interface Head {
|
|
19
|
+
title?: string;
|
|
20
|
+
/** `"%s · Shop"`: applied once to the merged title; innermost wins. */
|
|
21
|
+
titleTemplate?: string;
|
|
22
|
+
/** `<html>` attributes (needs the kit head hold; merged per attribute). */
|
|
23
|
+
html?: {
|
|
24
|
+
lang?: string;
|
|
25
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
26
|
+
class?: string;
|
|
27
|
+
};
|
|
28
|
+
body?: {
|
|
29
|
+
class?: string;
|
|
30
|
+
};
|
|
31
|
+
base?: {
|
|
32
|
+
href: string;
|
|
33
|
+
target?: string;
|
|
34
|
+
};
|
|
35
|
+
meta?: readonly HeadMeta[];
|
|
36
|
+
/** `<link>` tags; keyed per rel (one `canonical`; alternates by hreflang/type; others by href). */
|
|
37
|
+
links?: readonly HeadLink[];
|
|
38
|
+
scripts?: readonly HeadScript[];
|
|
39
|
+
/** Inline `<style>` blocks (trusted CSS, request nonce; deduped by `key`, else by text). Never managed: client navigation keeps them. */
|
|
40
|
+
styles?: readonly HeadStyle[];
|
|
41
|
+
}
|
|
42
|
+
/** An inline head `<style>` (component CSS such as `@rivetjs/motion` entrance rules). */
|
|
43
|
+
export interface HeadStyle {
|
|
44
|
+
/** Trusted CSS text (a `</style` sequence is escaped). */
|
|
45
|
+
css: string;
|
|
46
|
+
/** Dedupe key (default: the CSS text). */
|
|
47
|
+
key?: string;
|
|
48
|
+
media?: string;
|
|
49
|
+
}
|
|
50
|
+
/** A head `<meta>`. */
|
|
51
|
+
export interface HeadMeta {
|
|
52
|
+
name?: string;
|
|
53
|
+
property?: string;
|
|
54
|
+
httpEquiv?: string;
|
|
55
|
+
itemprop?: string;
|
|
56
|
+
content: string;
|
|
57
|
+
/** e.g. `theme-color` per color scheme; part of the dedupe key. */
|
|
58
|
+
media?: string;
|
|
59
|
+
/** Explicit dedupe key. */
|
|
60
|
+
key?: string;
|
|
61
|
+
}
|
|
62
|
+
/** A head `<link>`. */
|
|
63
|
+
export interface HeadLink {
|
|
64
|
+
rel: string;
|
|
65
|
+
href: string;
|
|
66
|
+
type?: string;
|
|
67
|
+
hreflang?: string;
|
|
68
|
+
media?: string;
|
|
69
|
+
sizes?: string;
|
|
70
|
+
as?: string;
|
|
71
|
+
crossorigin?: "anonymous" | "use-credentials";
|
|
72
|
+
imagesrcset?: string;
|
|
73
|
+
imagesizes?: string;
|
|
74
|
+
fetchpriority?: "high" | "low" | "auto";
|
|
75
|
+
title?: string;
|
|
76
|
+
color?: string;
|
|
77
|
+
integrity?: string;
|
|
78
|
+
referrerpolicy?: string;
|
|
79
|
+
key?: string;
|
|
80
|
+
}
|
|
81
|
+
/** When a script runs: in the head while parsing (`blocking`, `eager` = async, `defer`) or later from a tiny bootstrap. */
|
|
82
|
+
export type ScriptStrategy = "blocking" | "eager" | "defer" | "load" | "idle" | "interaction";
|
|
83
|
+
/** Static inline script code (see `inlineScript`); plain strings are refused. */
|
|
84
|
+
export interface InlineScript {
|
|
85
|
+
readonly code: string;
|
|
86
|
+
}
|
|
87
|
+
/** A head script: external (`src`), inline code, or a JSON-LD data block. */
|
|
88
|
+
export type HeadScript = {
|
|
89
|
+
src: string;
|
|
90
|
+
type?: "module" | "classic";
|
|
91
|
+
/** Default `defer`. */
|
|
92
|
+
strategy?: ScriptStrategy;
|
|
93
|
+
id?: string;
|
|
94
|
+
integrity?: string;
|
|
95
|
+
crossorigin?: "anonymous" | "use-credentials";
|
|
96
|
+
referrerpolicy?: string;
|
|
97
|
+
key?: string;
|
|
98
|
+
} | {
|
|
99
|
+
inline: InlineScript;
|
|
100
|
+
/** Position in the head (inline scripts always run while parsing). Default `defer`. */
|
|
101
|
+
strategy?: "blocking" | "eager" | "defer";
|
|
102
|
+
/** JSON the script reads from `document.currentScript.previousElementSibling`. */
|
|
103
|
+
data?: unknown;
|
|
104
|
+
id?: string;
|
|
105
|
+
key?: string;
|
|
106
|
+
} | {
|
|
107
|
+
jsonLd: object;
|
|
108
|
+
id?: string;
|
|
109
|
+
key?: string;
|
|
110
|
+
};
|
|
111
|
+
/** Request collector of component head entries (`@rivetjs/kit`), provided through `ServerData`. */
|
|
112
|
+
export interface HeadSink {
|
|
113
|
+
/** Record `head` for route level `level`; after the hold, returns the markup body-valid entries write in place. */
|
|
114
|
+
push(level: number, head: Head, component?: string): string | undefined;
|
|
115
|
+
/** CSP nonce of the response (inline scripts carry it). */
|
|
116
|
+
readonly nonce: string | undefined;
|
|
117
|
+
}
|
|
118
|
+
/** An action's outcome (form POST `?/name`): its return value, or the data and status of `fail()`. */
|
|
119
|
+
export interface ActionResult {
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly result: unknown;
|
|
122
|
+
/** Set when the action failed (`fail(status, data)`): the response status; `result` is the data. */
|
|
123
|
+
readonly status?: number;
|
|
124
|
+
}
|
|
125
|
+
/** The `error` member of a JSON answer: a request that failed (guard, action, loader, 404/405/413…). */
|
|
126
|
+
export interface AnswerError {
|
|
127
|
+
readonly status: number;
|
|
128
|
+
readonly message: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The handler's JSON answer to an enhanced action POST (`?/name` with `accept: application/json`),
|
|
132
|
+
* resume-format encoded (`encodeData`); `@rivetjs/forms` decodes it. Exactly one of `action`,
|
|
133
|
+
* `redirect` or `error` is set.
|
|
134
|
+
*/
|
|
135
|
+
export interface ActionAnswer {
|
|
136
|
+
/** The action ran: its return value, or the data and status of `fail()`. */
|
|
137
|
+
readonly action?: ActionResult;
|
|
138
|
+
/** Single flight (`x-rivet-revalidate` request header, status < 400): the page's re-run loader data, layouts outermost first. */
|
|
139
|
+
readonly data?: readonly unknown[];
|
|
140
|
+
/** Single flight: the route head for the fresh data (when the app has a `head` hook). */
|
|
141
|
+
readonly head?: Head;
|
|
142
|
+
/** Single flight: the loaders' `event.depends()` keys (absent when none called it). */
|
|
143
|
+
readonly deps?: LoaderDeps;
|
|
144
|
+
/** A redirect (guard, `redirect()` in the action, a request handler's 3xx) the client follows; `status` is its 3xx. */
|
|
145
|
+
readonly redirect?: string;
|
|
146
|
+
readonly status?: number;
|
|
147
|
+
readonly error?: AnswerError;
|
|
148
|
+
}
|
|
149
|
+
/** The handler's JSON answer to a client-navigation data request (`?_rivet=data`), resume-format encoded. */
|
|
150
|
+
export interface DataAnswer {
|
|
151
|
+
/** Loader results, layouts outermost first (`undefined` for the `keep` indices). */
|
|
152
|
+
readonly data?: readonly unknown[];
|
|
153
|
+
readonly status?: number;
|
|
154
|
+
readonly head?: Head;
|
|
155
|
+
/** Indices into `data` of kept layouts the server skipped (`_rivet_keep`). */
|
|
156
|
+
readonly keep?: readonly number[];
|
|
157
|
+
/** Indices of layouts whose loader read the URL. */
|
|
158
|
+
readonly byURL?: readonly number[];
|
|
159
|
+
readonly deps?: LoaderDeps;
|
|
160
|
+
readonly redirect?: string;
|
|
161
|
+
/** A guard or request handler answered with a non-redirect Response: load the document instead. */
|
|
162
|
+
readonly reload?: true;
|
|
163
|
+
readonly error?: AnswerError;
|
|
164
|
+
}
|
|
165
|
+
/** Loader results for one URL: layouts outermost first, then the page. */
|
|
166
|
+
export interface RoutePayload {
|
|
167
|
+
readonly data: readonly unknown[];
|
|
168
|
+
readonly head?: Head;
|
|
169
|
+
/** Data requests: indices into `data` of layouts the server skipped (the client keeps their data). */
|
|
170
|
+
readonly kept?: readonly number[];
|
|
171
|
+
/** Data requests: indices of layouts whose loader read the URL (reloaded on every URL change). */
|
|
172
|
+
readonly byURL?: readonly number[];
|
|
173
|
+
/** Keys the loaders that ran passed to `event.depends()` (absent: none did). */
|
|
174
|
+
readonly deps?: LoaderDeps;
|
|
175
|
+
}
|
package/shared/data.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Wire contract between the server handler and the client router.
|
|
2
|
+
/** Query flag marking a client-navigation data request. */
|
|
3
|
+
export const DATA_PARAM = "_rivet";
|
|
4
|
+
/** Data requests: comma-separated ids of the layouts whose data the client still has (loaders skipped). */
|
|
5
|
+
export const KEEP_PARAM = "_rivet_keep";
|
|
6
|
+
/** `_rivet_keep` entry naming the page's own loader (`invalidate(key)` of layout keys only: same URL). */
|
|
7
|
+
export const KEEP_PAGE = ".";
|
|
8
|
+
/**
|
|
9
|
+
* `search` (`?a&b=c d`) without the router's own params (`_rivet`, `_rivet_keep`), every other byte
|
|
10
|
+
* as sent: loaders see the page URL's query exactly, so their keys match on HTML and data requests.
|
|
11
|
+
*/
|
|
12
|
+
export function withoutRivet(search) {
|
|
13
|
+
if (!search.includes(DATA_PARAM))
|
|
14
|
+
return search;
|
|
15
|
+
let out = "";
|
|
16
|
+
for (const part of search.slice(1).split("&")) {
|
|
17
|
+
const name = part.split("=", 1)[0];
|
|
18
|
+
if (name !== DATA_PARAM && name !== KEEP_PARAM)
|
|
19
|
+
out += (out ? "&" : "?") + part;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|