@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/adapters/assemble.d.ts +32 -0
  4. package/adapters/assemble.js +32 -0
  5. package/adapters/build.d.ts +24 -0
  6. package/adapters/build.js +43 -0
  7. package/adapters/bun.d.ts +18 -0
  8. package/adapters/bun.js +44 -0
  9. package/adapters/cloudflare.d.ts +5 -0
  10. package/adapters/cloudflare.js +8 -0
  11. package/adapters/cluster.d.ts +7 -0
  12. package/adapters/cluster.js +130 -0
  13. package/adapters/csr.d.ts +12 -0
  14. package/adapters/csr.js +11 -0
  15. package/adapters/deno.d.ts +13 -0
  16. package/adapters/deno.js +35 -0
  17. package/adapters/fetch.d.ts +55 -0
  18. package/adapters/fetch.js +72 -0
  19. package/adapters/files.d.ts +20 -0
  20. package/adapters/files.js +155 -0
  21. package/adapters/idle-memory.d.ts +23 -0
  22. package/adapters/idle-memory.js +91 -0
  23. package/adapters/netlify.d.ts +13 -0
  24. package/adapters/netlify.js +15 -0
  25. package/adapters/node.d.ts +180 -0
  26. package/adapters/node.js +986 -0
  27. package/adapters/pack.d.ts +39 -0
  28. package/adapters/pack.js +360 -0
  29. package/adapters/server-target.d.ts +17 -0
  30. package/adapters/server-target.js +8 -0
  31. package/adapters/static.d.ts +84 -0
  32. package/adapters/static.js +231 -0
  33. package/adapters/target-build.d.ts +95 -0
  34. package/adapters/target-build.js +118 -0
  35. package/adapters/vercel-edge.d.ts +7 -0
  36. package/adapters/vercel-edge.js +9 -0
  37. package/adapters/vercel.d.ts +35 -0
  38. package/adapters/vercel.js +45 -0
  39. package/build/codegen.d.ts +97 -0
  40. package/build/codegen.js +1037 -0
  41. package/build/dev-errors.d.ts +23 -0
  42. package/build/dev-errors.js +247 -0
  43. package/build/lazy-rows.d.ts +19 -0
  44. package/build/lazy-rows.js +59 -0
  45. package/build/migrate.d.ts +31 -0
  46. package/build/migrate.js +385 -0
  47. package/build/reach.d.ts +60 -0
  48. package/build/reach.js +302 -0
  49. package/build/route-css.d.ts +54 -0
  50. package/build/route-css.js +216 -0
  51. package/build/routes.d.ts +55 -0
  52. package/build/routes.js +219 -0
  53. package/build/sync.d.ts +20 -0
  54. package/build/sync.js +16 -0
  55. package/build/vite.d.ts +208 -0
  56. package/build/vite.js +861 -0
  57. package/cli/rivet.d.ts +2 -0
  58. package/cli/rivet.js +101 -0
  59. package/client/active.d.ts +6 -0
  60. package/client/active.js +22 -0
  61. package/client/announce.d.ts +2 -0
  62. package/client/announce.js +21 -0
  63. package/client/dev.d.ts +41 -0
  64. package/client/dev.js +91 -0
  65. package/client/index.d.ts +156 -0
  66. package/client/index.js +714 -0
  67. package/client/layouts.d.ts +110 -0
  68. package/client/layouts.js +310 -0
  69. package/client/leave.d.ts +40 -0
  70. package/client/leave.js +75 -0
  71. package/client/link.d.ts +48 -0
  72. package/client/link.jsx +65 -0
  73. package/client/live.d.ts +57 -0
  74. package/client/live.js +170 -0
  75. package/client/navigate.d.ts +71 -0
  76. package/client/navigate.js +120 -0
  77. package/client/navigation.d.ts +28 -0
  78. package/client/navigation.js +14 -0
  79. package/client/params.d.ts +16 -0
  80. package/client/params.js +28 -0
  81. package/client/route-context.d.ts +32 -0
  82. package/client/route-context.js +21 -0
  83. package/client/scroll.d.ts +16 -0
  84. package/client/scroll.js +80 -0
  85. package/client/search.d.ts +8 -0
  86. package/client/search.js +17 -0
  87. package/client/tailwind.css +8 -0
  88. package/client/transition-helpers.d.ts +28 -0
  89. package/client/transition-helpers.js +63 -0
  90. package/client/transitions.css +90 -0
  91. package/client/transitions.d.ts +21 -0
  92. package/client/transitions.js +156 -0
  93. package/index.d.ts +135 -0
  94. package/index.js +8 -0
  95. package/package.json +156 -4
  96. package/server/app.d.ts +175 -0
  97. package/server/app.js +335 -0
  98. package/server/body.d.ts +20 -0
  99. package/server/body.js +8 -0
  100. package/server/deferred.d.ts +12 -0
  101. package/server/deferred.js +60 -0
  102. package/server/early-hints.d.ts +34 -0
  103. package/server/early-hints.js +63 -0
  104. package/server/handler.d.ts +240 -0
  105. package/server/handler.js +661 -0
  106. package/server/index.d.ts +9 -0
  107. package/server/index.js +6 -0
  108. package/server/inline-styles.d.ts +21 -0
  109. package/server/inline-styles.js +28 -0
  110. package/server/module.d.ts +14 -0
  111. package/server/module.js +96 -0
  112. package/server/render.d.ts +96 -0
  113. package/server/render.js +492 -0
  114. package/server/route-cache.d.ts +245 -0
  115. package/server/route-cache.js +813 -0
  116. package/server/security.d.ts +18 -0
  117. package/server/security.js +47 -0
  118. package/server/shell-build.d.ts +3 -0
  119. package/server/shell-build.js +2 -0
  120. package/server/shell-cache.d.ts +8 -0
  121. package/server/shell-cache.js +836 -0
  122. package/server/strict-csp.d.ts +7 -0
  123. package/server/strict-csp.js +18 -0
  124. package/server-only.d.ts +1 -0
  125. package/server-only.js +3 -0
  126. package/shared/actions.d.ts +10 -0
  127. package/shared/actions.js +18 -0
  128. package/shared/base.d.ts +8 -0
  129. package/shared/base.js +20 -0
  130. package/shared/cookie.d.ts +7 -0
  131. package/shared/cookie.js +32 -0
  132. package/shared/csp-directives.d.ts +9 -0
  133. package/shared/csp-directives.js +85 -0
  134. package/shared/csp-hash.d.ts +39 -0
  135. package/shared/csp-hash.js +121 -0
  136. package/shared/csp.d.ts +26 -0
  137. package/shared/csp.js +13 -0
  138. package/shared/data.d.ts +175 -0
  139. package/shared/data.js +22 -0
  140. package/shared/errors.d.ts +31 -0
  141. package/shared/errors.js +51 -0
  142. package/shared/head.d.ts +57 -0
  143. package/shared/head.js +242 -0
  144. package/shared/nonce.d.ts +2 -0
  145. package/shared/nonce.js +23 -0
  146. package/shared/prefetch-fallback.min.d.ts +1 -0
  147. package/shared/prefetch-fallback.min.js +2 -0
  148. package/shared/prefetch.d.ts +54 -0
  149. package/shared/prefetch.js +74 -0
  150. package/shared/redirect.d.ts +6 -0
  151. package/shared/redirect.js +25 -0
  152. package/shared/server-exports.d.ts +78 -0
  153. package/shared/server-exports.js +77 -0
  154. package/shared/slash.d.ts +8 -0
  155. package/shared/slash.js +24 -0
