@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,80 @@
1
+ const SCROLL = "rivet:scroll:";
2
+ let seq = 0;
3
+ // Scroll key of the entry the page shows; unknown until the first client navigation.
4
+ let shown;
5
+ let manual = false;
6
+ /** Key the current history entry (merged into its state) and make it the shown one. */
7
+ export function enter(i) {
8
+ const state = history.state;
9
+ const key = typeof state?.rivet === "number" ? state.rivet : Date.now() * 1000 + (seq++ % 1000);
10
+ if (state?.i !== i || state?.rivet !== key)
11
+ history.replaceState({ ...state, i, rivet: key }, "");
12
+ shown = key;
13
+ }
14
+ /**
15
+ * Before a navigation leaves the shown entry: take scroll restoration over from the browser (once)
16
+ * and save the position. `here`: `history.state` still belongs to that entry (link clicks), so it
17
+ * can be keyed now if no navigation keyed it yet.
18
+ */
19
+ export function leave(here, i) {
20
+ if (!manual) {
21
+ manual = true;
22
+ history.scrollRestoration = "manual";
23
+ // Reloads and cross-document traversals restore natively: hand control back while unloading.
24
+ addEventListener("pagehide", () => (history.scrollRestoration = "auto"));
25
+ addEventListener("pageshow", (event) => event.persisted && (history.scrollRestoration = "manual"));
26
+ }
27
+ if (here && shown === undefined)
28
+ enter(i);
29
+ if (shown === undefined)
30
+ return;
31
+ try {
32
+ sessionStorage.setItem(SCROLL + shown, JSON.stringify([scrollX, scrollY]));
33
+ }
34
+ catch {
35
+ /* Storage may be unavailable (private mode); scroll restoration degrades to top. */
36
+ }
37
+ }
38
+ /** Restore the current entry's saved position; false when there is none. */
39
+ export function restoreScroll() {
40
+ const state = history.state;
41
+ let position = null;
42
+ if (typeof state?.rivet === "number") {
43
+ try {
44
+ position = JSON.parse(sessionStorage.getItem(SCROLL + state.rivet) ?? "null");
45
+ }
46
+ catch {
47
+ position = null;
48
+ }
49
+ }
50
+ if (position)
51
+ scrollTo(position[0], position[1]);
52
+ return !!position;
53
+ }
54
+ /** Make `element` the focus (programmatically focusable when it is not already). */
55
+ export function focusOn(element) {
56
+ if (!element.hasAttribute("tabindex"))
57
+ element.setAttribute("tabindex", "-1");
58
+ element.focus({ preventScroll: true });
59
+ }
60
+ /** The element a URL's fragment names, if any. */
61
+ export function fragmentTarget(url) {
62
+ if (!url.hash)
63
+ return null;
64
+ try {
65
+ return document.getElementById(decodeURIComponent(url.hash.slice(1)));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /** Hash-only navigation: scroll to and focus the fragment's target; no fragment scrolls to the top. */
72
+ export function reveal(url) {
73
+ const target = fragmentTarget(url);
74
+ if (target) {
75
+ target.scrollIntoView();
76
+ focusOn(target);
77
+ }
78
+ else if (!url.hash)
79
+ scrollTo(0, 0);
80
+ }
@@ -0,0 +1,8 @@
1
+ import { type Context, type Signal } from "@rivetjs/reactivity";
2
+ /** Browser search params of the shown page (route commits update it; a fresh object per change). */
3
+ export declare const searchParams: Signal<URLSearchParams>;
4
+ /**
5
+ * Resumable search params context (`useSearchParams()`): the server entry provides the request's,
6
+ * resumed regions revive to `searchParams`.
7
+ */
8
+ export declare const SearchParams: Context<() => URLSearchParams>;
@@ -0,0 +1,17 @@
1
+ // Search params state (`useSearchParams()`), in its own module so apps that never read search params
2
+ // ship none of it: the router client syncs the signal only through the `_searchSync` hook set below.
3
+ import { createContext, signal } from "@rivetjs/reactivity";
4
+ import { _searchSync } from "./navigation.js";
5
+ // The shown page's search (`_searchSync.shown`, set per route commit): this module may load mid-navigation.
6
+ let shown = _searchSync.shown ?? (typeof location === "undefined" ? "" : location.search);
7
+ /** Browser search params of the shown page (route commits update it; a fresh object per change). */
8
+ export const searchParams = signal(new URLSearchParams(shown));
9
+ /**
10
+ * Resumable search params context (`useSearchParams()`): the server entry provides the request's,
11
+ * resumed regions revive to `searchParams`.
12
+ */
13
+ export const SearchParams = createContext(searchParams, { id: "rivet:search" });
14
+ _searchSync.search = () => {
15
+ if (_searchSync.shown !== shown)
16
+ searchParams.set(new URLSearchParams((shown = _searchSync.shown)));
17
+ };
@@ -0,0 +1,8 @@
1
+ /* @rivetjs/router Tailwind v4 variants: `@import "@rivetjs/router/tailwind.css";` after `@import "tailwindcss";`.
2
+ nav-pending: while the navigation indicator shows (html[data-rivet-pending], after the delay)
3
+ link-pending: on the clicked link until the new page commits (a[data-rivet-pending])
4
+ nav-back / nav-forward: during a back / forward transition (html[data-rivet-transition]) */
5
+ @custom-variant nav-pending (html[data-rivet-pending] &);
6
+ @custom-variant link-pending (&[data-rivet-pending]);
7
+ @custom-variant nav-back (html[data-rivet-transition~="back"] &);
8
+ @custom-variant nav-forward (html[data-rivet-transition~="forward"] &);
@@ -0,0 +1,28 @@
1
+ import type { NavigationInfo } from "./index.js";
2
+ /** A page module's `export const transition`: extra types, `false` (never animate into it), or per navigation. */
3
+ export type RouteTransition = false | {
4
+ types?: readonly string[];
5
+ } | ((nav: NavigationInfo) => false | {
6
+ types?: readonly string[];
7
+ });
8
+ /**
9
+ * View transition types of a navigation, or null for an instant swap. Defaults (`push`, `replace`,
10
+ * `back`, `forward`) are always first; link `data-transition` wins over the route's `transition`.
11
+ */
12
+ export declare function transitionTypes(nav: NavigationInfo): string[] | null;
13
+ /** `data-vt-name` elements by name, one per name (duplicates drop the name). */
14
+ export declare function namedElements(root: ParentNode | null): Map<string, HTMLElement | null>;
15
+ /** Names present exactly once on both sides: the shared elements of a transition. */
16
+ export declare function sharedNames(before: Map<string, HTMLElement | null>, after: Map<string, HTMLElement | null>): string[];
17
+ /**
18
+ * Indicator timer: `show` after `delay` unless finished first; once shown, `hide` no earlier than
19
+ * `minDuration` after showing. `start` while running keeps the timers (redirects, quick re-clicks).
20
+ */
21
+ export declare function indicator(delay: number, minDuration: number, show: () => void, hide: () => void, clock?: {
22
+ now(): number;
23
+ set(fn: () => void, ms: number): unknown;
24
+ clear(handle: unknown): void;
25
+ }): {
26
+ start(): void;
27
+ finish(): void;
28
+ };
@@ -0,0 +1,63 @@
1
+ const NAME = "data-vt-name";
2
+ /**
3
+ * View transition types of a navigation, or null for an instant swap. Defaults (`push`, `replace`,
4
+ * `back`, `forward`) are always first; link `data-transition` wins over the route's `transition`.
5
+ */
6
+ export function transitionTypes(nav) {
7
+ let route = nav.route;
8
+ if (typeof route === "function")
9
+ route = route(nav);
10
+ const link = nav.transition?.trim();
11
+ if (link === "none" || (route === false && !link))
12
+ return null;
13
+ return [nav.type, ...(link ? link.split(/\s+/) : route ? (route.types ?? []) : [])];
14
+ }
15
+ /** `data-vt-name` elements by name, one per name (duplicates drop the name). */
16
+ export function namedElements(root) {
17
+ const names = new Map();
18
+ for (const element of root?.querySelectorAll(`[${NAME}]`) ?? []) {
19
+ const name = element.getAttribute(NAME);
20
+ names.set(name, names.has(name) ? null : element);
21
+ }
22
+ return names;
23
+ }
24
+ /** Names present exactly once on both sides: the shared elements of a transition. */
25
+ export function sharedNames(before, after) {
26
+ return [...before].filter(([name, element]) => element && after.get(name)).map(([name]) => name);
27
+ }
28
+ /**
29
+ * Indicator timer: `show` after `delay` unless finished first; once shown, `hide` no earlier than
30
+ * `minDuration` after showing. `start` while running keeps the timers (redirects, quick re-clicks).
31
+ */
32
+ export function indicator(delay, minDuration, show, hide, clock = {
33
+ now: () => Date.now(),
34
+ set: (fn, ms) => setTimeout(fn, ms),
35
+ clear: (handle) => clearTimeout(handle),
36
+ }) {
37
+ let timer, shownAt = null, running = false;
38
+ return {
39
+ start() {
40
+ if (timer !== undefined && !running)
41
+ clock.clear(timer), (timer = undefined);
42
+ if (running)
43
+ return;
44
+ running = true;
45
+ if (shownAt === null)
46
+ timer = clock.set(() => ((timer = undefined), (shownAt = clock.now()), show()), delay);
47
+ },
48
+ finish() {
49
+ running = false;
50
+ if (shownAt === null) {
51
+ if (timer !== undefined)
52
+ clock.clear(timer), (timer = undefined);
53
+ return hide();
54
+ }
55
+ const rest = minDuration - (clock.now() - shownAt);
56
+ const done = () => ((timer = undefined), (shownAt = null), hide());
57
+ if (rest > 0)
58
+ timer = clock.set(done, rest);
59
+ else
60
+ done();
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,90 @@
1
+ /* @rivetjs/router route transitions: import from the app stylesheet (`@import "@rivetjs/router/transitions.css";`).
2
+ The router names the routes region `rivet-page` and shared elements (`data-vt-name`) only while a
3
+ transition runs, and sets `html[data-rivet-transition="push|replace|back|forward …types"]`. */
4
+
5
+ /* Layout chrome stays still: only the routes region and shared elements animate. */
6
+ ::view-transition-old(root),
7
+ ::view-transition-new(root) {
8
+ animation: none;
9
+ }
10
+ /* Input during the animation reaches the new page. */
11
+ ::view-transition {
12
+ pointer-events: none;
13
+ }
14
+
15
+ /* Default: the page area cross-fades. */
16
+ ::view-transition-group(rivet-page) {
17
+ animation-duration: 200ms;
18
+ }
19
+ ::view-transition-old(rivet-page) {
20
+ animation: 200ms ease both rivet-fade-out;
21
+ }
22
+ ::view-transition-new(rivet-page) {
23
+ animation: 200ms ease both rivet-fade-in;
24
+ }
25
+
26
+ /* Opt-in slides: add `rivet-slide` to the html element (or copy these rules with your own selector). */
27
+ html.rivet-slide[data-rivet-transition~="push"]::view-transition-old(rivet-page),
28
+ html.rivet-slide[data-rivet-transition~="forward"]::view-transition-old(rivet-page) {
29
+ animation: 200ms ease both rivet-slide-out-left;
30
+ }
31
+ html.rivet-slide[data-rivet-transition~="push"]::view-transition-new(rivet-page),
32
+ html.rivet-slide[data-rivet-transition~="forward"]::view-transition-new(rivet-page) {
33
+ animation: 200ms ease both rivet-slide-in-right;
34
+ }
35
+ html.rivet-slide[data-rivet-transition~="back"]::view-transition-old(rivet-page) {
36
+ animation: 200ms ease both rivet-slide-out-right;
37
+ }
38
+ html.rivet-slide[data-rivet-transition~="back"]::view-transition-new(rivet-page) {
39
+ animation: 200ms ease both rivet-slide-in-left;
40
+ }
41
+
42
+ /* Pending indicator: a thin indeterminate bar while `html[data-rivet-pending]` (after the delay). */
43
+ html[data-rivet-pending]::before {
44
+ content: "";
45
+ position: fixed;
46
+ inset: 0 0 auto 0;
47
+ z-index: 2147483647;
48
+ height: 2px;
49
+ background: currentColor;
50
+ transform-origin: 0 50%;
51
+ animation: rivet-progress 1.2s ease-in-out infinite;
52
+ pointer-events: none;
53
+ }
54
+
55
+ @keyframes rivet-fade-out {
56
+ to { opacity: 0; }
57
+ }
58
+ @keyframes rivet-fade-in {
59
+ from { opacity: 0; }
60
+ }
61
+ @keyframes rivet-slide-out-left {
62
+ to { opacity: 0; transform: translateX(-8%); }
63
+ }
64
+ @keyframes rivet-slide-in-right {
65
+ from { opacity: 0; transform: translateX(8%); }
66
+ }
67
+ @keyframes rivet-slide-out-right {
68
+ to { opacity: 0; transform: translateX(8%); }
69
+ }
70
+ @keyframes rivet-slide-in-left {
71
+ from { opacity: 0; transform: translateX(-8%); }
72
+ }
73
+ @keyframes rivet-progress {
74
+ 0% { transform: scaleX(0); }
75
+ 50% { transform: scaleX(0.6); }
76
+ 100% { transform: scaleX(1); opacity: 0; }
77
+ }
78
+
79
+ /* `reducedMotion: "allow"` leaves motion to CSS: remove it here. */
80
+ @media (prefers-reduced-motion: reduce) {
81
+ ::view-transition-group(*),
82
+ ::view-transition-old(*),
83
+ ::view-transition-new(*) {
84
+ animation: none !important;
85
+ }
86
+ html[data-rivet-pending]::before {
87
+ animation: none;
88
+ transform: scaleX(0.6);
89
+ }
90
+ }
@@ -0,0 +1,21 @@
1
+ import { type NavigationInfo } from "./index.js";
2
+ export { indicator, namedElements, sharedNames, transitionTypes, type RouteTransition } from "./transition-helpers.js";
3
+ /** Options of route transitions: view transitions, reduced motion and the pending indicator. */
4
+ export interface TransitionOptions {
5
+ /** `false`: pending UI only, instant swaps (default true). */
6
+ viewTransitions?: boolean;
7
+ /** `"skip"` (default): no view transition under `prefers-reduced-motion: reduce`; `"allow"`: CSS decides. */
8
+ reducedMotion?: "skip" | "allow";
9
+ /** Indicator timing (ms): `html[data-rivet-pending]` / `useNavigation().shown` after `delay`, kept ≥ `minDuration`. */
10
+ pending?: {
11
+ delay?: number;
12
+ minDuration?: number;
13
+ };
14
+ }
15
+ /** `rivet:before-swap` detail: the navigation, whether it animates, and `defer(promise)` (awaited ≤ 500 ms). */
16
+ export interface BeforeSwapDetail extends NavigationInfo {
17
+ readonly animate: boolean;
18
+ defer(promise: PromiseLike<unknown>): void;
19
+ }
20
+ /** Install the hooks (idempotent); generated apps call it when this module loads. */
21
+ export declare function install(options?: TransitionOptions): void;
@@ -0,0 +1,156 @@
1
+ // Route transitions + pending UI, loaded on the first navigation intent of apps built with
2
+ // `rivetRouter({ transitions })`. Wraps each route commit in a same-document View Transition when
3
+ // supported and enabled; drives the navigation signal and the pending attributes. No inline styles:
4
+ // transition names are CSSOM assignments, which a strict `style-src` does not block.
5
+ import { _afterSwap, _current, _transitions, settle } from "./index.js";
6
+ import { IDLE, navigation } from "./navigation.js";
7
+ import { indicator, namedElements, sharedNames, transitionTypes } from "./transition-helpers.js";
8
+ export { indicator, namedElements, sharedNames, transitionTypes } from "./transition-helpers.js";
9
+ const ROUTES = "[data-rivet-routes]";
10
+ const PENDING = "data-rivet-pending";
11
+ let installed = false;
12
+ /** Install the hooks (idempotent); generated apps call it when this module loads. */
13
+ export function install(options = {}) {
14
+ if (installed || typeof document === "undefined")
15
+ return;
16
+ installed = true;
17
+ const doc = document, html = doc.documentElement;
18
+ let anchor, nav = null, running = null;
19
+ const state = (shown) => nav ? { state: "loading", location: nav.to, from: nav.from, type: nav.type, shown } : { ...IDLE };
20
+ const timing = indicator(options.pending?.delay ?? 200, options.pending?.minDuration ?? 300, () => {
21
+ html.setAttribute(PENDING, "");
22
+ navigation.set(state(true));
23
+ }, () => {
24
+ html.removeAttribute(PENDING);
25
+ if (!nav)
26
+ navigation.set(IDLE);
27
+ });
28
+ // Clicked link: `data-rivet-pending` (+ `<Link pending>` classes) until the commit.
29
+ const mark = (next) => {
30
+ if (anchor === next)
31
+ return;
32
+ if (anchor) {
33
+ anchor.removeAttribute(PENDING);
34
+ anchor.classList.remove(...classes(anchor));
35
+ }
36
+ anchor = next;
37
+ if (anchor) {
38
+ anchor.setAttribute(PENDING, "");
39
+ anchor.classList.add(...classes(anchor));
40
+ }
41
+ };
42
+ const start = (next) => {
43
+ nav = next;
44
+ running?.skipTransition();
45
+ doc.querySelector(ROUTES)?.setAttribute("aria-busy", "true");
46
+ mark(next.anchor);
47
+ navigation.set(state(html.hasAttribute(PENDING)));
48
+ timing.start();
49
+ };
50
+ const finish = () => {
51
+ nav = null;
52
+ doc.querySelector(ROUTES)?.removeAttribute("aria-busy");
53
+ mark(undefined);
54
+ timing.finish();
55
+ // Held indicator: the state stays loading (shown) until it hides.
56
+ if (!html.hasAttribute(PENDING))
57
+ navigation.set(IDLE);
58
+ };
59
+ const event = (type, detail, cancelable = false) => doc.dispatchEvent(new CustomEvent(type, { detail, cancelable }));
60
+ _transitions.start = start;
61
+ _transitions.swap = (apply, next, info) => {
62
+ const types = transitionTypes(info);
63
+ const reduced = options.reducedMotion !== "allow" && matchMedia("(prefers-reduced-motion: reduce)").matches;
64
+ const animate = !!types && options.viewTransitions !== false && typeof doc.startViewTransition === "function" && !reduced && doc.visibilityState === "visible" && !info.visual;
65
+ // Superseded (a newer navigation started): `apply` discards; leave that navigation's state alone.
66
+ const current = () => nav === info || nav === null;
67
+ const host = doc.querySelector(ROUTES);
68
+ const named = [];
69
+ const name = (element, value) => {
70
+ if (!element)
71
+ return;
72
+ element.style.viewTransitionName = value;
73
+ const group = element.getAttribute("data-vt-class");
74
+ if (group && "viewTransitionClass" in element.style)
75
+ element.style.viewTransitionClass = group;
76
+ named.push(element);
77
+ };
78
+ let shared = [], incoming = new Map(), transition = null;
79
+ const update = () => {
80
+ if (!current())
81
+ return apply();
82
+ try {
83
+ apply();
84
+ }
85
+ finally {
86
+ for (const value of shared)
87
+ name(incoming.get(value), value);
88
+ settle(new URL(location.href));
89
+ finish();
90
+ _afterSwap();
91
+ event("rivet:after-swap", info);
92
+ }
93
+ };
94
+ const done = () => {
95
+ if (running === transition)
96
+ running = null;
97
+ for (const element of named) {
98
+ element.style.viewTransitionName = "";
99
+ if ("viewTransitionClass" in element.style)
100
+ element.style.viewTransitionClass = "";
101
+ }
102
+ if (host)
103
+ host.style.viewTransitionName = "";
104
+ html.removeAttribute("data-rivet-transition");
105
+ if (current())
106
+ event("rivet:navigated", info);
107
+ };
108
+ const run = () => {
109
+ if (!animate || !current()) {
110
+ update();
111
+ return done();
112
+ }
113
+ // Shared elements: names present once on both sides, assigned on the old DOM before the
114
+ // snapshot and on the new DOM inside `update`. The routes region animates as `rivet-page`.
115
+ incoming = namedElements(next);
116
+ const outgoing = namedElements(host);
117
+ shared = sharedNames(outgoing, incoming);
118
+ for (const value of shared)
119
+ name(outgoing.get(value), value);
120
+ if (host)
121
+ host.style.viewTransitionName = "rivet-page";
122
+ html.setAttribute("data-rivet-transition", types.join(" "));
123
+ running?.skipTransition();
124
+ try {
125
+ transition = doc.startViewTransition({ update, types: types });
126
+ }
127
+ catch {
128
+ // Before `types` (Chrome < 125): callback form; CSS keys off html[data-rivet-transition].
129
+ transition = doc.startViewTransition(update);
130
+ }
131
+ running = transition;
132
+ // Skipped before it animates (a newer navigation started): `ready` rejects, nothing to report.
133
+ transition.ready.catch(() => { });
134
+ transition.updateCallbackDone.catch((error) => queueMicrotask(() => {
135
+ throw error;
136
+ }));
137
+ transition.finished.then(done, done);
138
+ };
139
+ const deferred = [];
140
+ event("rivet:before-swap", { ...info, animate, defer: (promise) => void deferred.push(promise) });
141
+ if (!deferred.length)
142
+ return run();
143
+ let timeout;
144
+ void Promise.race([Promise.allSettled(deferred), new Promise((resolve) => (timeout = setTimeout(resolve, 500)))]).then(() => {
145
+ clearTimeout(timeout);
146
+ run();
147
+ });
148
+ };
149
+ // A navigation already in flight when this module arrived (first click before any intent).
150
+ const pending = _current();
151
+ if (pending)
152
+ start(pending);
153
+ }
154
+ function classes(anchor) {
155
+ return (anchor.getAttribute("data-rivet-pending-class") ?? "").split(/\s+/).filter(Boolean);
156
+ }
package/index.d.ts ADDED
@@ -0,0 +1,135 @@
1
+ import type { JSX } from "@rivetjs/runtime/jsx-runtime";
2
+ import type { HttpError as HttpErrorType } from "./shared/errors.js";
3
+ import type { ActionFailure } from "./shared/actions.js";
4
+ import type { Deferred, SERVER_EXPORT } from "./shared/server-exports.js";
5
+ export { load, guard, actions, entries, headers, onRequest, onGet, onPost, onPut, onPatch, onDelete, onOptions } from "./shared/server-exports.js";
6
+ export type { ServerExport, ServerKind, Deferred, LoadOptions } from "./shared/server-exports.js";
7
+ export { HttpError, redirect, deny, notFound, error, isHttpError, type RedirectOptions } from "./shared/errors.js";
8
+ export { safeRedirect } from "./shared/redirect.js";
9
+ export { parseCookies } from "./shared/cookie.js";
10
+ export { slashPath, TRAILING_SLASH, type TrailingSlash } from "./shared/slash.js";
11
+ export { BASE, withBase } from "./shared/base.js";
12
+ export { ActionFailure, fail, isActionFailure } from "./shared/actions.js";
13
+ export type { RequestEvent, ServerModule, Cookies, CookieOptions, ActionResult, Platform } from "./server/handler.js";
14
+ export type { Head, HeadLink, HeadMeta, HeadScript, HeadSink, HeadStyle, InlineScript, ScriptStrategy } from "./shared/data.js";
15
+ export type { ActionAnswer, AnswerError, DataAnswer } from "./shared/data.js";
16
+ /** Route id → params. Augmented by the generated `.rivet/routes.d.ts`. */
17
+ export interface RouteParams {
18
+ }
19
+ /** Route id → loader data (routes with a loader). Augmented by the generated `.rivet/routes.d.ts`. */
20
+ export interface RouteData {
21
+ }
22
+ /** Route id → its `actions(…)` object type (routes with server code). Augmented by `.rivet/routes.d.ts`. */
23
+ export interface RouteActions {
24
+ }
25
+ /** Layout id (`_layout`, `items/_layout`) → the params it receives. Augmented by `.rivet/routes.d.ts`. */
26
+ export interface LayoutParams {
27
+ }
28
+ /** Layout id → loader data (layouts with a loader). Augmented by `.rivet/routes.d.ts`. */
29
+ export interface LayoutData {
30
+ }
31
+ /** Ids of the app's file routes (e.g. `blog/[slug]`). */
32
+ export type RouteId = keyof RouteParams & string;
33
+ /** Ids of the app's `_layout` modules: the file path without extension (`_layout`, `(shop)/_layout`). */
34
+ export type LayoutId = keyof LayoutParams & string;
35
+ /** Augmented by the generated `.rivet/routes.d.ts`: `path` is the union of the app's page URL shapes. */
36
+ export interface RouteRegistry {
37
+ }
38
+ /**
39
+ * URL paths of the app's pages: params and splats are `${string}` (`"/" | \`/items/${string}\``).
40
+ * Plain `string` until codegen ran (no `.rivet/routes.d.ts` in the program).
41
+ */
42
+ export type RoutePath = RouteRegistry extends {
43
+ path: infer P extends string;
44
+ } ? P : string;
45
+ /** A page path with optional query and hash: `<Link to>`. */
46
+ export type LinkHref = RoutePath | `${RoutePath}?${string}` | `${RoutePath}#${string}`;
47
+ declare const unchecked: unique symbol;
48
+ /** `PageProps<Unchecked<typeof loader>>`: loader data without the serializability check. */
49
+ export interface Unchecked<T> {
50
+ readonly [unchecked]: T;
51
+ }
52
+ /** Loader data the page receives must survive the resume encoder: no functions, symbols or class instances. */
53
+ export interface NotSerializable<T> {
54
+ "rivet: loader data must be serializable (plain objects, arrays, primitives, Date, URL, URLSearchParams, RegExp, Map, Set, typed arrays, ArrayBuffer, DataView, Error — no functions, symbols or class instances); opt out with PageProps<Unchecked<typeof loader>>": T;
55
+ }
56
+ type Leaf = string | number | boolean | bigint | null | undefined | Date | URL | URLSearchParams | RegExp | ArrayBuffer | ArrayBufferView;
57
+ type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7];
58
+ type IsError<T> = T extends Error ? ("stack" extends keyof T ? true : false) : false;
59
+ type Serializable<T, D extends number = 8> = [D] extends [never] ? true : T extends Leaf ? true : T extends Deferred<infer V> ? Serializable<V, D> : T extends symbol | ((...args: never[]) => unknown) ? false : IsError<T> extends true ? true : T extends ReadonlyMap<infer K, infer V> ? Serializable<K, Depth[D]> extends true ? Serializable<V, Depth[D]> : false : T extends ReadonlySet<infer V> ? Serializable<V, Depth[D]> : T extends readonly (infer E)[] ? Serializable<E, Depth[D]> : T extends object ? false extends {
60
+ [K in keyof T]-?: Serializable<T[K], Depth[D]>;
61
+ }[keyof T] ? false : true : true;
62
+ type Returned<L> = L extends (...args: never[]) => infer R ? (R extends Deferred<infer V> ? Deferred<V> : Awaited<R>) : L;
63
+ /**
64
+ * Data type of a loader (`DataOf<typeof loader>`, `loader = load(…)`), checked to be serializable; plain data types
65
+ * pass through unchanged (`DataOf<Item[]>` is `Item[]`).
66
+ */
67
+ export type DataOf<T> = T extends Unchecked<infer L> ? Returned<L> : T extends (...args: never[]) => unknown ? Serializable<Returned<T>> extends true ? Returned<T> : NotSerializable<Returned<T>> : T;
68
+ /** The export of module type `M` tagged as server export kind `K` (by tag, any name); `never` without. */
69
+ type Tagged<M, K> = {
70
+ [N in keyof M]: M[N] extends {
71
+ readonly [SERVER_EXPORT]: K;
72
+ } ? M[N] : never;
73
+ }[keyof M];
74
+ /** Loader data of a route module (`typeof import("./page")`): its `load(…)` export, any name; `unknown` without. */
75
+ export type ModuleData<M> = [Tagged<M, "load">] extends [never] ? unknown : DataOf<Tagged<M, "load">>;
76
+ /** Form actions of a route module: its `actions(…)` export, any name; `{}` without. */
77
+ export type ModuleActions<M> = [Tagged<M, "actions">] extends [never] ? {} : Tagged<M, "actions">;
78
+ /** A string that is not an id of `RouteParams` / `LayoutParams`: `data` and `params` are unusable. */
79
+ export interface UnknownRoute<Id> {
80
+ "rivet: not a route or layout id of this app (see .rivet/routes.d.ts)": Id;
81
+ }
82
+ type IsLiteral<D> = D extends string ? (string extends D ? false : true) : false;
83
+ type PageData<D> = D extends RouteId ? (D extends keyof RouteData ? RouteData[D] : undefined) : IsLiteral<D> extends true ? UnknownRoute<D> : DataOf<D>;
84
+ type PageParams<D, P> = D extends RouteId ? RouteParams[D] : IsLiteral<D> extends true ? UnknownRoute<D> : P;
85
+ type LayoutDataOf<D> = D extends LayoutId ? (D extends keyof LayoutData ? LayoutData[D] : undefined) : IsLiteral<D> extends true ? UnknownRoute<D> : DataOf<D>;
86
+ type LayoutParamsOf<D, P> = D extends LayoutId ? LayoutParams[D] : IsLiteral<D> extends true ? UnknownRoute<D> : P;
87
+ /**
88
+ * Props every page component receives (layouts: `LayoutProps`). `D` is a route id (generated
89
+ * types: `PageProps<"items/[id]">` types data, params), the loader (`PageProps<typeof loader>`
90
+ * infers the data) or the data type.
91
+ */
92
+ export interface PageProps<D = unknown, P extends Record<string, string> = Record<string, string>> {
93
+ data: PageData<D>;
94
+ params: Readonly<PageParams<D, P>>;
95
+ url: URL;
96
+ }
97
+ /**
98
+ * Props of a live page (`export const live = true`): `data` is an accessor. Same-URL revalidation (an
99
+ * enhanced form submit, `invalidate()`) updates it in place — the page is not re-rendered, so its
100
+ * local state (an open drawer, typed input, focus) survives. `D` as in `PageProps`.
101
+ */
102
+ export interface LivePageProps<D = unknown, P extends Record<string, string> = Record<string, string>> {
103
+ data: () => PageData<D>;
104
+ params: Readonly<PageParams<D, P>>;
105
+ url: URL;
106
+ }
107
+ /**
108
+ * Props of a `_layout.tsx` component. Accessors: the root layout stays mounted across navigations.
109
+ * `D` is a layout id (`LayoutProps<"items/_layout">`), the loader or the data type.
110
+ */
111
+ export interface LayoutProps<D = unknown, P = {}> {
112
+ data: () => LayoutDataOf<D>;
113
+ params: () => Readonly<LayoutParamsOf<D, P>>;
114
+ url: () => URL;
115
+ children?: JSX.Element;
116
+ }
117
+ type Outcome<R> = R extends HttpErrorType | Response ? never : R extends ActionFailure<infer F> ? {
118
+ readonly result: F;
119
+ readonly status: number;
120
+ } : {
121
+ readonly result: R;
122
+ readonly status?: undefined;
123
+ };
124
+ type ActionsOf<A> = A extends RouteId ? (A extends keyof RouteActions ? RouteActions[A] : {}) : A;
125
+ /** Action names of a route id (`RouteActions`) or an actions object type (`typeof forms`, `forms = actions(…)`). */
126
+ export type ActionName<A> = keyof ActionsOf<A> & string;
127
+ /**
128
+ * Typed `actionResult()`: per action name its return value (`redirect()`/`Response` excluded), or
129
+ * the data and status of `fail()`. `A` is a route id or `typeof forms` (`forms = actions(…)`).
130
+ */
131
+ export type ActionResultOf<A, N extends ActionName<A> = ActionName<A>> = {
132
+ [K in N]: {
133
+ readonly name: K;
134
+ } & Outcome<Awaited<ActionsOf<A>[K] extends (...args: never[]) => infer R ? R : never>>;
135
+ }[N];
package/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // Route-module API: server export wrappers, control-flow helpers and types for guards, loaders and actions.
2
+ export { load, guard, actions, entries, headers, onRequest, onGet, onPost, onPut, onPatch, onDelete, onOptions } from "./shared/server-exports.js";
3
+ export { HttpError, redirect, deny, notFound, error, isHttpError } from "./shared/errors.js";
4
+ export { safeRedirect } from "./shared/redirect.js";
5
+ export { parseCookies } from "./shared/cookie.js";
6
+ export { slashPath, TRAILING_SLASH } from "./shared/slash.js";
7
+ export { BASE, withBase } from "./shared/base.js";
8
+ export { ActionFailure, fail, isActionFailure } from "./shared/actions.js";