@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
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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A redirect, denial or HTTP failure ending the guard/loader chain. */
|
|
2
|
+
export declare class HttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
/** Redirect target for 3xx. */
|
|
5
|
+
readonly location: string | undefined;
|
|
6
|
+
/** Safe to show to users (4xx messages); 5xx details stay server-side. */
|
|
7
|
+
readonly expose: boolean;
|
|
8
|
+
constructor(status: number, message: string, location?: string);
|
|
9
|
+
}
|
|
10
|
+
/** Options of `redirect()`. */
|
|
11
|
+
export interface RedirectOptions {
|
|
12
|
+
status?: 301 | 302 | 303 | 307 | 308;
|
|
13
|
+
/** Allow an absolute `http(s):` URL on another origin (payment providers, SSO). */
|
|
14
|
+
external?: boolean;
|
|
15
|
+
}
|
|
16
|
+
type RedirectStatus = NonNullable<RedirectOptions["status"]>;
|
|
17
|
+
/**
|
|
18
|
+
* Redirect (default 302 Found, 303 See Other from actions; 307/308 keep the method). Same-origin
|
|
19
|
+
* only: relative targets, or absolute URLs on the request's origin; `{ external: true }` allows
|
|
20
|
+
* other origins. Protocol-relative (`//x`), backslash and control-character targets throw.
|
|
21
|
+
*/
|
|
22
|
+
export declare function redirect(location: string, status?: RedirectStatus | RedirectOptions, options?: RedirectOptions): HttpError;
|
|
23
|
+
/** Refuse access (default 403). */
|
|
24
|
+
export declare function deny(status?: 401 | 403, message?: string): HttpError;
|
|
25
|
+
/** The resource does not exist; renders the nearest `_404`. */
|
|
26
|
+
export declare function notFound(message?: string): HttpError;
|
|
27
|
+
/** Fail with an explicit status; renders the nearest `_error`. */
|
|
28
|
+
export declare function error(status: number, message: string): HttpError;
|
|
29
|
+
/** Whether a value is an `HttpError` thrown by `error()` / `notFound()`. */
|
|
30
|
+
export declare const isHttpError: (value: unknown) => value is HttpError;
|
|
31
|
+
export {};
|
package/shared/errors.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Control-flow results for guards, loaders and actions: thrown or returned, never rendered as data.
|
|
2
|
+
import { unsafeTarget } from "./redirect.js";
|
|
3
|
+
/** A redirect, denial or HTTP failure ending the guard/loader chain. */
|
|
4
|
+
export class HttpError extends Error {
|
|
5
|
+
constructor(status, message, location) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "HttpError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.location = location;
|
|
10
|
+
this.expose = status < 500;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Redirect (default 302 Found, 303 See Other from actions; 307/308 keep the method). Same-origin
|
|
15
|
+
* only: relative targets, or absolute URLs on the request's origin; `{ external: true }` allows
|
|
16
|
+
* other origins. Protocol-relative (`//x`), backslash and control-character targets throw.
|
|
17
|
+
*/
|
|
18
|
+
export function redirect(location, status, options) {
|
|
19
|
+
const opts = typeof status === "object" ? status : options;
|
|
20
|
+
const code = typeof status === "number" ? status : opts?.status;
|
|
21
|
+
const external = opts?.external === true;
|
|
22
|
+
const unsafe = unsafeTarget(location);
|
|
23
|
+
if (unsafe)
|
|
24
|
+
throw new Error(`redirect(): unsafe target ${JSON.stringify(location)} (${unsafe})`);
|
|
25
|
+
if (external && !/^https?:\/\//i.test(location))
|
|
26
|
+
throw new Error(`redirect(): external target must be an absolute http(s) URL, got ${JSON.stringify(location)}`);
|
|
27
|
+
const error = new HttpError(code ?? 302, "Redirect", location);
|
|
28
|
+
// Actions answer a defaulted redirect with 303 (POST → GET); an explicit status is kept.
|
|
29
|
+
if (code === undefined)
|
|
30
|
+
Object.defineProperty(error, "defaultStatus", { value: true });
|
|
31
|
+
// The handler checks absolute targets against the request origin unless external.
|
|
32
|
+
if (external)
|
|
33
|
+
Object.defineProperty(error, "external", { value: true });
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
/** Refuse access (default 403). */
|
|
37
|
+
export function deny(status = 403, message = status === 401 ? "Unauthorized" : "Forbidden") {
|
|
38
|
+
return new HttpError(status, message);
|
|
39
|
+
}
|
|
40
|
+
/** The resource does not exist; renders the nearest `_404`. */
|
|
41
|
+
export function notFound(message = "Not found") {
|
|
42
|
+
return new HttpError(404, message);
|
|
43
|
+
}
|
|
44
|
+
/** Fail with an explicit status; renders the nearest `_error`. */
|
|
45
|
+
export function error(status, message) {
|
|
46
|
+
if (!Number.isInteger(status) || status < 400 || status > 599)
|
|
47
|
+
throw new RangeError("error() status must be 4xx or 5xx");
|
|
48
|
+
return new HttpError(status, message);
|
|
49
|
+
}
|
|
50
|
+
/** Whether a value is an `HttpError` thrown by `error()` / `notFound()`. */
|
|
51
|
+
export const isHttpError = (value) => value instanceof HttpError;
|
package/shared/head.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Head, HeadLink, HeadMeta, HeadScript, InlineScript } from "./data.js";
|
|
2
|
+
/** One head contribution: `level` −1 document defaults, layouts 0…n−1, page n; `phase` 0 route export, 1 components. */
|
|
3
|
+
export interface RankedHead {
|
|
4
|
+
readonly level: number;
|
|
5
|
+
readonly phase: 0 | 1;
|
|
6
|
+
readonly seq: number;
|
|
7
|
+
readonly head: Head;
|
|
8
|
+
}
|
|
9
|
+
/** Brand static inline script code (kit `?script` imports, `unsafeInlineScript`). */
|
|
10
|
+
export declare function inlineScript(code: string): InlineScript;
|
|
11
|
+
export declare const isInlineScript: (value: unknown) => value is InlineScript;
|
|
12
|
+
export declare function metaKey(meta: HeadMeta): string;
|
|
13
|
+
export declare function linkKey(link: HeadLink): string;
|
|
14
|
+
export declare function scriptKey(script: HeadScript): string;
|
|
15
|
+
/**
|
|
16
|
+
* Merge contributions by rank `(level, phase, seq)`, later winning per key. The title template
|
|
17
|
+
* applies once to the winning title (the output's `title` is final; `titleTemplate` is kept).
|
|
18
|
+
*/
|
|
19
|
+
export declare function rankMerge(entries: readonly RankedHead[]): Head | undefined;
|
|
20
|
+
export interface HeadTagOptions {
|
|
21
|
+
/** Stylesheet URLs, linked before the head's own stylesheets (capo position 7). */
|
|
22
|
+
styles?: readonly string[];
|
|
23
|
+
/** Entries of `styles` written as an inline `<style>` (URL → CSS, with the nonce) instead of a link, in place. */
|
|
24
|
+
inlineStyles?: Readonly<Record<string, string>> | undefined;
|
|
25
|
+
/** CSP nonce for inline and external scripts. */
|
|
26
|
+
nonce?: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Mark managed tags `data-rh` (kit updates them on client navigation); entries in `persistent`
|
|
29
|
+
* (root layout components, never re-rendered in the browser) get `data-rh="0"` and are kept.
|
|
30
|
+
*/
|
|
31
|
+
managed?: boolean | HeadManaged | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface HeadManaged {
|
|
34
|
+
readonly persistent: ReadonlySet<object>;
|
|
35
|
+
}
|
|
36
|
+
export declare function linkTag(link: HeadLink, mark?: string): string;
|
|
37
|
+
export declare function metaTag(meta: HeadMeta, mark?: string): string;
|
|
38
|
+
export declare const jsonLdTag: (data: object, id?: string, mark?: string) => string;
|
|
39
|
+
export declare const BOOT_LIST_END = ",d=document,";
|
|
40
|
+
/**
|
|
41
|
+
* Bootstrap for `load` / `idle` / `interaction` scripts: `list` (escaped JSON array) inlined, code
|
|
42
|
+
* for only the strategies the page uses. Injected scripts are async by default.
|
|
43
|
+
*/
|
|
44
|
+
export declare function _scriptBootstrap(strategies: Iterable<string>, list?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Tags for a merged head in capo.js order: base, title, preconnect, async scripts, blocking scripts,
|
|
47
|
+
* stylesheets (`styles` first), preloads, deferred scripts (+ bootstrap), prefetch, meta / SEO links,
|
|
48
|
+
* JSON-LD. Charset, viewport and the CSP meta belong to the shell.
|
|
49
|
+
*/
|
|
50
|
+
export declare function headTags(head: Head | undefined, options?: HeadTagOptions): string;
|
|
51
|
+
/** `<html>` attributes (leading space) for shells: the head's `html` over request defaults (i18n `locale`/`dir`). */
|
|
52
|
+
export declare function htmlAttributes(head: Head | undefined, defaults?: {
|
|
53
|
+
lang?: string | undefined;
|
|
54
|
+
dir?: string | undefined;
|
|
55
|
+
}): string;
|
|
56
|
+
/** `<body>` attributes (leading space). */
|
|
57
|
+
export declare const bodyAttributes: (head: Head | undefined) => string;
|
package/shared/head.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Head merging (rank + dedupe keys) and serialization (capo.js order), shared by server and kit.
|
|
2
|
+
import { escapeJSON } from "@rivetjs/runtime/resume-format";
|
|
3
|
+
import { escapeAttribute, escapeText } from "@rivetjs/runtime/escape";
|
|
4
|
+
const inline = new WeakSet();
|
|
5
|
+
/** Brand static inline script code (kit `?script` imports, `unsafeInlineScript`). */
|
|
6
|
+
export function inlineScript(code) {
|
|
7
|
+
const script = Object.freeze({ code });
|
|
8
|
+
inline.add(script);
|
|
9
|
+
return script;
|
|
10
|
+
}
|
|
11
|
+
export const isInlineScript = (value) => typeof value === "object" && value !== null && inline.has(value);
|
|
12
|
+
export function metaKey(meta) {
|
|
13
|
+
if (meta.key !== undefined)
|
|
14
|
+
return "k:" + meta.key;
|
|
15
|
+
const base = meta.name !== undefined
|
|
16
|
+
? "n:" + meta.name
|
|
17
|
+
: meta.property !== undefined
|
|
18
|
+
? "p:" + meta.property
|
|
19
|
+
: meta.httpEquiv !== undefined
|
|
20
|
+
? "h:" + meta.httpEquiv.toLowerCase()
|
|
21
|
+
: "i:" + meta.itemprop;
|
|
22
|
+
return meta.media !== undefined ? base + "|" + meta.media : base;
|
|
23
|
+
}
|
|
24
|
+
export function linkKey(link) {
|
|
25
|
+
if (link.key !== undefined)
|
|
26
|
+
return "k:" + link.key;
|
|
27
|
+
const rel = link.rel.toLowerCase();
|
|
28
|
+
if (rel === "canonical")
|
|
29
|
+
return "canonical";
|
|
30
|
+
// hreflang alternates may share an href (`en` and `x-default`); feeds differ by href.
|
|
31
|
+
if (rel === "alternate")
|
|
32
|
+
return link.hreflang !== undefined ? `alternate ${link.hreflang} ${link.type ?? ""}` : `alternate ${link.type ?? ""} ${link.href}`;
|
|
33
|
+
if (rel === "icon" || rel === "apple-touch-icon" || rel === "mask-icon")
|
|
34
|
+
return `${rel} ${link.sizes ?? ""} ${link.media ?? ""}`;
|
|
35
|
+
return `${rel} ${link.href || link.imagesrcset || ""} ${link.media ?? ""}`;
|
|
36
|
+
}
|
|
37
|
+
let jsonLdSeq = 0;
|
|
38
|
+
export function scriptKey(script) {
|
|
39
|
+
if (script.key !== undefined)
|
|
40
|
+
return "k:" + script.key;
|
|
41
|
+
if ("src" in script)
|
|
42
|
+
return "s:" + (script.id ?? script.src);
|
|
43
|
+
if ("inline" in script)
|
|
44
|
+
return "i:" + (script.id ?? script.inline.code);
|
|
45
|
+
// JSON-LD without an id is never deduplicated (several blocks are fine).
|
|
46
|
+
return script.id !== undefined ? "j:" + script.id : "j#" + jsonLdSeq++;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Merge contributions by rank `(level, phase, seq)`, later winning per key. The title template
|
|
50
|
+
* applies once to the winning title (the output's `title` is final; `titleTemplate` is kept).
|
|
51
|
+
*/
|
|
52
|
+
export function rankMerge(entries) {
|
|
53
|
+
if (!entries.length)
|
|
54
|
+
return undefined;
|
|
55
|
+
const sorted = [...entries].sort((a, b) => a.level - b.level || a.phase - b.phase || a.seq - b.seq);
|
|
56
|
+
let title, template, base;
|
|
57
|
+
let html, body;
|
|
58
|
+
const meta = new Map(), links = new Map(), scripts = new Map(), styles = new Map();
|
|
59
|
+
for (const { head } of sorted) {
|
|
60
|
+
if (head.title !== undefined)
|
|
61
|
+
title = head.title;
|
|
62
|
+
if (head.titleTemplate !== undefined)
|
|
63
|
+
template = head.titleTemplate;
|
|
64
|
+
if (head.base)
|
|
65
|
+
base = head.base;
|
|
66
|
+
if (head.html)
|
|
67
|
+
html = { ...html, ...head.html };
|
|
68
|
+
if (head.body)
|
|
69
|
+
body = { ...body, ...head.body };
|
|
70
|
+
for (const entry of head.meta ?? [])
|
|
71
|
+
meta.set(metaKey(entry), entry);
|
|
72
|
+
for (const entry of head.links ?? [])
|
|
73
|
+
links.set(linkKey(entry), entry);
|
|
74
|
+
for (const entry of head.scripts ?? [])
|
|
75
|
+
scripts.set(scriptKey(entry), entry);
|
|
76
|
+
for (const entry of head.styles ?? [])
|
|
77
|
+
styles.set(entry.key ?? entry.css, entry);
|
|
78
|
+
}
|
|
79
|
+
if (title !== undefined && template !== undefined)
|
|
80
|
+
title = template.replace("%s", title);
|
|
81
|
+
// The template stays alongside the final title: client head updates template component titles.
|
|
82
|
+
return {
|
|
83
|
+
...(title !== undefined ? { title } : {}),
|
|
84
|
+
...(template !== undefined ? { titleTemplate: template } : {}),
|
|
85
|
+
...(html ? { html } : {}),
|
|
86
|
+
...(body ? { body } : {}),
|
|
87
|
+
...(base ? { base } : {}),
|
|
88
|
+
...(meta.size ? { meta: [...meta.values()] } : {}),
|
|
89
|
+
...(links.size ? { links: [...links.values()] } : {}),
|
|
90
|
+
...(scripts.size ? { scripts: [...scripts.values()] } : {}),
|
|
91
|
+
...(styles.size ? { styles: [...styles.values()] } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const attr = (name, value) => value === undefined || value === false ? "" : value === true ? " " + name : ` ${name}="${escapeAttribute(value)}"`;
|
|
95
|
+
const LINK_ATTRIBUTES = [
|
|
96
|
+
"rel",
|
|
97
|
+
"href",
|
|
98
|
+
"type",
|
|
99
|
+
"hreflang",
|
|
100
|
+
"media",
|
|
101
|
+
"sizes",
|
|
102
|
+
"as",
|
|
103
|
+
"crossorigin",
|
|
104
|
+
"imagesrcset",
|
|
105
|
+
"imagesizes",
|
|
106
|
+
"fetchpriority",
|
|
107
|
+
"title",
|
|
108
|
+
"color",
|
|
109
|
+
"integrity",
|
|
110
|
+
"referrerpolicy",
|
|
111
|
+
];
|
|
112
|
+
export function linkTag(link, mark = "") {
|
|
113
|
+
let tag = "<link";
|
|
114
|
+
for (const name of LINK_ATTRIBUTES)
|
|
115
|
+
tag += attr(name, link[name]);
|
|
116
|
+
return tag + mark + ">";
|
|
117
|
+
}
|
|
118
|
+
export function metaTag(meta, mark = "") {
|
|
119
|
+
const key = meta.name !== undefined
|
|
120
|
+
? attr("name", meta.name)
|
|
121
|
+
: meta.property !== undefined
|
|
122
|
+
? attr("property", meta.property)
|
|
123
|
+
: meta.httpEquiv !== undefined
|
|
124
|
+
? attr("http-equiv", meta.httpEquiv)
|
|
125
|
+
: meta.itemprop !== undefined
|
|
126
|
+
? attr("itemprop", meta.itemprop)
|
|
127
|
+
: "";
|
|
128
|
+
return key ? `<meta${key} content="${escapeAttribute(meta.content)}"${attr("media", meta.media)}${mark}>` : "";
|
|
129
|
+
}
|
|
130
|
+
export const jsonLdTag = (data, id, mark = "") => `<script type="application/ld+json"${attr("id", id)}${mark}>${escapeJSON(JSON.stringify(data))}</script>`;
|
|
131
|
+
const deferred = (s) => s.strategy === "load" || s.strategy === "idle" || s.strategy === "interaction";
|
|
132
|
+
// Script bootstrap (minified by hand), one inline script per page: `{let l=<list>,…}` (block scope).
|
|
133
|
+
// It runs while the head parses, before `load`. List entries: `{ u: src, k, …script properties }`,
|
|
134
|
+
// `k` = index in STRATEGY_CODES. `l` doubles as the policy's allowlist.
|
|
135
|
+
const STRATEGY_CODES = ["load", "idle", "interaction"];
|
|
136
|
+
export const BOOT_LIST_END = ",d=document,";
|
|
137
|
+
const BOOT_CORE = BOOT_LIST_END +
|
|
138
|
+
'E=addEventListener,p,r=k=>l.map(({u,k:s,...o})=>s==k&&d.head.append(Object.assign(d.createElement("script"),o,{src:p?.createScriptURL(u)??u})));' +
|
|
139
|
+
// The page's one `rivet-script` policy (unlisted URLs become ""; JSON-only scripts for kit JSON-LD), shared with kit's head manager.
|
|
140
|
+
'try{d.$rs={p:p=trustedTypes.createPolicy("rivet-script",{createScriptURL:u=>l.some(s=>s.u==u)?u:"",createScript:c=>JSON.parse(c)&&c}),l}}catch{}';
|
|
141
|
+
// Window-level scroll / wheel listeners are passive already; listeners stay attached as no-ops. Touch
|
|
142
|
+
// interaction arrives as pointerdown.
|
|
143
|
+
const BOOT_IDLE = 'let t,n,g=()=>n||(clearTimeout(t),t=setTimeout(()=>{n=1;(self.requestIdleCallback||setTimeout)(()=>r(1),{timeout:4e3})},150));E("scroll",g);g();';
|
|
144
|
+
const BOOT_INTERACTION = 'let i,x=()=>i||(i=r(2));"pointerdown keydown wheel".split(" ").map(e=>E(e,x,!0));';
|
|
145
|
+
/**
|
|
146
|
+
* Bootstrap for `load` / `idle` / `interaction` scripts: `list` (escaped JSON array) inlined, code
|
|
147
|
+
* for only the strategies the page uses. Injected scripts are async by default.
|
|
148
|
+
*/
|
|
149
|
+
export function _scriptBootstrap(strategies, list = "[]") {
|
|
150
|
+
const used = new Set(strategies);
|
|
151
|
+
const onload = (used.has("load") ? "r(0);" : "") + (used.has("idle") ? BOOT_IDLE : "");
|
|
152
|
+
return "{let l=" + list + BOOT_CORE + (onload ? `E("load",()=>{${onload}});` : "") + (used.has("interaction") ? BOOT_INTERACTION : "") + "}";
|
|
153
|
+
}
|
|
154
|
+
function scriptTag(script, nonce, mark) {
|
|
155
|
+
const module = script.type === "module";
|
|
156
|
+
const strategy = script.strategy ?? "defer";
|
|
157
|
+
const timing = strategy === "eager" ? " async" : strategy === "defer" && !module ? " defer" : "";
|
|
158
|
+
return `<script${module ? ' type="module"' : ""}${timing} src="${escapeAttribute(script.src)}"${attr("id", script.id)}${attr("integrity", script.integrity)}${attr("crossorigin", script.crossorigin)}${attr("referrerpolicy", script.referrerpolicy)}${nonce}${mark}></script>`;
|
|
159
|
+
}
|
|
160
|
+
/** Inline `<style>` (unmanaged: never `data-rh`). CSS never contains a closing tag. */
|
|
161
|
+
const styleTag = (style, nonce) => `<style${attr("media", style.media)}${nonce}>${style.css.replace(/<\/(style)/gi, "<\\/$1")}</style>`;
|
|
162
|
+
/** Inline script: its JSON data block (if any) right before it. Code never contains a closing tag. */
|
|
163
|
+
function inlineTag(script, nonce) {
|
|
164
|
+
if (!isInlineScript(script.inline))
|
|
165
|
+
throw new TypeError("head script `inline` must be an InlineScript (`?script` import or unsafeInlineScript)");
|
|
166
|
+
const data = script.data !== undefined ? `<script type="application/json">${escapeJSON(JSON.stringify(script.data))}</script>` : "";
|
|
167
|
+
return `${data}<script${attr("id", script.id)}${nonce}>${script.inline.code.replace(/<\/(script)/gi, "<\\/$1")}</script>`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Tags for a merged head in capo.js order: base, title, preconnect, async scripts, blocking scripts,
|
|
171
|
+
* stylesheets (`styles` first), preloads, deferred scripts (+ bootstrap), prefetch, meta / SEO links,
|
|
172
|
+
* JSON-LD. Charset, viewport and the CSP meta belong to the shell.
|
|
173
|
+
*/
|
|
174
|
+
export function headTags(head, options = {}) {
|
|
175
|
+
const managed = options.managed;
|
|
176
|
+
const markOf = (entry) => (!managed ? "" : typeof managed === "object" && managed.persistent.has(entry) ? ' data-rh="0"' : ' data-rh=""');
|
|
177
|
+
const nonce = options.nonce !== undefined ? ` nonce="${options.nonce.replace(/[^\w+/=-]/g, "")}"` : "";
|
|
178
|
+
// One string per capo.js group (index = order), appended in place.
|
|
179
|
+
const groups = ["", "", "", "", "", "", "", "", "", "", "", ""];
|
|
180
|
+
const sheets = options.inlineStyles;
|
|
181
|
+
for (const href of options.styles ?? []) {
|
|
182
|
+
const css = sheets && Object.hasOwn(sheets, href) ? sheets[href] : undefined;
|
|
183
|
+
groups[5] += css !== undefined ? styleTag({ css }, nonce) : `<link rel="stylesheet" href="${escapeAttribute(href)}">`;
|
|
184
|
+
}
|
|
185
|
+
// Inline styles after the stylesheets (same group): component rules (motion entrances) win ties over sheet rules.
|
|
186
|
+
const inlineStyles = head?.styles?.map((style) => styleTag(style, nonce)).join("") ?? "";
|
|
187
|
+
if (head?.base)
|
|
188
|
+
groups[0] += `<base href="${escapeAttribute(head.base.href)}"${attr("target", head.base.target)}>`;
|
|
189
|
+
if (head?.title !== undefined)
|
|
190
|
+
groups[1] += `<title>${escapeText(head.title)}</title>`;
|
|
191
|
+
for (const link of head?.links ?? []) {
|
|
192
|
+
const rel = link.rel.toLowerCase();
|
|
193
|
+
const group = rel === "preconnect"
|
|
194
|
+
? 2
|
|
195
|
+
: rel === "stylesheet"
|
|
196
|
+
? 5
|
|
197
|
+
: rel === "preload" || rel === "modulepreload"
|
|
198
|
+
? 6
|
|
199
|
+
: rel === "prefetch" || rel === "dns-prefetch" || rel === "prerender"
|
|
200
|
+
? 8
|
|
201
|
+
: 10;
|
|
202
|
+
groups[group] += linkTag(link, markOf(link));
|
|
203
|
+
}
|
|
204
|
+
groups[5] += inlineStyles;
|
|
205
|
+
const later = [];
|
|
206
|
+
for (const script of head?.scripts ?? []) {
|
|
207
|
+
if ("jsonLd" in script)
|
|
208
|
+
groups[11] += jsonLdTag(script.jsonLd, script.id, markOf(script));
|
|
209
|
+
else if ("inline" in script) {
|
|
210
|
+
const strategy = script.strategy ?? "defer";
|
|
211
|
+
groups[strategy === "eager" ? 3 : strategy === "blocking" ? 4 : 7] += inlineTag(script, nonce);
|
|
212
|
+
}
|
|
213
|
+
else if (deferred(script))
|
|
214
|
+
later.push(script);
|
|
215
|
+
else {
|
|
216
|
+
const strategy = script.strategy ?? "defer";
|
|
217
|
+
groups[strategy === "eager" ? 3 : strategy === "blocking" ? 4 : 7] += scriptTag(script, nonce, markOf(script));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (later.length) {
|
|
221
|
+
const list = later.map((s) => ({
|
|
222
|
+
u: s.src,
|
|
223
|
+
k: STRATEGY_CODES.indexOf(s.strategy),
|
|
224
|
+
...(s.type === "module" ? { type: "module" } : {}),
|
|
225
|
+
...(s.id ? { id: s.id } : {}),
|
|
226
|
+
...(s.integrity ? { integrity: s.integrity } : {}),
|
|
227
|
+
...(s.crossorigin ? { crossOrigin: s.crossorigin } : {}),
|
|
228
|
+
...(s.referrerpolicy ? { referrerPolicy: s.referrerpolicy } : {}),
|
|
229
|
+
}));
|
|
230
|
+
groups[7] += `<script id="rivet-scripts"${nonce}>${_scriptBootstrap(later.map((s) => s.strategy), escapeJSON(JSON.stringify(list)))}</script>`;
|
|
231
|
+
}
|
|
232
|
+
for (const meta of head?.meta ?? [])
|
|
233
|
+
groups[9] += metaTag(meta, markOf(meta));
|
|
234
|
+
return groups.join("");
|
|
235
|
+
}
|
|
236
|
+
/** `<html>` attributes (leading space) for shells: the head's `html` over request defaults (i18n `locale`/`dir`). */
|
|
237
|
+
export function htmlAttributes(head, defaults = {}) {
|
|
238
|
+
const html = { ...defaults, ...head?.html };
|
|
239
|
+
return attr("lang", html.lang) + attr("dir", html.dir) + attr("class", head?.html?.class);
|
|
240
|
+
}
|
|
241
|
+
/** `<body>` attributes (leading space). */
|
|
242
|
+
export const bodyAttributes = (head) => attr("class", head?.body?.class);
|
package/shared/nonce.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// CSP nonces: the one generator behind the node and fetch adapters (Web Crypto, every runtime).
|
|
2
|
+
// 16 fresh CSPRNG bytes per nonce, cut from a pooled 4 KiB `getRandomValues` fill (every byte used once; the pool
|
|
3
|
+
// refills when it runs out) and base64-encoded by table: one `getRandomValues` + `btoa(String.fromCharCode(…))`
|
|
4
|
+
// per request cost ~0.8 µs, this ~0.1 µs.
|
|
5
|
+
const POOL = 4096;
|
|
6
|
+
const pool = new Uint8Array(POOL);
|
|
7
|
+
let at = POOL;
|
|
8
|
+
const DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
9
|
+
/** Fresh 128-bit CSP nonce (base64) for one response's inline scripts. */
|
|
10
|
+
export function nonce() {
|
|
11
|
+
if (at + 16 > POOL) {
|
|
12
|
+
crypto.getRandomValues(pool);
|
|
13
|
+
at = 0;
|
|
14
|
+
}
|
|
15
|
+
let out = "";
|
|
16
|
+
// 5 groups of 3 bytes → 20 digits, then the 16th byte → 2 digits + "==".
|
|
17
|
+
for (let end = at + 15; at < end; at += 3) {
|
|
18
|
+
const n = (pool[at] << 16) | (pool[at + 1] << 8) | pool[at + 2];
|
|
19
|
+
out += DIGITS[n >> 18] + DIGITS[(n >> 12) & 63] + DIGITS[(n >> 6) & 63] + DIGITS[n & 63];
|
|
20
|
+
}
|
|
21
|
+
const last = pool[at++];
|
|
22
|
+
return out + DIGITS[last >> 2] + DIGITS[(last & 3) << 4] + "==";
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FALLBACK = "(function(m,g){const a=document;if(HTMLScriptElement.supports?.(\"speculationrules\"))return;const r=new Set,v=a.createElement(\"link\").relList.supports?.(\"prefetch\"),u=\"rivet:prefetch\",f={priority:\"low\"},y={capture:!0,passive:!0},s=(t,e)=>a.addEventListener(t,e,y);let i,c=null,n=2;try{n=sessionStorage.getItem(u)||2}catch{}const h=t=>{const e=t.target?.closest?.(m);return e&&g?.test(e.pathname)&&!e.matches(\"[data-rivet-reload],[rel~=external]\")?null:e},p=t=>{const e=navigator.connection;if(!t||e?.saveData||/2g/.test(e?.effectiveType))return;const o=t.href.split(\"#\")[0];if(t.origin!==location.origin||o===location.href.split(\"#\")[0]||r.has(o)||n<1)return;r.size>99&&r.delete(r.values().next().value),r.add(o);const d=()=>v?a.head.append(Object.assign(a.createElement(\"link\"),{rel:\"prefetch\",as:\"document\",href:o})):fetch(o,f).catch(()=>{});if(n==\"1\")return void d();n=\"\",fetch(o,{...f,cache:\"no-store\"}).then(l=>{l.body?.cancel();const T=l.headers.get(\"cache-control\")+\",\"+l.headers.get(\"pragma\");n=/no-(store|cache)|^(?!.*stale-while).*max-age=0\\b/.test(T)?\"0\":\"1\";try{sessionStorage.setItem(u,n)}catch{}n==\"1\"&&d()},()=>{n=2,r.delete(o)})};s(\"pointerover\",t=>{const e=h(t)||null;e!==c&&(clearTimeout(i),c=e,e&&(i=setTimeout(p,65,e)))}),s(\"pointerout\",t=>{c&&!c.contains(t.relatedTarget)&&(clearTimeout(i),c=null)});for(const t of[\"focusin\",\"touchstart\",\"pointerdown\"])s(t,e=>p(h(e)))})";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated by build/prefetch-script.ts from client/prefetch-fallback.ts. Do not edit.
|
|
2
|
+
export const FALLBACK = "(function(m,g){const a=document;if(HTMLScriptElement.supports?.(\"speculationrules\"))return;const r=new Set,v=a.createElement(\"link\").relList.supports?.(\"prefetch\"),u=\"rivet:prefetch\",f={priority:\"low\"},y={capture:!0,passive:!0},s=(t,e)=>a.addEventListener(t,e,y);let i,c=null,n=2;try{n=sessionStorage.getItem(u)||2}catch{}const h=t=>{const e=t.target?.closest?.(m);return e&&g?.test(e.pathname)&&!e.matches(\"[data-rivet-reload],[rel~=external]\")?null:e},p=t=>{const e=navigator.connection;if(!t||e?.saveData||/2g/.test(e?.effectiveType))return;const o=t.href.split(\"#\")[0];if(t.origin!==location.origin||o===location.href.split(\"#\")[0]||r.has(o)||n<1)return;r.size>99&&r.delete(r.values().next().value),r.add(o);const d=()=>v?a.head.append(Object.assign(a.createElement(\"link\"),{rel:\"prefetch\",as:\"document\",href:o})):fetch(o,f).catch(()=>{});if(n==\"1\")return void d();n=\"\",fetch(o,{...f,cache:\"no-store\"}).then(l=>{l.body?.cancel();const T=l.headers.get(\"cache-control\")+\",\"+l.headers.get(\"pragma\");n=/no-(store|cache)|^(?!.*stale-while).*max-age=0\\b/.test(T)?\"0\":\"1\";try{sessionStorage.setItem(u,n)}catch{}n==\"1\"&&d()},()=>{n=2,r.delete(o)})};s(\"pointerover\",t=>{const e=h(t)||null;e!==c&&(clearTimeout(i),c=e,e&&(i=setTimeout(p,65,e)))}),s(\"pointerout\",t=>{c&&!c.contains(t.relatedTarget)&&(clearTimeout(i),c=null)});for(const t of[\"focusin\",\"touchstart\",\"pointerdown\"])s(t,e=>p(h(e)))})";
|