@@ -0,0 +1,110 @@
1
+ import { type Signal } from "@rivetjs/reactivity";
2
+ import { type RouteMatch } from "@rivetjs/runtime/router";
3
+ import type { LoaderDeps } from "../shared/data.js";
4
+ /** Generated layout table (a module constant of `.rivet/app.tsx`). */
5
+ export interface LayoutTable {
6
+ /** Layout ids: `/` + folder (`/`, `/shop`, `/u/[id]`). */
7
+ readonly ids: readonly string[];
8
+ /** Each layout's own params (from its folder), compared to decide whether its data is still valid. */
9
+ readonly params: readonly (readonly string[])[];
10
+ /** Bits per layout: 1 component (rendered, persisted), 2 server exports (a loader the client may keep). */
11
+ readonly kind: readonly number[];
12
+ /** The root layout rendered around the routes region (-1: none). */
13
+ readonly root: number;
14
+ /** Page routes: pattern and layout indices, outermost first (the route's `data` order). */
15
+ readonly routes: readonly (readonly [pattern: string, layouts: readonly number[]])[];
16
+ }
17
+ /** One layout's props as signals (resume-serialized with the App). */
18
+ export interface LayoutSignals {
19
+ readonly d: Signal<unknown>;
20
+ readonly p: Signal<Readonly<Record<string, string>>>;
21
+ readonly u: Signal<URL>;
22
+ }
23
+ /** Layout state of one App: level keys, per-layout signals, the URL (path + search) whose data the server sent (and its URL-bound layouts). */
24
+ export interface Layouts {
25
+ readonly k: readonly Signal<string>[];
26
+ /** Per layout (server-only ones hold 0, never read). */
27
+ readonly l: readonly LayoutSignals[];
28
+ readonly at: string;
29
+ readonly x: readonly number[];
30
+ /** The server render's `event.depends()` keys (route data order); absent when no loader called it. */
31
+ readonly dk?: LoaderDeps;
32
+ }
33
+ interface Loaded {
34
+ readonly data: readonly unknown[];
35
+ readonly kept?: readonly number[];
36
+ readonly byURL?: readonly number[];
37
+ readonly failure?: unknown;
38
+ }
39
+ interface Navigator {
40
+ location(): URL;
41
+ }
42
+ /**
43
+ * @internal A matcher that remembers its last answer by path (intent prefetch and each navigation
44
+ * re-match the same URL several times). Callers read `route` / `params` only: `url` is the first caller's.
45
+ */
46
+ export declare function _lastMatch(match: (url: URL) => RouteMatch | null): (url: URL) => RouteMatch | null;
47
+ /** @internal Generated `App`: layout signals (server data for the request's chain) and level keys. */
48
+ export declare function _layouts(server: {
49
+ data: readonly unknown[];
50
+ url: URL;
51
+ error?: unknown;
52
+ byURL?: readonly number[];
53
+ deps?: LoaderDeps;
54
+ } | null, router: Navigator, table: LayoutTable, levels: number): Layouts;
55
+ /** @internal Data may have changed (form submit, `invalidate()`): the next data request reloads every layout. */
56
+ export declare function _stale(): void;
57
+ /** @internal `invalidate()`: the page level reloads although its URL is unchanged. */
58
+ export declare function _bump(): void;
59
+ /** @internal Data request: ids of the target's layouts whose data the shown chain still has (same own params). */
60
+ export declare function _keepIds(state: Layouts | undefined, table: LayoutTable | undefined, url: URL): string | undefined;
61
+ /** @internal Route data for `url` arrived (`kept`: indices the server skipped; their keys stay as recorded). */
62
+ export declare function _depsLoaded(url: URL, length: number, fresh: LoaderDeps | undefined, kept?: readonly number[]): void;
63
+ /** Which loaders of the shown page `invalidate(key)` reruns. */
64
+ export interface InvalidationPlan {
65
+ /** Some loader matched (else nothing to do). */
66
+ readonly any: boolean;
67
+ /** The page's own loader matched. */
68
+ readonly page: boolean;
69
+ /** Ids of the layouts that matched. */
70
+ readonly drop: ReadonlySet<string>;
71
+ }
72
+ /**
73
+ * @internal `invalidate(key)`: the shown page's loaders whose keys pass `test`; undefined when the
74
+ * keys are unknown or the data may be stale anyway (form submit, `invalidate()`): rerun everything.
75
+ */
76
+ export declare function _plan(state: Layouts | undefined, table: LayoutTable | undefined, url: URL, test: (key: string) => boolean): InvalidationPlan | undefined;
77
+ /** @internal `invalidate(key)` reruns `drop` (layout ids) for `url`: the next keep list leaves them out. */
78
+ export declare function _invalidating(url: URL, drop: ReadonlySet<string>): void;
79
+ /**
80
+ * @internal `invalidate(key)` without the page loader, or a live page's in-place refresh: the layouts that
81
+ * reran (not in `kept`) update in place; the page stays. URL-bound layouts that reran are not kept across URL changes afterwards.
82
+ */
83
+ export declare function _refreshed(state: Layouts, table: LayoutTable, url: URL, result: {
84
+ readonly data: readonly unknown[];
85
+ readonly kept?: readonly number[];
86
+ readonly byURL?: readonly number[];
87
+ }): void;
88
+ /**
89
+ * @internal App `clientEffect`: derive the level keys from the location (a level reloads when its key changes).
90
+ * `inPlace(from, url)` (live apps): true when the change from the shown URL `from` is refreshed in place (a
91
+ * search-only navigation of the shown live page) — no level key moves.
92
+ */
93
+ export declare function _sync(router: Navigator, state: Layouts, table: LayoutTable, inPlace?: (from: string, url: URL) => boolean): void;
94
+ /** @internal Swap and hold of nested Routers: their first commit lands inside the outer level's render. */
95
+ export declare const _building: () => boolean;
96
+ /** @internal Level `k` renders a layout: `fn(token)` builds it with the Router of level `k + 1` inside. */
97
+ export declare function _enter<V>(k: number, result: unknown, url: URL, fn: (token: number) => V): V;
98
+ /** @internal The navigator of level `k`'s Router: tracks the level key only; orphans never load. */
99
+ export declare function _level(router: Navigator, state: Layouts, k: number, t: number): Navigator;
100
+ /** @internal Level route load: the outer level's handoff (synchronous), never for orphans, else undefined. */
101
+ export declare function _take<T>(k: number, url: URL): T | Promise<T> | undefined;
102
+ /**
103
+ * @internal Level route load settled: update the chain's layout signals (kept layouts keep theirs).
104
+ * A failure below a layout whose data was reloaded re-renders from the outermost level instead.
105
+ */
106
+ export declare function _landed<T extends Loaded>(state: Layouts, table: LayoutTable, k: number, r: number, result: T, match: {
107
+ params: Readonly<Record<string, string>>;
108
+ url: URL;
109
+ }): T | Promise<T>;
110
+ export {};
@@ -0,0 +1,310 @@
1
+ // Persistent layouts of generated apps. Every layout with a component stays mounted while client
2
+ // navigation stays below it: its props are App-level signals (`data`, `params`, `url`) that loads
3
+ // update in place, and each nesting level renders its own Router region that tracks one level key
4
+ // signal (the layout at that level, or the page at the deepest one). A level reloads only when its
5
+ // key changes; it fetches data for the whole chain, skipping layouts whose data the client keeps.
6
+ import { batch, signal, untrack } from "@rivetjs/reactivity";
7
+ import { compileRoutes } from "@rivetjs/runtime/router";
8
+ const browser = typeof document !== "undefined";
9
+ const never = () => new Promise(() => { });
10
+ /**
11
+ * @internal A matcher that remembers its last answer by path (intent prefetch and each navigation
12
+ * re-match the same URL several times). Callers read `route` / `params` only: `url` is the first caller's.
13
+ */
14
+ export function _lastMatch(match) {
15
+ let path, hit = null;
16
+ return (url) => (url.pathname === path ? hit : ((path = url.pathname), (hit = match(url))));
17
+ }
18
+ const matchers = new WeakMap();
19
+ function find(table, url) {
20
+ let match = matchers.get(table);
21
+ if (!match)
22
+ matchers.set(table, (match = _lastMatch(compileRoutes(table.routes.map(([path, layouts]) => ({ path, render: () => layouts }))))));
23
+ const hit = match(url);
24
+ return hit ? { layouts: hit.route.render(undefined, hit), params: hit.params } : null;
25
+ }
26
+ const own = (table, i, params) => i + "\0" + table.params[i].map((name) => params[name] ?? "").join("\0");
27
+ const same = (a, b) => {
28
+ const keys = Object.keys(a);
29
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
30
+ };
31
+ /** Level keys of a URL: `L<index>` per persisted layout, then `P<path?query>` for the page level. */
32
+ function keysOf(table, url, hit = find(table, url)) {
33
+ const keys = [];
34
+ if (hit)
35
+ for (const i of hit.layouts)
36
+ if (table.kind[i] & 1 && i !== table.root)
37
+ keys.push("L" + i);
38
+ keys.push("P" + url.pathname + url.search);
39
+ return keys;
40
+ }
41
+ /** @internal Generated `App`: layout signals (server data for the request's chain) and level keys. */
42
+ export function _layouts(server, router, table, levels) {
43
+ const url = server?.url ?? untrack(router.location);
44
+ const hit = find(table, url);
45
+ // Error renders: data of the outermost layouts that loaded (the unmatched URL's: the root layout's).
46
+ const data = server ? server.data : null;
47
+ // Server error renders show the failure at level 0 (no nested levels): any navigation reloads it.
48
+ const keys = server?.error ? ["E"] : keysOf(table, url, hit);
49
+ return {
50
+ k: Array.from({ length: levels }, (_, k) => signal(keys[k] ?? "")),
51
+ l: table.kind.map((kind, i) => {
52
+ if (!(kind & 1))
53
+ return 0;
54
+ const at = hit ? hit.layouts.indexOf(i) : i === table.root ? 0 : -1;
55
+ return {
56
+ d: signal(at >= 0 ? data?.[at] : undefined),
57
+ // The root layout stays outside the routes: no params, like before.
58
+ p: signal(at >= 0 && i !== table.root ? hit.params : {}),
59
+ u: signal(url),
60
+ };
61
+ }),
62
+ // Only a server render says which layouts read the URL; without it (dev mounts) nothing is kept at first.
63
+ // Path + search (no origin: prerendered state carries no build-time origin).
64
+ at: data && !server?.error && server?.byURL ? url.pathname + url.search : "",
65
+ x: server?.byURL ?? [],
66
+ // Only when a loader called `depends()`: other apps' resume state stays as it was.
67
+ ...(server?.deps ? { dk: server.deps } : {}),
68
+ };
69
+ }
70
+ // Browser state: layouts the shown chain has data for (`own` keys), staleness, level handoffs.
71
+ const committed = new WeakMap();
72
+ let stale = false;
73
+ let bump = false;
74
+ let revision = 0;
75
+ function shown(state, table) {
76
+ const known = committed.get(state);
77
+ if (known)
78
+ return known;
79
+ // Resumed pages: the server's chain is fresh (its URL-bound layouts excepted).
80
+ const set = new Set();
81
+ const hit = state.at ? find(table, new URL(state.at, location.href)) : null;
82
+ hit?.layouts.forEach((i, at) => state.x.includes(at) || set.add(own(table, i, hit.params)));
83
+ committed.set(state, set);
84
+ return set;
85
+ }
86
+ /** @internal Data may have changed (form submit, `invalidate()`): the next data request reloads every layout. */
87
+ export function _stale() {
88
+ stale = true;
89
+ }
90
+ /** @internal `invalidate()`: the page level reloads although its URL is unchanged. */
91
+ export function _bump() {
92
+ bump = true;
93
+ }
94
+ /** @internal Data request: ids of the target's layouts whose data the shown chain still has (same own params). */
95
+ export function _keepIds(state, table, url) {
96
+ if (!browser || !state || !table || stale)
97
+ return undefined;
98
+ const hit = find(table, url);
99
+ if (!hit)
100
+ return undefined;
101
+ seed(state);
102
+ const set = shown(state, table);
103
+ // `invalidate(key)` of this URL: the layouts whose keys match rerun although the client has their data.
104
+ const drop = forced && forced.at === pathOf(url) ? forced.drop : undefined;
105
+ const ids = hit.layouts.filter((i) => table.kind[i] & 2 && set.has(own(table, i, hit.params)) && !drop?.has(table.ids[i])).map((i) => table.ids[i]);
106
+ return ids.length ? ids.join(",") : undefined;
107
+ }
108
+ // `event.depends()` keys of the shown route data (route data order) for `invalidate(key)`; null: unknown.
109
+ let deps = null;
110
+ // `invalidate(key)` in flight: ids of the layouts it reruns (left out of the keep list for its URL).
111
+ let forced = null;
112
+ const pathOf = (url) => url.pathname + url.search;
113
+ // Resumed pages: the server render's keys (none recorded when no loader called `depends()`).
114
+ function seed(state) {
115
+ if (!deps && state.at)
116
+ deps = { at: state.at, list: state.dk ?? [] };
117
+ }
118
+ /** @internal Route data for `url` arrived (`kept`: indices the server skipped; their keys stay as recorded). */
119
+ export function _depsLoaded(url, length, fresh, kept) {
120
+ if (!browser)
121
+ return;
122
+ const previous = deps?.list;
123
+ // Kept loaders whose keys were never seen (no resume state): unknown, so `invalidate(key)` reruns everything.
124
+ if (kept?.length && !previous)
125
+ return void (deps = null);
126
+ const skipped = new Set(kept);
127
+ deps = { at: pathOf(url), list: Array.from({ length }, (_, i) => (skipped.has(i) ? previous[i] : fresh?.[i])) };
128
+ }
129
+ /**
130
+ * @internal `invalidate(key)`: the shown page's loaders whose keys pass `test`; undefined when the
131
+ * keys are unknown or the data may be stale anyway (form submit, `invalidate()`): rerun everything.
132
+ */
133
+ export function _plan(state, table, url, test) {
134
+ if (!browser || stale)
135
+ return undefined;
136
+ if (state)
137
+ seed(state);
138
+ if (!deps || deps.at !== pathOf(url))
139
+ return undefined;
140
+ const list = deps.list;
141
+ const matches = (at) => !!list[at]?.some(test);
142
+ const hit = table ? find(table, url) : null;
143
+ // No layout table (apps without layouts, static and CSR apps): nothing can be kept, any match reruns all.
144
+ if (!hit) {
145
+ const any = list.some((_, at) => matches(at));
146
+ return { any, page: any, drop: new Set() };
147
+ }
148
+ const drop = new Set(hit.layouts.flatMap((i, at) => (matches(at) ? [table.ids[i]] : [])));
149
+ const mine = matches(hit.layouts.length);
150
+ return { any: mine || drop.size > 0, page: mine, drop };
151
+ }
152
+ /** @internal `invalidate(key)` reruns `drop` (layout ids) for `url`: the next keep list leaves them out. */
153
+ export function _invalidating(url, drop) {
154
+ forced = { at: pathOf(url), drop };
155
+ }
156
+ /**
157
+ * @internal `invalidate(key)` without the page loader, or a live page's in-place refresh: the layouts that
158
+ * reran (not in `kept`) update in place; the page stays. URL-bound layouts that reran are not kept across URL changes afterwards.
159
+ */
160
+ export function _refreshed(state, table, url, result) {
161
+ forced = null;
162
+ const hit = find(table, url);
163
+ if (!hit)
164
+ return;
165
+ // The request named only data the client still had (`_keepIds` keeps nothing while stale): all fresh now.
166
+ stale = false;
167
+ const kept = new Set(result.kept), bound = new Set(result.byURL);
168
+ const before = shown(state, table);
169
+ batch(() => hit.layouts.forEach((i, at) => {
170
+ const layout = table.kind[i] & 1 ? state.l[i] : undefined;
171
+ if (!layout)
172
+ return;
173
+ if (!kept.has(at))
174
+ layout.d.set(result.data[at]);
175
+ // A search-only navigation refreshed in place: the layouts' `url` follows it.
176
+ if (untrack(layout.u).href !== url.href)
177
+ layout.u.set(url);
178
+ }));
179
+ committed.set(state, new Set(hit.layouts.flatMap((i, at) => {
180
+ const id = own(table, i, hit.params);
181
+ return (kept.has(at) ? before.has(id) : !bound.has(at)) ? [id] : [];
182
+ })));
183
+ }
184
+ // The document's URL when this module loaded (before any client navigation could run).
185
+ const loadedAt = browser ? location.href.split("#")[0] : "";
186
+ const synced = new WeakMap();
187
+ /**
188
+ * @internal App `clientEffect`: derive the level keys from the location (a level reloads when its key changes).
189
+ * `inPlace(from, url)` (live apps): true when the change from the shown URL `from` is refreshed in place (a
190
+ * search-only navigation of the shown live page) — no level key moves.
191
+ */
192
+ export function _sync(router, state, table, inPlace) {
193
+ const url = router.location();
194
+ // First run without a navigation yet: the page shows what the server rendered (even when it was
195
+ // rendered for another URL, e.g. a prerendered page served under a path prefix).
196
+ const from = synced.get(state);
197
+ synced.set(state, url.href.split("#")[0]);
198
+ if (from === undefined && !bump && url.href.split("#")[0] === loadedAt)
199
+ return;
200
+ if (!bump && inPlace?.(from ?? loadedAt, url))
201
+ return;
202
+ const keys = keysOf(table, url);
203
+ if (bump) {
204
+ bump = false;
205
+ keys[keys.length - 1] += "~" + ++revision;
206
+ }
207
+ // Outer levels first: an outer level that re-renders disposes the inner ones before they run.
208
+ batch(() => keys.forEach((key, k) => state.k[k]?.set(key)));
209
+ }
210
+ // A level that renders a layout hands its loaded result to the Router inside it, which takes it
211
+ // synchronously (one commit per navigation). Tokens tell the Router of the shown layout instance from
212
+ // orphans: a server-rendered inner region whose outer level the browser has re-rendered since.
213
+ let handoff = null;
214
+ let building = 0;
215
+ let token = 0;
216
+ let outermost = Infinity;
217
+ const current = [];
218
+ const ORPHAN = "#rivet:orphan";
219
+ /** @internal Swap and hold of nested Routers: their first commit lands inside the outer level's render. */
220
+ export const _building = () => building > 0;
221
+ /** @internal Level `k` renders a layout: `fn(token)` builds it with the Router of level `k + 1` inside. */
222
+ export function _enter(k, result, url, fn) {
223
+ if (!browser)
224
+ return fn(0);
225
+ const mine = (current[k + 1] = ++token), mark = { k: k + 1, href: url.href, result };
226
+ const run = (build) => {
227
+ const previous = handoff;
228
+ handoff = mark;
229
+ building++;
230
+ try {
231
+ return build();
232
+ }
233
+ finally {
234
+ building--;
235
+ handoff = previous;
236
+ }
237
+ };
238
+ const value = run(() => fn(mine));
239
+ // A layout that returns an accessor (the dev HMR wrapper, e.g.) is built when its insertion reads it: that
240
+ // read is part of this render too, so the inner Router still takes the handoff and holds no server outlet.
241
+ return typeof value === "function" ? (() => run(value)) : value;
242
+ }
243
+ /** @internal The navigator of level `k`'s Router: tracks the level key only; orphans never load. */
244
+ export function _level(router, state, k, t) {
245
+ // Token 0: created by the App or re-created from server HTML (resume): inner server regions are orphans now.
246
+ if (browser && t === 0)
247
+ outermost = Math.min(outermost, k);
248
+ return {
249
+ location() {
250
+ state.k[k]();
251
+ const url = untrack(router.location);
252
+ if (browser && !(t === 0 ? k <= outermost : current[k] === t))
253
+ url.hash = ORPHAN;
254
+ return url;
255
+ },
256
+ };
257
+ }
258
+ /** @internal Level route load: the outer level's handoff (synchronous), never for orphans, else undefined. */
259
+ export function _take(k, url) {
260
+ if (!browser)
261
+ return undefined;
262
+ if (url.hash === ORPHAN)
263
+ return never();
264
+ if (handoff && handoff.k === k && handoff.href === url.href) {
265
+ const { result } = handoff;
266
+ handoff = null;
267
+ return result;
268
+ }
269
+ return undefined;
270
+ }
271
+ /**
272
+ * @internal Level route load settled: update the chain's layout signals (kept layouts keep theirs).
273
+ * A failure below a layout whose data was reloaded re-renders from the outermost level instead.
274
+ */
275
+ export function _landed(state, table, k, r, result, match) {
276
+ const layouts = table.routes[r][1];
277
+ forced = null;
278
+ if (result.failure !== undefined) {
279
+ if (!browser || k === 0)
280
+ return result;
281
+ // Shown in place when every layout around this level kept its data, else from level 0.
282
+ const set = stale ? new Set() : shown(state, table);
283
+ const outer = layouts.filter((i) => table.kind[i] & 1 && i !== table.root).slice(0, k);
284
+ if (outer.every((i) => !(table.kind[i] & 2) || set.has(own(table, i, match.params))))
285
+ return result;
286
+ handoff = { k: 0, href: match.url.href, result };
287
+ state.k[0].set("E" + ++revision);
288
+ return never();
289
+ }
290
+ const kept = new Set(result.kept);
291
+ batch(() => layouts.forEach((i, at) => {
292
+ const layout = state.l[i];
293
+ if (!(table.kind[i] & 1) || !layout)
294
+ return;
295
+ if (!kept.has(at))
296
+ layout.d.set(result.data[at]);
297
+ if (i !== table.root && !same(untrack(layout.p), match.params))
298
+ layout.p.set(match.params);
299
+ // Same URL (a dev mount's first load): keep the value, so URL effects do not rerun.
300
+ if (untrack(layout.u).href !== match.url.href)
301
+ layout.u.set(match.url);
302
+ }));
303
+ if (browser) {
304
+ stale = false;
305
+ // URL-bound loaders (they read `url` / `request`) reload on the next URL change.
306
+ const bound = new Set(result.byURL);
307
+ committed.set(state, new Set(layouts.flatMap((i, at) => (bound.has(at) ? [] : [own(table, i, match.params)]))));
308
+ }
309
+ return result;
310
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Leaving the page (`beforeLeave`): a client navigation (`link`: a link, `navigate()` or
3
+ * `setSearchParams()`), back/forward between client-routed entries (`history`), or `unload`
4
+ * (reload, close, a full page load).
5
+ */
6
+ export interface LeaveEvent {
7
+ readonly from: URL;
8
+ /** Destination; null on unload (the browser does not tell). */
9
+ readonly to: URL | null;
10
+ /** `history`: back/forward; the address bar already shows `to` (a cancel walks back to `from`). */
11
+ readonly type: "link" | "history" | "unload";
12
+ /** Stay on the page. On unload the browser asks the user with its own dialog (custom text is not shown). */
13
+ cancel(): void;
14
+ }
15
+ /**
16
+ * @internal The router client's guard checks, installed by the first `beforeLeave()`. Until then no
17
+ * guard can exist and the router skips them, so apps that never call `beforeLeave` ship no guard code.
18
+ */
19
+ export declare const _guard: {
20
+ cancelled?: typeof _leaveCancelled;
21
+ blocked?: typeof _popBlocked;
22
+ };
23
+ /** @internal Run the guards for leaving to `to` (null: unload); true when one cancelled. */
24
+ export declare function _leaveCancelled(to: URL | null, from?: URL, type?: LeaveEvent["type"]): boolean;
25
+ /**
26
+ * @internal Back/forward guard. The router's popstate listeners (runtime router, router client) ask
27
+ * first, so the answer is kept per event: true when the router must ignore this popstate (a guard
28
+ * cancelled it, or it is the walk back). `committed`: the URL the routes region shows, `index` its
29
+ * history entry index (`history.state.i`).
30
+ */
31
+ export declare function _popBlocked(event: Event, committed: string, index: number): boolean;
32
+ /**
33
+ * Guard leaving the page: `fn` runs before a client navigation (link, `navigate()`), on back/forward
34
+ * between client-routed entries and on `beforeunload`; `leave.cancel()` keeps the page (ask first with
35
+ * `confirm()`: `if (!confirm(msg)) leave.cancel()`; `leave.type` tells which). A cancelled back/forward
36
+ * walks history back to the guarded entry (the router never sees either popstate). The `beforeunload`
37
+ * listener exists only while a guard is registered (it keeps pages out of the back/forward cache).
38
+ * Returns the unregister function.
39
+ */
40
+ export declare function beforeLeave(fn: (leave: LeaveEvent) => void): () => void;
@@ -0,0 +1,75 @@
1
+ const leaveGuards = new Set();
2
+ /**
3
+ * @internal The router client's guard checks, installed by the first `beforeLeave()`. Until then no
4
+ * guard can exist and the router skips them, so apps that never call `beforeLeave` ship no guard code.
5
+ */
6
+ export const _guard = {};
7
+ /** @internal Run the guards for leaving to `to` (null: unload); true when one cancelled. */
8
+ export function _leaveCancelled(to, from = new URL(location.href), type = to ? "link" : "unload") {
9
+ if (!leaveGuards.size)
10
+ return false;
11
+ let cancelled = false;
12
+ const leave = { from, to, type, cancel: () => void (cancelled = true) };
13
+ for (const guard of leaveGuards)
14
+ guard(leave);
15
+ return cancelled;
16
+ }
17
+ function onBeforeUnload(event) {
18
+ if (!_leaveCancelled(null))
19
+ return;
20
+ event.preventDefault();
21
+ // Older browsers prompt only with a returnValue.
22
+ event.returnValue = "";
23
+ }
24
+ // Set while `history.go` walks back to the guarded entry: that popstate is ours, not a navigation.
25
+ let restoring = false;
26
+ let last = null, lastBlocked = false;
27
+ /**
28
+ * @internal Back/forward guard. The router's popstate listeners (runtime router, router client) ask
29
+ * first, so the answer is kept per event: true when the router must ignore this popstate (a guard
30
+ * cancelled it, or it is the walk back). `committed`: the URL the routes region shows, `index` its
31
+ * history entry index (`history.state.i`).
32
+ */
33
+ export function _popBlocked(event, committed, index) {
34
+ if (event !== last)
35
+ (last = event), (lastBlocked = blocked(committed, index));
36
+ return lastBlocked;
37
+ }
38
+ function blocked(committed, index) {
39
+ if (restoring)
40
+ return !(restoring = false);
41
+ if (!leaveGuards.size)
42
+ return false;
43
+ const from = new URL(committed), to = new URL(location.href);
44
+ // Same page, another fragment: not leaving (fragment links are not guarded either).
45
+ if (from.pathname === to.pathname && from.search === to.search)
46
+ return false;
47
+ const delta = index - (history.state?.i ?? 0);
48
+ // Entries the router did not number cannot be walked back to: let them through.
49
+ if (!delta || !_leaveCancelled(to, from, "history"))
50
+ return false;
51
+ restoring = true;
52
+ history.go(delta);
53
+ return true;
54
+ }
55
+ /**
56
+ * Guard leaving the page: `fn` runs before a client navigation (link, `navigate()`), on back/forward
57
+ * between client-routed entries and on `beforeunload`; `leave.cancel()` keeps the page (ask first with
58
+ * `confirm()`: `if (!confirm(msg)) leave.cancel()`; `leave.type` tells which). A cancelled back/forward
59
+ * walks history back to the guarded entry (the router never sees either popstate). The `beforeunload`
60
+ * listener exists only while a guard is registered (it keeps pages out of the back/forward cache).
61
+ * Returns the unregister function.
62
+ */
63
+ export function beforeLeave(fn) {
64
+ if (typeof window === "undefined")
65
+ return () => { };
66
+ _guard.cancelled = _leaveCancelled;
67
+ _guard.blocked = _popBlocked;
68
+ if (!leaveGuards.size)
69
+ addEventListener("beforeunload", onBeforeUnload);
70
+ leaveGuards.add(fn);
71
+ return () => {
72
+ if (leaveGuards.delete(fn) && !leaveGuards.size)
73
+ removeEventListener("beforeunload", onBeforeUnload);
74
+ };
75
+ }
@@ -0,0 +1,48 @@
1
+ import type { JSX } from "@rivetjs/runtime/jsx-runtime";
2
+ import type { LinkHref } from "../index.js";
3
+ type Anchor = JSX.IntrinsicElements["a"];
4
+ /** Props of `<Link>`. */
5
+ export interface LinkProps extends Omit<Anchor, "href" | "children" | "aria-current"> {
6
+ /** A page of this app: `RoutePath` (from `.rivet/routes.d.ts`) with optional `?query` / `#hash`; the URL base path is added. */
7
+ to: LinkHref;
8
+ /**
9
+ * View transition types added for this navigation (`data-transition`; overrides the route's own);
10
+ * `false`: no animation. Needs `rivetRouter({ transitions })`.
11
+ */
12
+ transition?: string | readonly string[] | false;
13
+ /**
14
+ * `"intent"` (default): the client router loads data and page chunk on hover/focus. `"none"`: nothing
15
+ * before the click. `"document"` / `"prerender"`: when the click is a document load (`reload`, or an
16
+ * app without client routing), the browser fetches (or prerenders) the page ahead — Speculation Rules,
17
+ * else `<link rel=prefetch>` / `fetch` on intent; links the client router handles keep `"intent"`
18
+ * behaviour. Unset: `rivetRouter({ prefetch })`'s default, else `"intent"`; an explicit `"intent"` keeps the
19
+ * router's prefetch only (no document prefetch from the app default).
20
+ */
21
+ prefetch?: "intent" | "none" | "document" | "prerender";
22
+ /** Classes set on the link while its navigation is pending (with `data-rivet-pending`). Needs `transitions`. */
23
+ pending?: string;
24
+ /** Replace the current history entry instead of pushing one. */
25
+ replace?: boolean;
26
+ /** Full document navigation instead of client routing (`data-rivet-reload`). */
27
+ reload?: boolean;
28
+ /**
29
+ * Mark the link while it points at the current page: `aria-current="page"` + `data-current`.
30
+ * `true` / `"exact"`: same path only; `"prefix"`: also below it (`aria-current="true"`), e.g. a section link.
31
+ * Kept current across client navigation. An explicit `aria-current` wins.
32
+ */
33
+ active?: boolean | "exact" | "prefix";
34
+ "aria-current"?: "page" | "step" | "location" | "date" | "time" | "true" | "false";
35
+ children?: Anchor["children"];
36
+ }
37
+ /**
38
+ * Compiler ABI (server target): the `href` of a bare `<Link to>` — the app path under the URL base, in the
39
+ * `trailingSlash` form. The compiler writes a bare server `<Link>` (only `to`, a literal `class`, static text) as
40
+ * `<a href={_linkHref(to)}>` markup in the enclosing template (same bytes as the component's bare path).
41
+ */
42
+ export declare function _linkHref(to: LinkHref): string;
43
+ /**
44
+ * Typed navigation link: `<Link to="/items/42" transition="zoom" pending="opacity-60">Item</Link>`.
45
+ * Other `<a>` attributes and event handlers (`style`, `data-*`, `download`, `onClick`…) pass through.
46
+ */
47
+ export declare function Link(props: LinkProps): import("@rivetjs/runtime/jsx-runtime").CompiledElement;
48
+ export {};
@@ -0,0 +1,65 @@
1
+ // `<Link>`: the router's navigation component. Renders a plain `<a href>` (works without JS, resumes
2
+ // without any component code); the router's delegated handlers read its data attributes.
3
+ import { useContext } from "@rivetjs/reactivity";
4
+ import { withBase } from "../shared/base.js";
5
+ import { slashHref, TRAILING_SLASH } from "../shared/slash.js";
6
+ import { currentOf } from "./active.js";
7
+ import { ServerData } from "./route-context.js";
8
+ const attribute = (on) => (on ? "" : undefined);
9
+ /**
10
+ * Compiler ABI (server target): the `href` of a bare `<Link to>` — the app path under the URL base, in the
11
+ * `trailingSlash` form. The compiler writes a bare server `<Link>` (only `to`, a literal `class`, static text) as
12
+ * `<a href={_linkHref(to)}>` markup in the enclosing template (same bytes as the component's bare path).
13
+ */
14
+ export function _linkHref(to) {
15
+ return TRAILING_SLASH === "ignore" ? withBase(to) : slashHref(withBase(to), TRAILING_SLASH);
16
+ }
17
+ /**
18
+ * Typed navigation link: `<Link to="/items/42" transition="zoom" pending="opacity-60">Item</Link>`.
19
+ * Other `<a>` attributes and event handlers (`style`, `data-*`, `download`, `onClick`…) pass through.
20
+ */
21
+ export function Link(props) {
22
+ // Bare link (only `to`, children and a string `class`; every other prop absent or undefined — the
23
+ // common case): `<a class href>` without the option attributes, the rest object or the key scan below.
24
+ let bare = true;
25
+ for (const key in props)
26
+ if (key !== "to" &&
27
+ key !== "children" &&
28
+ (key === "class" ? props.class !== undefined && typeof props.class !== "string" : props[key] !== undefined)) {
29
+ bare = false;
30
+ break;
31
+ }
32
+ if (bare) {
33
+ return (<a class={props.class} href={_linkHref(props.to)}>
34
+ {props.children}
35
+ </a>);
36
+ }
37
+ const { to, transition: types, prefetch, pending, replace, reload, active, children, ...rest } = props;
38
+ // `rivetRouter({ trailingSlash })`: the path in its canonical form (a build-time constant; folded away by default).
39
+ const href = TRAILING_SLASH === "ignore" ? withBase(to) : slashHref(withBase(to), TRAILING_SLASH);
40
+ const transition = types === false ? "none" : typeof types === "string" ? types : types?.join(" ");
41
+ const mode = active ? (active === "prefix" ? "prefix" : "exact") : undefined;
42
+ let current = rest["aria-current"];
43
+ if (mode && current === undefined) {
44
+ // The request's URL on the server; the browser's while rendering on the client.
45
+ const here = useContext(ServerData)?.url ?? (typeof location === "undefined" ? undefined : location);
46
+ if (here)
47
+ current = currentOf(new URL(href, here.href).pathname, here.pathname, mode);
48
+ }
49
+ const currentAttr = mode ? attribute(!!current) : undefined, activeAttr = rest["aria-current"] === undefined ? mode : undefined;
50
+ // Fast path (no attributes beyond a string `class`, the common case): explicit attributes, no generic
51
+ // spread (same markup: the spread put `class` first too; class arrays / objects keep the spread).
52
+ let plain = rest.class === undefined || typeof rest.class === "string";
53
+ for (const key in rest)
54
+ if (key !== "class") {
55
+ plain = false;
56
+ break;
57
+ }
58
+ if (plain)
59
+ return (<a class={rest.class} href={href} aria-current={current} data-current={currentAttr} data-rivet-active={activeAttr} data-transition={transition} data-rivet-prefetch={prefetch} data-rivet-pending-class={pending} data-rivet-replace={attribute(replace)} data-rivet-reload={attribute(reload)}>
60
+ {children}
61
+ </a>);
62
+ return (<a {...rest} href={href} aria-current={current} data-current={currentAttr} data-rivet-active={activeAttr} data-transition={transition} data-rivet-prefetch={prefetch} data-rivet-pending-class={pending} data-rivet-replace={attribute(replace)} data-rivet-reload={attribute(reload)}>
63
+ {children}
64
+ </a>);
65
+ }