@rivetjs/router 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +7 -0
- package/adapters/files.js +103 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +163 -0
- package/adapters/node.js +940 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +92 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +33 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +84 -0
- package/build/codegen.js +920 -0
- package/build/dev-errors.d.ts +19 -0
- package/build/dev-errors.js +236 -0
- package/build/migrate.d.ts +15 -0
- package/build/migrate.js +146 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +50 -0
- package/build/routes.js +202 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +195 -0
- package/build/vite.js +703 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +100 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +74 -0
- package/client/index.d.ts +152 -0
- package/client/index.js +680 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +138 -0
- package/index.js +7 -0
- package/package.json +156 -4
- package/server/app.d.ts +169 -0
- package/server/app.js +302 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +196 -0
- package/server/handler.js +623 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +78 -0
- package/server/render.d.ts +94 -0
- package/server/render.js +489 -0
- package/server/route-cache.d.ts +175 -0
- package/server/route-cache.js +742 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DocumentShell } from "../server/render.js";
|
|
2
|
+
/** What a link loads ahead of a document navigation: the HTML (`document`) or the whole page in the background (`prerender`). */
|
|
3
|
+
export type DocumentPrefetch = "document" | "prerender";
|
|
4
|
+
/**
|
|
5
|
+
* When Chromium acts on the rules (Speculation Rules `eagerness`): `conservative` on pointer / touch
|
|
6
|
+
* down, `moderate` also after the pointer rests on a link ~200 ms, `eager` sooner still (on desktop:
|
|
7
|
+
* a short hover; Chromium decides the exact heuristics).
|
|
8
|
+
*/
|
|
9
|
+
export type PrefetchEagerness = "conservative" | "moderate" | "eager";
|
|
10
|
+
/** Document prefetch options (Speculation Rules with a fallback). */
|
|
11
|
+
export interface DocumentPrefetchOptions {
|
|
12
|
+
/**
|
|
13
|
+
* App default (`rivetRouter({ prefetch })`): links without their own `data-rivet-prefetch` choice
|
|
14
|
+
* (plain `<a>` too) prefetch this way. Absent: only links marked `document` / `prerender`.
|
|
15
|
+
*/
|
|
16
|
+
readonly default?: DocumentPrefetch;
|
|
17
|
+
/**
|
|
18
|
+
* The client router intercepts same-app links (every server / static app with more than one
|
|
19
|
+
* route): those navigations never load a document (the router prefetches their data + page chunk
|
|
20
|
+
* on intent itself), so only links that load documents take part: `data-rivet-reload` /
|
|
21
|
+
* `rel=external` links, and — with `routes` — links whose path no route matches.
|
|
22
|
+
*/
|
|
23
|
+
readonly routed?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* RegExp source over `URL.pathname.slice(1)` matching every path a route of the app (page or
|
|
26
|
+
* endpoint) matches (generated: `routePathRegExp` in `build/codegen.ts`). Links to other paths load
|
|
27
|
+
* documents and take part; endpoint paths never do (only when marked `reload`). Implies `routed`.
|
|
28
|
+
*/
|
|
29
|
+
readonly routes?: string;
|
|
30
|
+
/** Speculation Rules eagerness (`rivetRouter({ prefetchEagerness })`); default `moderate`. The fallback keeps its own intent timing. */
|
|
31
|
+
readonly eagerness?: PrefetchEagerness;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* CSS selector of the links prefetched with `kind` (`"all"`: either kind). Routed apps: only
|
|
35
|
+
* `data-rivet-reload` / `rel=external` links — with `routes`, path-based detection is not a selector
|
|
36
|
+
* (see `speculationRules`), so the selector leaves it out.
|
|
37
|
+
*/
|
|
38
|
+
export declare function prefetchSelector(kind: DocumentPrefetch | "all", options?: DocumentPrefetchOptions): string;
|
|
39
|
+
/**
|
|
40
|
+
* Speculation Rules (Chromium) for a page at `url`: document rules over the prefetch selectors,
|
|
41
|
+
* same origin only (`/*`), not the page itself (either trailing-slash form), `moderate` eagerness by default (pointer resting ~200 ms on a
|
|
42
|
+
* link, or pointer down). The browser applies them to links added later too (no registry needed).
|
|
43
|
+
*/
|
|
44
|
+
export declare function speculationRules(url: URL, options?: DocumentPrefetchOptions): string;
|
|
45
|
+
/** Head tags: the rules (Chromium) and the inline fallback (`<link rel=prefetch>` / `fetch` on intent elsewhere). */
|
|
46
|
+
export declare function prefetchTags(url: URL, nonce: string | undefined, options?: DocumentPrefetchOptions): string;
|
|
47
|
+
/** Wrap a document shell: every page gets the prefetch tags right before `</head>` (generated `entry.server.tsx`). */
|
|
48
|
+
export declare function documentPrefetch(shell: DocumentShell, options?: DocumentPrefetchOptions): DocumentShell;
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` now, or — on a page the browser is prerendering (`<Link prefetch="prerender">`) — once the
|
|
51
|
+
* user actually navigates to it (`prerenderingchange`). For page-view analytics and other "the user
|
|
52
|
+
* saw this page" side effects. No-op outside a browser.
|
|
53
|
+
*/
|
|
54
|
+
export declare function whenActivated(fn: () => void): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { FALLBACK } from "./prefetch-fallback.min.js";
|
|
2
|
+
/** Links the browser would navigate in this tab (no download, no other target). */
|
|
3
|
+
const SAME_TAB = ":not([download],[target]:not([target=_self]))";
|
|
4
|
+
/** Links of a client-routed app that load a document whatever their path (the router never intercepts them). */
|
|
5
|
+
const RELOAD = "[data-rivet-reload],[rel~=external]";
|
|
6
|
+
/**
|
|
7
|
+
* CSS selector of the links prefetched with `kind` (`"all"`: either kind). Routed apps: only
|
|
8
|
+
* `data-rivet-reload` / `rel=external` links — with `routes`, path-based detection is not a selector
|
|
9
|
+
* (see `speculationRules`), so the selector leaves it out.
|
|
10
|
+
*/
|
|
11
|
+
export function prefetchSelector(kind, options = {}) {
|
|
12
|
+
const marked = kind === "all" ? "a[data-rivet-prefetch=document],a[data-rivet-prefetch=prerender]" : `a[data-rivet-prefetch=${kind}]`;
|
|
13
|
+
const any = options.default && (kind === "all" || options.default === kind) ? ",a:not([data-rivet-prefetch])" : "";
|
|
14
|
+
return `:is(${marked}${any})[href]${options.routed && options.routes === undefined ? `:is(${RELOAD})` : ""}${SAME_TAB}`;
|
|
15
|
+
}
|
|
16
|
+
/** URLPattern-escape a path (`:` `*` `?` `+` `(` `)` `{` `}` `\` are syntax). */
|
|
17
|
+
const pattern = (path) => path.replace(/[\\:*?+(){}]/g, "\\$&");
|
|
18
|
+
/** Script text is JSON: `<` escaped so no `</script` can close it. */
|
|
19
|
+
const json = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
20
|
+
/**
|
|
21
|
+
* Speculation Rules (Chromium) for a page at `url`: document rules over the prefetch selectors,
|
|
22
|
+
* same origin only (`/*`), not the page itself (either trailing-slash form), `moderate` eagerness by default (pointer resting ~200 ms on a
|
|
23
|
+
* link, or pointer down). The browser applies them to links added later too (no registry needed).
|
|
24
|
+
*/
|
|
25
|
+
export function speculationRules(url, options = {}) {
|
|
26
|
+
// The page's own path with or without its trailing slash (`/a` and `/a/` are the same page by default).
|
|
27
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
28
|
+
const self = path ? pattern(path) + "{/}?" : "/";
|
|
29
|
+
// Routed app with its route paths: a reload link, or a path no route matches (URLPattern regexp group).
|
|
30
|
+
const routes = options.routes;
|
|
31
|
+
const documents = routes === undefined ? [] : [{ or: [{ selector_matches: RELOAD }, { not: { href_matches: routes ? "/(" + routes + ")" : "/" } }] }];
|
|
32
|
+
const rule = (kind) => [
|
|
33
|
+
{
|
|
34
|
+
source: "document",
|
|
35
|
+
where: { and: [{ href_matches: "/*" }, { not: { href_matches: self } }, { selector_matches: prefetchSelector(kind, options) }, ...documents] },
|
|
36
|
+
eagerness: options.eagerness ?? "moderate",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
return json({ prefetch: rule("document"), prerender: rule("prerender") });
|
|
40
|
+
}
|
|
41
|
+
/** Head tags: the rules (Chromium) and the inline fallback (`<link rel=prefetch>` / `fetch` on intent elsewhere). */
|
|
42
|
+
export function prefetchTags(url, nonce, options = {}) {
|
|
43
|
+
const attr = nonce !== undefined ? ` nonce="${nonce.replace(/[^\w+/=-]/g, "")}"` : "";
|
|
44
|
+
const selector = prefetchSelector("all", options);
|
|
45
|
+
// Routed app with its route paths: the fallback applies the same path rule (a RegExp literal: the source
|
|
46
|
+
// escapes every `/`, and `<` / line terminators are percent-encoded in it).
|
|
47
|
+
const routes = options.routes === undefined ? "" : ",/^\\/(?:" + options.routes + ")$/";
|
|
48
|
+
return (`<script type="speculationrules"${attr}>${speculationRules(url, options)}</script>` + `<script${attr}>${FALLBACK}(${json(selector)}${routes})</script>`);
|
|
49
|
+
}
|
|
50
|
+
/** Wrap a document shell: every page gets the prefetch tags right before `</head>` (generated `entry.server.tsx`). */
|
|
51
|
+
export function documentPrefetch(shell, options = {}) {
|
|
52
|
+
// Written into the page as a RegExp literal: no `<` (`</script`), no line terminator, no unescaped `/`.
|
|
53
|
+
if (options.routes !== undefined && /[<\n\r\u2028\u2029]|(^|[^\\])(\\\\)*\//.test(options.routes))
|
|
54
|
+
throw new TypeError("documentPrefetch: invalid routes source");
|
|
55
|
+
return (input) => {
|
|
56
|
+
const parts = shell(input);
|
|
57
|
+
const tags = prefetchTags(input.url, input.nonce, options);
|
|
58
|
+
const at = parts.start.lastIndexOf("</head>");
|
|
59
|
+
return at < 0 ? { start: parts.start + tags, end: parts.end } : { start: parts.start.slice(0, at) + tags + parts.start.slice(at), end: parts.end };
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Run `fn` now, or — on a page the browser is prerendering (`<Link prefetch="prerender">`) — once the
|
|
64
|
+
* user actually navigates to it (`prerenderingchange`). For page-view analytics and other "the user
|
|
65
|
+
* saw this page" side effects. No-op outside a browser.
|
|
66
|
+
*/
|
|
67
|
+
export function whenActivated(fn) {
|
|
68
|
+
if (typeof document === "undefined")
|
|
69
|
+
return;
|
|
70
|
+
if (document.prerendering)
|
|
71
|
+
document.addEventListener("prerenderingchange", () => fn(), { once: true });
|
|
72
|
+
else
|
|
73
|
+
fn();
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Absolute URL (has a scheme). */
|
|
2
|
+
export declare const ABSOLUTE: RegExp;
|
|
3
|
+
/** Why `location` cannot be a same-origin redirect target as written, or undefined when it can. */
|
|
4
|
+
export declare function unsafeTarget(location: string): string | undefined;
|
|
5
|
+
/** `target` when it is a same-origin path (`/a/b?c#d`), else `fallback`. Never an absolute or protocol-relative URL. */
|
|
6
|
+
export declare function safeRedirect(target: unknown, fallback?: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Open-redirect guards: `redirect()` targets and user-supplied targets (`?redirectTo=`).
|
|
2
|
+
/** Control characters, backslashes or a leading `//` can make browsers resolve a path to another origin. */
|
|
3
|
+
const UNSAFE = /[\u0000-\u001f\u007f\\]|^\s|^\/\//;
|
|
4
|
+
/** Absolute URL (has a scheme). */
|
|
5
|
+
export const ABSOLUTE = /^[a-z][a-z\d+.-]*:/i;
|
|
6
|
+
/** Why `location` cannot be a same-origin redirect target as written, or undefined when it can. */
|
|
7
|
+
export function unsafeTarget(location) {
|
|
8
|
+
if (UNSAFE.test(location))
|
|
9
|
+
return "protocol-relative, backslash or control characters";
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
/** `target` when it is a same-origin path (`/a/b?c#d`), else `fallback`. Never an absolute or protocol-relative URL. */
|
|
13
|
+
export function safeRedirect(target, fallback = "/") {
|
|
14
|
+
if (typeof target !== "string" || !target.startsWith("/") || unsafeTarget(target))
|
|
15
|
+
return fallback;
|
|
16
|
+
const base = "http://rivet.invalid";
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(target, base);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
return url.origin === base ? url.pathname + url.search + url.hash : fallback;
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Trailing slash policy: keep URLs as written, strip the slash, or add it. */
|
|
2
|
+
export type TrailingSlash = "ignore" | "never" | "always";
|
|
3
|
+
/** The app's policy (rewritten by `build/vite.ts` when `rivetRouter({ trailingSlash })` is set). */
|
|
4
|
+
export declare const TRAILING_SLASH: TrailingSlash;
|
|
5
|
+
/** A URL path in canonical form; `/` (and `ignore`) unchanged. `always` leaves file paths (`/sitemap.xml`) unslashed. */
|
|
6
|
+
export declare function slashPath(pathname: string, policy: TrailingSlash): string;
|
|
7
|
+
/** A path href (`/a?q#h`) with its path in canonical form; other forms (full, relative URLs) unchanged. */
|
|
8
|
+
export declare function slashHref(href: string, policy?: TrailingSlash): string;
|
package/shared/slash.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Trailing-slash policy (`rivetRouter({ trailingSlash })`). `"ignore"` (default): `/a` and `/a/` are
|
|
2
|
+
// the same page, URLs stay as written. `"never"` / `"always"`: one canonical form; page GET/HEAD
|
|
3
|
+
// requests for the other answer 308 (server/handler.ts), `<Link>` / `navigate()` hrefs are written in
|
|
4
|
+
// it and the static adapter lays files out for it. `TRAILING_SLASH` is a build-time constant: the
|
|
5
|
+
// router's Vite plugin rewrites it in this module only when the option is set, so apps without it
|
|
6
|
+
// fold the canonicalization away (and their bundles stay byte-identical).
|
|
7
|
+
/** The app's policy (rewritten by `build/vite.ts` when `rivetRouter({ trailingSlash })` is set). */
|
|
8
|
+
export const TRAILING_SLASH = "ignore";
|
|
9
|
+
/** A last path segment with a file extension (`/sitemap.xml`, `/a/b.json`; not a dot-file like `/.well-known`). */
|
|
10
|
+
const FILE = /[^/]\.[^./]+$/;
|
|
11
|
+
/** A URL path in canonical form; `/` (and `ignore`) unchanged. `always` leaves file paths (`/sitemap.xml`) unslashed. */
|
|
12
|
+
export function slashPath(pathname, policy) {
|
|
13
|
+
if (policy === "ignore" || pathname === "/")
|
|
14
|
+
return pathname;
|
|
15
|
+
const has = pathname.endsWith("/");
|
|
16
|
+
return policy === "never" ? (has ? pathname.slice(0, -1) : pathname) : has || FILE.test(pathname) ? pathname : pathname + "/";
|
|
17
|
+
}
|
|
18
|
+
/** A path href (`/a?q#h`) with its path in canonical form; other forms (full, relative URLs) unchanged. */
|
|
19
|
+
export function slashHref(href, policy = TRAILING_SLASH) {
|
|
20
|
+
if (policy === "ignore" || !href.startsWith("/") || href.startsWith("//"))
|
|
21
|
+
return href;
|
|
22
|
+
const at = href.search(/[?#]/);
|
|
23
|
+
return at < 0 ? slashPath(href, policy) : slashPath(href.slice(0, at), policy) + href.slice(at);
|
|
24
|
+
}
|