@rivetjs/router 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/adapters/assemble.d.ts +32 -0
- package/adapters/assemble.js +32 -0
- package/adapters/build.d.ts +24 -0
- package/adapters/build.js +43 -0
- package/adapters/bun.d.ts +18 -0
- package/adapters/bun.js +44 -0
- package/adapters/cloudflare.d.ts +5 -0
- package/adapters/cloudflare.js +8 -0
- package/adapters/cluster.d.ts +7 -0
- package/adapters/cluster.js +130 -0
- package/adapters/csr.d.ts +12 -0
- package/adapters/csr.js +11 -0
- package/adapters/deno.d.ts +13 -0
- package/adapters/deno.js +35 -0
- package/adapters/fetch.d.ts +55 -0
- package/adapters/fetch.js +72 -0
- package/adapters/files.d.ts +20 -0
- package/adapters/files.js +155 -0
- package/adapters/idle-memory.d.ts +23 -0
- package/adapters/idle-memory.js +91 -0
- package/adapters/netlify.d.ts +13 -0
- package/adapters/netlify.js +15 -0
- package/adapters/node.d.ts +180 -0
- package/adapters/node.js +986 -0
- package/adapters/pack.d.ts +39 -0
- package/adapters/pack.js +360 -0
- package/adapters/server-target.d.ts +17 -0
- package/adapters/server-target.js +8 -0
- package/adapters/static.d.ts +84 -0
- package/adapters/static.js +231 -0
- package/adapters/target-build.d.ts +95 -0
- package/adapters/target-build.js +118 -0
- package/adapters/vercel-edge.d.ts +7 -0
- package/adapters/vercel-edge.js +9 -0
- package/adapters/vercel.d.ts +35 -0
- package/adapters/vercel.js +45 -0
- package/build/codegen.d.ts +97 -0
- package/build/codegen.js +1037 -0
- package/build/dev-errors.d.ts +23 -0
- package/build/dev-errors.js +247 -0
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +31 -0
- package/build/migrate.js +385 -0
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/route-css.d.ts +54 -0
- package/build/route-css.js +216 -0
- package/build/routes.d.ts +55 -0
- package/build/routes.js +219 -0
- package/build/sync.d.ts +20 -0
- package/build/sync.js +16 -0
- package/build/vite.d.ts +208 -0
- package/build/vite.js +861 -0
- package/cli/rivet.d.ts +2 -0
- package/cli/rivet.js +101 -0
- package/client/active.d.ts +6 -0
- package/client/active.js +22 -0
- package/client/announce.d.ts +2 -0
- package/client/announce.js +21 -0
- package/client/dev.d.ts +41 -0
- package/client/dev.js +91 -0
- package/client/index.d.ts +156 -0
- package/client/index.js +714 -0
- package/client/layouts.d.ts +110 -0
- package/client/layouts.js +310 -0
- package/client/leave.d.ts +40 -0
- package/client/leave.js +75 -0
- package/client/link.d.ts +48 -0
- package/client/link.jsx +65 -0
- package/client/live.d.ts +57 -0
- package/client/live.js +170 -0
- package/client/navigate.d.ts +71 -0
- package/client/navigate.js +120 -0
- package/client/navigation.d.ts +28 -0
- package/client/navigation.js +14 -0
- package/client/params.d.ts +16 -0
- package/client/params.js +28 -0
- package/client/route-context.d.ts +32 -0
- package/client/route-context.js +21 -0
- package/client/scroll.d.ts +16 -0
- package/client/scroll.js +80 -0
- package/client/search.d.ts +8 -0
- package/client/search.js +17 -0
- package/client/tailwind.css +8 -0
- package/client/transition-helpers.d.ts +28 -0
- package/client/transition-helpers.js +63 -0
- package/client/transitions.css +90 -0
- package/client/transitions.d.ts +21 -0
- package/client/transitions.js +156 -0
- package/index.d.ts +135 -0
- package/index.js +8 -0
- package/package.json +156 -4
- package/server/app.d.ts +175 -0
- package/server/app.js +335 -0
- package/server/body.d.ts +20 -0
- package/server/body.js +8 -0
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/early-hints.d.ts +34 -0
- package/server/early-hints.js +63 -0
- package/server/handler.d.ts +240 -0
- package/server/handler.js +661 -0
- package/server/index.d.ts +9 -0
- package/server/index.js +6 -0
- package/server/inline-styles.d.ts +21 -0
- package/server/inline-styles.js +28 -0
- package/server/module.d.ts +14 -0
- package/server/module.js +96 -0
- package/server/render.d.ts +96 -0
- package/server/render.js +492 -0
- package/server/route-cache.d.ts +245 -0
- package/server/route-cache.js +813 -0
- package/server/security.d.ts +18 -0
- package/server/security.js +47 -0
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/server/strict-csp.d.ts +7 -0
- package/server/strict-csp.js +18 -0
- package/server-only.d.ts +1 -0
- package/server-only.js +3 -0
- package/shared/actions.d.ts +10 -0
- package/shared/actions.js +18 -0
- package/shared/base.d.ts +8 -0
- package/shared/base.js +20 -0
- package/shared/cookie.d.ts +7 -0
- package/shared/cookie.js +32 -0
- package/shared/csp-directives.d.ts +9 -0
- package/shared/csp-directives.js +85 -0
- package/shared/csp-hash.d.ts +39 -0
- package/shared/csp-hash.js +121 -0
- package/shared/csp.d.ts +26 -0
- package/shared/csp.js +13 -0
- package/shared/data.d.ts +175 -0
- package/shared/data.js +22 -0
- package/shared/errors.d.ts +31 -0
- package/shared/errors.js +51 -0
- package/shared/head.d.ts +57 -0
- package/shared/head.js +242 -0
- package/shared/nonce.d.ts +2 -0
- package/shared/nonce.js +23 -0
- package/shared/prefetch-fallback.min.d.ts +1 -0
- package/shared/prefetch-fallback.min.js +2 -0
- package/shared/prefetch.d.ts +54 -0
- package/shared/prefetch.js +74 -0
- package/shared/redirect.d.ts +6 -0
- package/shared/redirect.js +25 -0
- package/shared/server-exports.d.ts +78 -0
- package/shared/server-exports.js +77 -0
- package/shared/slash.d.ts +8 -0
- package/shared/slash.js +24 -0
package/client/live.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type Signal } from "@rivetjs/reactivity";
|
|
2
|
+
import { type DataSource, type Navigator } from "./index.js";
|
|
3
|
+
import { type LayoutTable, type Layouts } from "./layouts.js";
|
|
4
|
+
/** @internal The App's live-page slot (resume state carries it): the shown live page's data signal and URL. */
|
|
5
|
+
export interface LivePage {
|
|
6
|
+
/** Data source of the app (`server` / `static`). */
|
|
7
|
+
readonly m: DataSource;
|
|
8
|
+
/** The shown live page's data signal (unset: no live page rendered yet). */
|
|
9
|
+
s?: Signal<unknown>;
|
|
10
|
+
/** Its URL (`pathname + search`). */
|
|
11
|
+
u?: string;
|
|
12
|
+
/** Its index in the route data (= its layout count). */
|
|
13
|
+
d?: number;
|
|
14
|
+
/** Refresh generation: only the newest in-place refresh applies. */
|
|
15
|
+
n?: number;
|
|
16
|
+
/** Single-level apps: the top Router's key, set (`equals: false`: a same-URL revalidation re-runs the Router)
|
|
17
|
+
* on every location change except a fragment-only one or a search-only one of the shown live page. */
|
|
18
|
+
k?: Signal<string>;
|
|
19
|
+
/** The shown URL without its fragment (what `k` was last set for, or the search-only URL refreshed in place). */
|
|
20
|
+
h?: string;
|
|
21
|
+
/** `k` follows the location (effect started by the first navigation entry point). */
|
|
22
|
+
a?: true;
|
|
23
|
+
/** The data signal whose reader chunks were warmed (`_liveIntent`: once per page render). */
|
|
24
|
+
w?: Signal<unknown>;
|
|
25
|
+
}
|
|
26
|
+
type Located = {
|
|
27
|
+
location(): URL;
|
|
28
|
+
};
|
|
29
|
+
/** @internal Generated `App`: the live-page slot (`router`: single-level apps, whose top Router keys on `k`). */
|
|
30
|
+
export declare function _live(m: DataSource, router?: Located): LivePage;
|
|
31
|
+
/**
|
|
32
|
+
* @internal Generated `App` intent listeners (focusin, pointerdown): an interaction inside a form of the shown live
|
|
33
|
+
* page warms the chunks of the resumed bindings that read its data (and the layouts' data), so a submit's in-place
|
|
34
|
+
* refresh needs no round trip after the answer. Resumed pages only (the resume runtime's `rivet.warm` hook).
|
|
35
|
+
*/
|
|
36
|
+
export declare function _liveIntent(event: Event, live: LivePage, state?: Layouts): void;
|
|
37
|
+
/** @internal Generated `App`: the top Router's navigator — route work keyed on the slot's `k`. */
|
|
38
|
+
export declare const _liveRouter: (router: Located, live: LivePage) => Located;
|
|
39
|
+
/**
|
|
40
|
+
* @internal Generated `App` navigation entry points (link click, popstate, `rivet:go`): once, start the effect
|
|
41
|
+
* that moves `k` with the location — except for a search-only change of the shown live page, refreshed in place.
|
|
42
|
+
*/
|
|
43
|
+
export declare function _armLive(router: Located & Navigator, live: LivePage, state?: Layouts, table?: LayoutTable): void;
|
|
44
|
+
/**
|
|
45
|
+
* @internal Generated `App` of a nested-layout app (`_sync`'s `inPlace`): a search-only change of the shown live
|
|
46
|
+
* page (`from`: the URL `_sync` saw last) refreshes it in place — no level key moves, the layouts keep their data
|
|
47
|
+
* unless their loaders read the URL. A failed loader re-renders the page level (`_bump` + `_sync`).
|
|
48
|
+
*/
|
|
49
|
+
export declare function _liveLevels(router: Located & Navigator, live: LivePage, state: Layouts, table: LayoutTable, from: string, url: URL): boolean;
|
|
50
|
+
/** @internal Generated route render of a live page: its `data` accessor (a new signal per render). */
|
|
51
|
+
export declare function _livePage(slot: LivePage, value: unknown, url: URL, depth: number): () => unknown;
|
|
52
|
+
/**
|
|
53
|
+
* @internal Generated `App` (`rivet:go`) when some page is live: same-URL revalidations of the shown
|
|
54
|
+
* live page refresh in place; everything else is served like `_serve`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function _serveLive(router: Navigator, live: LivePage, state?: Layouts, table?: LayoutTable): void;
|
|
57
|
+
export {};
|
package/client/live.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Live pages (`export const live = true` in a page module): the page's `data` prop is an accessor
|
|
2
|
+
// backed by a signal, and same-URL revalidation (an enhanced form submit, `invalidate()`, an
|
|
3
|
+
// `invalidate(key)` that reruns the page loader) updates it — and the layouts that reran — in place.
|
|
4
|
+
// The page is not re-rendered: page-local state (an open drawer, typed input, focus, scroll) survives.
|
|
5
|
+
// Search-only navigations of the shown live page (`?new` with JS, `setSearchParams`, back / forward between
|
|
6
|
+
// them) refresh it in place too (single-level apps: the top Router keys on the slot's `k`, not the URL; nested
|
|
7
|
+
// layout levels: `_sync` leaves every level key as it is, `_liveLevels`).
|
|
8
|
+
// Used by the generated `App` only when some page is live (pay-per-use: apps without one never import it).
|
|
9
|
+
import { batch, effect, signal, untrack } from "@rivetjs/reactivity";
|
|
10
|
+
import { _clearPrefetch, _current, _dropPrefetch, _inPlace, _perform, routeData } from "./index.js";
|
|
11
|
+
import { _bump, _depsLoaded, _invalidating, _keepIds, _plan, _refreshed, _stale, _sync } from "./layouts.js";
|
|
12
|
+
import { _take } from "./navigate.js";
|
|
13
|
+
const pageOf = (url) => url.href.split("#")[0];
|
|
14
|
+
/** @internal Generated `App`: the live-page slot (`router`: single-level apps, whose top Router keys on `k`). */
|
|
15
|
+
export function _live(m, router) {
|
|
16
|
+
if (!router)
|
|
17
|
+
return { m };
|
|
18
|
+
const h = pageOf(untrack(router.location));
|
|
19
|
+
return { m, k: signal(h, { equals: false }), h };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @internal Generated `App` intent listeners (focusin, pointerdown): an interaction inside a form of the shown live
|
|
23
|
+
* page warms the chunks of the resumed bindings that read its data (and the layouts' data), so a submit's in-place
|
|
24
|
+
* refresh needs no round trip after the answer. Resumed pages only (the resume runtime's `rivet.warm` hook).
|
|
25
|
+
*/
|
|
26
|
+
export function _liveIntent(event, live, state) {
|
|
27
|
+
const warm = globalThis[Symbol.for("rivet.warm")];
|
|
28
|
+
if (!warm || !live.s || live.w === live.s || !event.target?.closest?.("form"))
|
|
29
|
+
return;
|
|
30
|
+
live.w = live.s;
|
|
31
|
+
warm(live.s);
|
|
32
|
+
if (state)
|
|
33
|
+
for (const layout of state.l)
|
|
34
|
+
if (layout)
|
|
35
|
+
warm(layout.d);
|
|
36
|
+
}
|
|
37
|
+
/** @internal Generated `App`: the top Router's navigator — route work keyed on the slot's `k`. */
|
|
38
|
+
export const _liveRouter = (router, live) => ({
|
|
39
|
+
location() {
|
|
40
|
+
live.k();
|
|
41
|
+
return untrack(router.location);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* @internal Generated `App` navigation entry points (link click, popstate, `rivet:go`): once, start the effect
|
|
46
|
+
* that moves `k` with the location — except for a search-only change of the shown live page, refreshed in place.
|
|
47
|
+
*/
|
|
48
|
+
export function _armLive(router, live, state, table) {
|
|
49
|
+
if (live.a || !live.k)
|
|
50
|
+
return;
|
|
51
|
+
live.a = true;
|
|
52
|
+
let seen;
|
|
53
|
+
effect(() => {
|
|
54
|
+
const url = router.location();
|
|
55
|
+
untrack(() => {
|
|
56
|
+
const shown = live.h, page = pageOf(url),
|
|
57
|
+
// The same full URL again: a revalidation (the runtime router re-set it); else a fragment-only change.
|
|
58
|
+
again = url.href === seen;
|
|
59
|
+
seen = url.href;
|
|
60
|
+
if (page === shown)
|
|
61
|
+
return void (again && live.k.set(page));
|
|
62
|
+
const from = new URL(shown);
|
|
63
|
+
live.h = page;
|
|
64
|
+
if (live.s && live.u === from.pathname + from.search && url.pathname === from.pathname) {
|
|
65
|
+
// The shown live page, another search (same route and params): the Router keeps it, its data refreshes in place.
|
|
66
|
+
live.u = url.pathname + url.search;
|
|
67
|
+
void refresh(router, live, url, undefined, state && table ? _keepIds(state, table, url) : undefined, _inPlace, state, table, () => live.k.set(live.h));
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
live.k.set(page);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* @internal Generated `App` of a nested-layout app (`_sync`'s `inPlace`): a search-only change of the shown live
|
|
76
|
+
* page (`from`: the URL `_sync` saw last) refreshes it in place — no level key moves, the layouts keep their data
|
|
77
|
+
* unless their loaders read the URL. A failed loader re-renders the page level (`_bump` + `_sync`).
|
|
78
|
+
*/
|
|
79
|
+
export function _liveLevels(router, live, state, table, from, url) {
|
|
80
|
+
const shown = new URL(from);
|
|
81
|
+
if (!live.s || live.u !== shown.pathname + shown.search || url.pathname !== shown.pathname || url.search === shown.search)
|
|
82
|
+
return false;
|
|
83
|
+
live.u = url.pathname + url.search;
|
|
84
|
+
void refresh(router, live, url, undefined, _keepIds(state, table, url), _inPlace, state, table, () => {
|
|
85
|
+
_bump();
|
|
86
|
+
_sync(router, state, table);
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/** @internal Generated route render of a live page: its `data` accessor (a new signal per render). */
|
|
91
|
+
export function _livePage(slot, value, url, depth) {
|
|
92
|
+
const data = signal(value);
|
|
93
|
+
slot.s = data;
|
|
94
|
+
slot.u = url.pathname + url.search;
|
|
95
|
+
slot.d = depth;
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* @internal Generated `App` (`rivet:go`) when some page is live: same-URL revalidations of the shown
|
|
100
|
+
* live page refresh in place; everything else is served like `_serve`.
|
|
101
|
+
*/
|
|
102
|
+
export function _serveLive(router, live, state, table) {
|
|
103
|
+
for (const request of _take())
|
|
104
|
+
if (!inPlace(router, live, request, state, table))
|
|
105
|
+
_perform(router, request, state, table);
|
|
106
|
+
}
|
|
107
|
+
function inPlace(router, live, request, state, table) {
|
|
108
|
+
const { url, done } = request;
|
|
109
|
+
const target = request.invalidate;
|
|
110
|
+
// Not a revalidation, no client router, another page shown, or a navigation in flight: the regular path.
|
|
111
|
+
if (!target || !router.navigate || !live.s || live.u !== url.pathname + url.search || _current())
|
|
112
|
+
return false;
|
|
113
|
+
let keep;
|
|
114
|
+
if (typeof target === "function") {
|
|
115
|
+
_dropPrefetch();
|
|
116
|
+
const plan = _plan(state, table, url, target);
|
|
117
|
+
// Nothing matched, or layouts only (they refresh in place already): the regular path.
|
|
118
|
+
if (plan && !plan.page)
|
|
119
|
+
return false;
|
|
120
|
+
// The page's loader reruns; layouts whose keys did not match stay kept.
|
|
121
|
+
if (plan) {
|
|
122
|
+
_invalidating(url, plan.drop);
|
|
123
|
+
keep = _keepIds(state, table, url);
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
_stale();
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
_clearPrefetch();
|
|
130
|
+
void refresh(router, live, url, typeof target === "object" ? target : undefined, keep, done, state, table);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
async function refresh(router, live, url, fresh, keep, done, state, table,
|
|
134
|
+
// A search navigation: its Router commit on a failed loader (else none: the regular rerun).
|
|
135
|
+
search) {
|
|
136
|
+
const n = (live.n = (live.n ?? 0) + 1);
|
|
137
|
+
let result;
|
|
138
|
+
if (fresh?.data) {
|
|
139
|
+
// An enhanced action's single-flight answer: no request.
|
|
140
|
+
result = { data: fresh.data };
|
|
141
|
+
_depsLoaded(url, fresh.data.length, fresh.deps);
|
|
142
|
+
if (fresh.head?.title !== undefined)
|
|
143
|
+
document.title = fresh.head.title;
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
try {
|
|
147
|
+
// Detached from any navigation (a redirect answer navigates and never settles).
|
|
148
|
+
result = await routeData({ signal: new AbortController().signal, get: () => null }, { url }, router, live.m, keep);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A failed loader: the page re-renders with the route's error view (a search navigation: its Router commit).
|
|
152
|
+
if (!search)
|
|
153
|
+
return _perform(router, { url, invalidate: true, done }, state, table);
|
|
154
|
+
if (n === live.n)
|
|
155
|
+
search();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// A newer refresh, or a navigation that started meanwhile, wins (a search navigation: the one in flight is its own).
|
|
159
|
+
const nav = _current();
|
|
160
|
+
if (n === live.n && (!nav || (search && nav.to.pathname + nav.to.search === live.u)) && live.u === location.pathname + location.search) {
|
|
161
|
+
batch(() => {
|
|
162
|
+
if (state && table)
|
|
163
|
+
_refreshed(state, table, url, result);
|
|
164
|
+
live.s.set(result.data[live.d]);
|
|
165
|
+
});
|
|
166
|
+
done();
|
|
167
|
+
}
|
|
168
|
+
else if (!search)
|
|
169
|
+
done();
|
|
170
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { LinkHref } from "../index.js";
|
|
2
|
+
import type { Head, LoaderDeps } from "../shared/data.js";
|
|
3
|
+
/** Options of `navigate()`. */
|
|
4
|
+
export interface NavigateOptions {
|
|
5
|
+
/** Replace the current history entry instead of pushing one. */
|
|
6
|
+
replace?: boolean;
|
|
7
|
+
/** App keys merged into the new entry's `history.state` (the router adds `i` and `rivet`). */
|
|
8
|
+
state?: Readonly<Record<string, unknown>>;
|
|
9
|
+
/** `false`: keep the scroll position instead of scrolling to the top / `#hash` target. */
|
|
10
|
+
scroll?: boolean;
|
|
11
|
+
/** `false`: leave focus where it is instead of moving it to the new page's heading. */
|
|
12
|
+
focus?: boolean;
|
|
13
|
+
/** View transition types for this navigation (like `<Link transition>`); `false`: no animation. */
|
|
14
|
+
transition?: string | readonly string[] | false;
|
|
15
|
+
}
|
|
16
|
+
/** @internal A queued request, served by the app router (`_serve`). */
|
|
17
|
+
export interface NavigateRequest extends NavigateOptions {
|
|
18
|
+
readonly url: URL;
|
|
19
|
+
/** Same URL, fresh route data (`invalidate()`); data already in hand skips the request; a key test reruns matching loaders only. */
|
|
20
|
+
readonly invalidate?: true | FreshData | ((key: string) => boolean);
|
|
21
|
+
readonly done: () => void;
|
|
22
|
+
}
|
|
23
|
+
/** @internal Take the queued requests (the app router serves them). */
|
|
24
|
+
export declare const _take: () => NavigateRequest[];
|
|
25
|
+
/**
|
|
26
|
+
* @internal The router brand's entry (`_brand`, booted App only: its `rivet:go` listener serves the request): a
|
|
27
|
+
* navigation (outside the base path: a document load) or, with `invalidate`, fresh data for the shown URL.
|
|
28
|
+
*/
|
|
29
|
+
export declare function _go(url: URL, invalidate?: true | FreshData): Promise<void>;
|
|
30
|
+
/** Route data in the data-endpoint shape (an enhanced action's single-flight answer). */
|
|
31
|
+
export interface FreshData {
|
|
32
|
+
readonly data?: readonly unknown[];
|
|
33
|
+
readonly head?: Head;
|
|
34
|
+
/** The loaders' `event.depends()` keys (the answer's `deps`). */
|
|
35
|
+
readonly deps?: LoaderDeps;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What `invalidate()` reruns: loaders that called `event.depends(key)` with a matching key — one key
|
|
39
|
+
* (a `URL` counts as its `href`), any of a list, or those a predicate accepts (it gets each key).
|
|
40
|
+
*/
|
|
41
|
+
export type InvalidateTarget = string | URL | readonly (string | URL)[] | ((key: string) => boolean);
|
|
42
|
+
/**
|
|
43
|
+
* Navigate like a link click, from anywhere (event handlers, effects, other libraries): same
|
|
44
|
+
* `rivet:navigate` (cancelable) event, transitions, pending state, announcer, focus and scroll
|
|
45
|
+
* bookkeeping. Resolves once the new page committed (or immediately for a hash-only change, a
|
|
46
|
+
* canceled navigation or the current URL). `to` strings starting with `/` are app paths (the URL base
|
|
47
|
+
* path is added). Cross-origin URLs, URLs outside the base and pages without a client router load
|
|
48
|
+
* the document (`location.assign`; the promise then never settles). Non-HTTP(S) URLs reject.
|
|
49
|
+
*/
|
|
50
|
+
export declare function navigate(to: LinkHref | (string & {}) | URL, options?: NavigateOptions): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Re-run the current page's loaders (fresh route data for the same URL) and re-render it in place:
|
|
53
|
+
* no history entry, transition, scroll or focus move (the focused control keeps focus by id). Also
|
|
54
|
+
* clears the intent-prefetch cache. `fresh` (data in the data-endpoint shape, such as an enhanced
|
|
55
|
+
* action's single-flight answer) skips the request. Resolves when the fresh page committed; without
|
|
56
|
+
* a client router the document reloads.
|
|
57
|
+
*
|
|
58
|
+
* `invalidate(key | keys | (key) => boolean)`: only the loaders whose `event.depends()` keys match
|
|
59
|
+
* rerun (one data request; the others are kept). The page re-renders when its own loader reruns;
|
|
60
|
+
* otherwise only the layouts that reran update in place. Nothing matches: no request. Keys not known
|
|
61
|
+
* yet (dev mounts, apps without layouts before their first client navigation): every loader reruns.
|
|
62
|
+
*/
|
|
63
|
+
export declare function invalidate(target?: FreshData | InvalidateTarget): Promise<void>;
|
|
64
|
+
/** Alias of `invalidate()`. */
|
|
65
|
+
export declare const revalidate: typeof invalidate;
|
|
66
|
+
/**
|
|
67
|
+
* Navigate to the current path with new search params (the hash is dropped). `update`: the new
|
|
68
|
+
* params, or a function of a copy of the current ones. Like any search change, the route re-runs
|
|
69
|
+
* (loaders, page re-render); scroll and focus stay unless `scroll` / `focus` say otherwise.
|
|
70
|
+
*/
|
|
71
|
+
export declare function setSearchParams(update: URLSearchParams | Record<string, string> | ((current: URLSearchParams) => URLSearchParams | Record<string, string> | void), options?: NavigateOptions): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { underBase, withBase } from "../shared/base.js";
|
|
2
|
+
import { slashPath, TRAILING_SLASH } from "../shared/slash.js";
|
|
3
|
+
const queue = [];
|
|
4
|
+
/** @internal Take the queued requests (the app router serves them). */
|
|
5
|
+
export const _take = () => queue.splice(0);
|
|
6
|
+
/**
|
|
7
|
+
* @internal The router brand's entry (`_brand`, booted App only: its `rivet:go` listener serves the request): a
|
|
8
|
+
* navigation (outside the base path: a document load) or, with `invalidate`, fresh data for the shown URL.
|
|
9
|
+
*/
|
|
10
|
+
export function _go(url, invalidate) {
|
|
11
|
+
if (!invalidate && !underBase(url.pathname))
|
|
12
|
+
return (location.assign(url.href), new Promise(() => { }));
|
|
13
|
+
if (!invalidate && TRAILING_SLASH !== "ignore")
|
|
14
|
+
url.pathname = slashPath(url.pathname, TRAILING_SLASH);
|
|
15
|
+
return new Promise((done) => {
|
|
16
|
+
queue.push({ url, ...(invalidate ? { invalidate } : {}), done });
|
|
17
|
+
document.dispatchEvent(new CustomEvent("rivet:go"));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Unserved requests wait this long for the router, then load the document.
|
|
21
|
+
const WAIT = 10_000;
|
|
22
|
+
function serve(url, options, fallback) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const d = document;
|
|
25
|
+
const ask = () => pending() && d.dispatchEvent(new CustomEvent("rivet:go"));
|
|
26
|
+
let timer;
|
|
27
|
+
// Only the resumed-page wait (below) listens.
|
|
28
|
+
const settle = () => timer && (clearTimeout(timer), d.removeEventListener("rivet:attach", ask));
|
|
29
|
+
const request = { ...options, url, done: () => (settle(), resolve()) };
|
|
30
|
+
const pending = () => queue.includes(request);
|
|
31
|
+
const drop = () => (settle(), queue.splice(queue.indexOf(request), 1), fallback());
|
|
32
|
+
queue.push(request);
|
|
33
|
+
ask();
|
|
34
|
+
if (!pending())
|
|
35
|
+
return;
|
|
36
|
+
// No App listening (the app was built without navigate() detected) or no resume loader: a document load.
|
|
37
|
+
const loader = d.$r;
|
|
38
|
+
if (!loader || !d.querySelector("[data-rivet-go]"))
|
|
39
|
+
return drop();
|
|
40
|
+
// Resumed page: the App's listener attaches when the resume runtime drains its pending listeners (at idle, or
|
|
41
|
+
// before the first delegated handler runs) and says so (`rivet:attach`): ask again then; its chunk loads on the
|
|
42
|
+
// first `rivet:go`. Boot the runtime now; no answer within WAIT: a document load.
|
|
43
|
+
d.addEventListener("rivet:attach", ask);
|
|
44
|
+
timer = setTimeout(() => pending() && drop(), WAIT);
|
|
45
|
+
if (!loader.r?.())
|
|
46
|
+
loader.b?.();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Navigate like a link click, from anywhere (event handlers, effects, other libraries): same
|
|
51
|
+
* `rivet:navigate` (cancelable) event, transitions, pending state, announcer, focus and scroll
|
|
52
|
+
* bookkeeping. Resolves once the new page committed (or immediately for a hash-only change, a
|
|
53
|
+
* canceled navigation or the current URL). `to` strings starting with `/` are app paths (the URL base
|
|
54
|
+
* path is added). Cross-origin URLs, URLs outside the base and pages without a client router load
|
|
55
|
+
* the document (`location.assign`; the promise then never settles). Non-HTTP(S) URLs reject.
|
|
56
|
+
*/
|
|
57
|
+
export function navigate(to, options = {}) {
|
|
58
|
+
if (typeof document === "undefined")
|
|
59
|
+
return Promise.reject(new Error("navigate() needs a browser"));
|
|
60
|
+
// App paths (`/items/1`) get the URL base path; full and relative URLs are taken as they are.
|
|
61
|
+
const url = new URL(typeof to === "string" ? withBase(to) : to, location.href);
|
|
62
|
+
if (!/^https?:$/.test(url.protocol))
|
|
63
|
+
return Promise.reject(new TypeError("navigate() requires an HTTP(S) URL"));
|
|
64
|
+
const load = () => location[options.replace ? "replace" : "assign"](url.href);
|
|
65
|
+
// Another origin, or outside the app's base path: a document load.
|
|
66
|
+
if (url.origin !== location.origin || !underBase(url.pathname)) {
|
|
67
|
+
load();
|
|
68
|
+
return new Promise(() => { });
|
|
69
|
+
}
|
|
70
|
+
// `rivetRouter({ trailingSlash })`: the app's paths in their canonical form (folded away by default).
|
|
71
|
+
if (TRAILING_SLASH !== "ignore")
|
|
72
|
+
url.pathname = slashPath(url.pathname, TRAILING_SLASH);
|
|
73
|
+
return serve(url, options, load);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Re-run the current page's loaders (fresh route data for the same URL) and re-render it in place:
|
|
77
|
+
* no history entry, transition, scroll or focus move (the focused control keeps focus by id). Also
|
|
78
|
+
* clears the intent-prefetch cache. `fresh` (data in the data-endpoint shape, such as an enhanced
|
|
79
|
+
* action's single-flight answer) skips the request. Resolves when the fresh page committed; without
|
|
80
|
+
* a client router the document reloads.
|
|
81
|
+
*
|
|
82
|
+
* `invalidate(key | keys | (key) => boolean)`: only the loaders whose `event.depends()` keys match
|
|
83
|
+
* rerun (one data request; the others are kept). The page re-renders when its own loader reruns;
|
|
84
|
+
* otherwise only the layouts that reran update in place. Nothing matches: no request. Keys not known
|
|
85
|
+
* yet (dev mounts, apps without layouts before their first client navigation): every loader reruns.
|
|
86
|
+
*/
|
|
87
|
+
export function invalidate(target) {
|
|
88
|
+
if (typeof document === "undefined")
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
return serve(new URL(location.href), { invalidate: invalidation(target) }, () => location.reload());
|
|
91
|
+
}
|
|
92
|
+
const href = (key) => (typeof key === "string" ? key : key.href);
|
|
93
|
+
function invalidation(target) {
|
|
94
|
+
if (typeof target === "function")
|
|
95
|
+
return target;
|
|
96
|
+
if (typeof target === "string" || target instanceof URL) {
|
|
97
|
+
const key = href(target);
|
|
98
|
+
return (k) => k === key;
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(target)) {
|
|
101
|
+
const keys = new Set(target.map(href));
|
|
102
|
+
return (k) => keys.has(k);
|
|
103
|
+
}
|
|
104
|
+
return target?.data ? target : true;
|
|
105
|
+
}
|
|
106
|
+
/** Alias of `invalidate()`. */
|
|
107
|
+
export const revalidate = invalidate;
|
|
108
|
+
/**
|
|
109
|
+
* Navigate to the current path with new search params (the hash is dropped). `update`: the new
|
|
110
|
+
* params, or a function of a copy of the current ones. Like any search change, the route re-runs
|
|
111
|
+
* (loaders, page re-render); scroll and focus stay unless `scroll` / `focus` say otherwise.
|
|
112
|
+
*/
|
|
113
|
+
export function setSearchParams(update, options = {}) {
|
|
114
|
+
const url = new URL(location.href);
|
|
115
|
+
const current = new URLSearchParams(url.search);
|
|
116
|
+
const next = typeof update === "function" ? (update(current) ?? current) : update;
|
|
117
|
+
url.search = new URLSearchParams(next).toString();
|
|
118
|
+
url.hash = "";
|
|
119
|
+
return navigate(url, { scroll: false, focus: false, ...options });
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Context, type Signal } from "@rivetjs/reactivity";
|
|
2
|
+
/** How a navigation started. */
|
|
3
|
+
export type NavigationType = "push" | "replace" | "back" | "forward";
|
|
4
|
+
/** State of the client navigation in flight (`useNavigation()`). */
|
|
5
|
+
export interface NavigationState {
|
|
6
|
+
/** `submitting` is reserved for enhanced forms. */
|
|
7
|
+
readonly state: "idle" | "loading" | "submitting";
|
|
8
|
+
/** Destination of the navigation in flight. */
|
|
9
|
+
readonly location: URL | null;
|
|
10
|
+
readonly from: URL | null;
|
|
11
|
+
readonly type: NavigationType | null;
|
|
12
|
+
/** Indicator visible: set after `pending.delay`, kept at least `pending.minDuration`. */
|
|
13
|
+
readonly shown: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Navigation state when nothing is loading. */
|
|
16
|
+
export declare const IDLE: NavigationState;
|
|
17
|
+
/** Browser navigation state (the transitions module writes it; idle on the server and without transitions). */
|
|
18
|
+
export declare const navigation: Signal<NavigationState>;
|
|
19
|
+
/**
|
|
20
|
+
* Resumable navigation context: the server entry provides `navigation` under this id, so resumed
|
|
21
|
+
* regions that read it revive to the browser signal (`createResume({ contexts })`).
|
|
22
|
+
*/
|
|
23
|
+
export declare const Navigation: Context<() => NavigationState>;
|
|
24
|
+
/** @internal Route commits record the shown search and run `search` (set by search.ts when loaded). */
|
|
25
|
+
export declare const _searchSync: {
|
|
26
|
+
shown?: string;
|
|
27
|
+
search?: () => void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Navigation state shared by the router client, the lazy transitions module and resumed components.
|
|
2
|
+
// Kept tiny: the resume entry imports it (the context must exist before any component resumes).
|
|
3
|
+
import { createContext, signal } from "@rivetjs/reactivity";
|
|
4
|
+
/** Navigation state when nothing is loading. */
|
|
5
|
+
export const IDLE = Object.freeze({ state: "idle", location: null, from: null, type: null, shown: false });
|
|
6
|
+
/** Browser navigation state (the transitions module writes it; idle on the server and without transitions). */
|
|
7
|
+
export const navigation = signal(IDLE);
|
|
8
|
+
/**
|
|
9
|
+
* Resumable navigation context: the server entry provides `navigation` under this id, so resumed
|
|
10
|
+
* regions that read it revive to the browser signal (`createResume({ contexts })`).
|
|
11
|
+
*/
|
|
12
|
+
export const Navigation = createContext(navigation, { id: "rivet:navigation" });
|
|
13
|
+
/** @internal Route commits record the shown search and run `search` (set by search.ts when loaded). */
|
|
14
|
+
export const _searchSync = {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Context } from "@rivetjs/reactivity";
|
|
2
|
+
import type { RouteId, RouteParams } from "../index.js";
|
|
3
|
+
/** @internal Route params of the rendered route (generated apps provide it per route render). */
|
|
4
|
+
export declare const RouteParamsContext: Context<() => Readonly<Record<string, string>>>;
|
|
5
|
+
/**
|
|
6
|
+
* The current route's params: `const params = useParams<"items/[id]">(); params().id`. Call during
|
|
7
|
+
* component setup. Pages re-render per navigation, so their value is that page's; nested layouts
|
|
8
|
+
* stay mounted and see the current route's params; the root layout (outside the routes) sees `{}`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useParams<R extends RouteId = never>(): () => Readonly<[R] extends [never] ? Record<string, string> : RouteParams[R]>;
|
|
11
|
+
/**
|
|
12
|
+
* The current URL's search params, updated when a navigation commits (also in the root layout and
|
|
13
|
+
* resumed regions): `const search = useSearchParams(); search().get("q")`. Call during component setup.
|
|
14
|
+
* Treat the value as read-only; change it with `setSearchParams`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useSearchParams(): () => URLSearchParams;
|
package/client/params.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Route params and search params accessors for components under file routes. Both are signals, so a
|
|
2
|
+
// resumed binding that reads them revives without running component code:
|
|
3
|
+
// - params: each page render provides `signal(match.params)` (the page re-renders on every
|
|
4
|
+
// navigation, so the signal of a mounted page never changes; its value resumes with the page);
|
|
5
|
+
// persisted nested layouts get their App-level params signal (updated in place);
|
|
6
|
+
// - search: an identified context (the server provides the request's, resumed regions revive to
|
|
7
|
+
// the browser signal that route commits update), also live in the root layout.
|
|
8
|
+
import { createContext, signal, useContext } from "@rivetjs/reactivity";
|
|
9
|
+
import { SearchParams } from "./search.js";
|
|
10
|
+
const none = signal(Object.freeze({}));
|
|
11
|
+
/** @internal Route params of the rendered route (generated apps provide it per route render). */
|
|
12
|
+
export const RouteParamsContext = createContext(none);
|
|
13
|
+
/**
|
|
14
|
+
* The current route's params: `const params = useParams<"items/[id]">(); params().id`. Call during
|
|
15
|
+
* component setup. Pages re-render per navigation, so their value is that page's; nested layouts
|
|
16
|
+
* stay mounted and see the current route's params; the root layout (outside the routes) sees `{}`.
|
|
17
|
+
*/
|
|
18
|
+
export function useParams() {
|
|
19
|
+
return useContext(RouteParamsContext);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The current URL's search params, updated when a navigation commits (also in the root layout and
|
|
23
|
+
* resumed regions): `const search = useSearchParams(); search().get("q")`. Call during component setup.
|
|
24
|
+
* Treat the value as read-only; change it with `setSearchParams`.
|
|
25
|
+
*/
|
|
26
|
+
export function useSearchParams() {
|
|
27
|
+
return useContext(SearchParams);
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Context } from "@rivetjs/reactivity";
|
|
2
|
+
import type { ActionResult, HeadSink, RoutePayload } from "../shared/data.js";
|
|
3
|
+
import type { ActionName, ActionResultOf, RouteActions } from "../index.js";
|
|
4
|
+
/** Server render input handed to routes through context (never serialized: no id). */
|
|
5
|
+
export interface ServerRoutePayload extends RoutePayload {
|
|
6
|
+
readonly url: URL;
|
|
7
|
+
/** Set when the handler failed the request (rendered by the route's error view). */
|
|
8
|
+
readonly error?: {
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
};
|
|
12
|
+
/** The form action the request performed (no-JS POST). */
|
|
13
|
+
readonly action?: ActionResult;
|
|
14
|
+
/** Server render with `@rivetjs/kit`: the request's component head collector. */
|
|
15
|
+
readonly headSink?: HeadSink;
|
|
16
|
+
/** Layouts whose loader read the URL: never kept by client navigation (unknown: none kept). */
|
|
17
|
+
readonly byURL?: readonly number[];
|
|
18
|
+
}
|
|
19
|
+
/** Context with the route data the server already loaded for this render. */
|
|
20
|
+
export declare const ServerData: Context<ServerRoutePayload | null>;
|
|
21
|
+
/**
|
|
22
|
+
* The result of the form action this page was rendered after (a no-JS POST to `?/name`), when
|
|
23
|
+
* `name` matches (any action when omitted). Call during component setup (server render; dev client
|
|
24
|
+
* mount). In the browser each action result answers once, so later client renders start clean.
|
|
25
|
+
*/
|
|
26
|
+
export declare function actionResult(name?: string): ActionResult | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Typed: `actionResult<"items/[id]">("save")` (route id, generated `RouteActions`) or
|
|
29
|
+
* `actionResult<typeof forms>()` (`forms = actions(…)`). Pass the name as the second type argument to narrow the
|
|
30
|
+
* result to that action (`actionResult<"items/[id]", "save">("save")`).
|
|
31
|
+
*/
|
|
32
|
+
export declare function actionResult<A extends (keyof RouteActions & string) | Record<string, unknown>, N extends ActionName<A> = ActionName<A>>(name?: N): ActionResultOf<A, N> | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Server render input handed to routes through context, and the form action result read from it.
|
|
2
|
+
import { createContext, useContext } from "@rivetjs/reactivity";
|
|
3
|
+
/** Context with the route data the server already loaded for this render. */
|
|
4
|
+
export const ServerData = createContext(null);
|
|
5
|
+
let actionsRead;
|
|
6
|
+
export function actionResult(name) {
|
|
7
|
+
const server = useContext(ServerData);
|
|
8
|
+
const action = server?.action;
|
|
9
|
+
if (!server || !action || (name !== undefined && action.name !== name))
|
|
10
|
+
return undefined;
|
|
11
|
+
if (typeof document !== "undefined") {
|
|
12
|
+
actionsRead ??= new WeakMap();
|
|
13
|
+
let read = actionsRead.get(server);
|
|
14
|
+
if (!read)
|
|
15
|
+
actionsRead.set(server, (read = new Set()));
|
|
16
|
+
if (read.has(action.name))
|
|
17
|
+
return undefined;
|
|
18
|
+
read.add(action.name);
|
|
19
|
+
}
|
|
20
|
+
return action;
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Key the current history entry (merged into its state) and make it the shown one. */
|
|
2
|
+
export declare function enter(i: number): void;
|
|
3
|
+
/**
|
|
4
|
+
* Before a navigation leaves the shown entry: take scroll restoration over from the browser (once)
|
|
5
|
+
* and save the position. `here`: `history.state` still belongs to that entry (link clicks), so it
|
|
6
|
+
* can be keyed now if no navigation keyed it yet.
|
|
7
|
+
*/
|
|
8
|
+
export declare function leave(here: boolean, i: number): void;
|
|
9
|
+
/** Restore the current entry's saved position; false when there is none. */
|
|
10
|
+
export declare function restoreScroll(): boolean;
|
|
11
|
+
/** Make `element` the focus (programmatically focusable when it is not already). */
|
|
12
|
+
export declare function focusOn(element: HTMLElement): void;
|
|
13
|
+
/** The element a URL's fragment names, if any. */
|
|
14
|
+
export declare function fragmentTarget(url: URL): HTMLElement | null;
|
|
15
|
+
/** Hash-only navigation: scroll to and focus the fragment's target; no fragment scrolls to the top. */
|
|
16
|
+
export declare function reveal(url: URL): void;
|