@rangojs/router 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +52 -0
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/thenable-status.d.ts +9 -0
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +884 -768
- package/package.json +4 -4
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +43 -1
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +75 -80
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/ssr-suspension-warning.ts +2 -13
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -8,7 +8,7 @@ argument-hint: [layout|route|parallel|intercept]
|
|
|
8
8
|
|
|
9
9
|
`transition()` opts a route (or group of routes) into transition-driven navigation. It does two things, and you choose how far to go:
|
|
10
10
|
|
|
11
|
-
1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions.
|
|
11
|
+
1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions. While the hold lasts, `useLoader` readers of the loaders being re-run report `isLoading: true` — keyed by loader family (`$$id`), not by segment; see the hooks skill, `data.md`.
|
|
12
12
|
2. **`<ViewTransition>` (the animation, layered on top).** On React 19.3+ (or an experimental build), rango also wraps the segment content in React's `<ViewTransition>` so the swap cross-fades/morphs. This is the only part that needs 19.3+; pass `viewTransition: false` to keep #1 without it (and place your own `<ViewTransition>` where you want it).
|
|
13
13
|
|
|
14
14
|
> The `<ViewTransition>` layer requires a React that exports `<ViewTransition>` / `addTransitionType`: stable 19.3+ or an experimental build. Rango feature-detects it; on React 19.2 that layer is a no-op — but the `startTransition` driving (content hold) still applies.
|
|
@@ -52,7 +52,7 @@ A simple cross-fade between pages that share a layout:
|
|
|
52
52
|
import { urls } from "@rangojs/router";
|
|
53
53
|
import { Outlet } from "@rangojs/router/client";
|
|
54
54
|
|
|
55
|
-
function ShopShell(
|
|
55
|
+
function ShopShell() {
|
|
56
56
|
return (
|
|
57
57
|
<div className="shop">
|
|
58
58
|
<NavBar />
|
|
@@ -74,23 +74,34 @@ export const urlpatterns = urls(({ layout, path, transition }) => [
|
|
|
74
74
|
]);
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
The config values are React `<ViewTransition>` classes: React applies them as
|
|
78
|
+
`view-transition-class`, so the CSS targets the view-transition
|
|
79
|
+
pseudo-elements by class:
|
|
80
|
+
|
|
77
81
|
```css
|
|
78
|
-
::view-transition-old(
|
|
79
|
-
animation:
|
|
82
|
+
::view-transition-old(.page-fade) {
|
|
83
|
+
animation: 200ms ease both fade-out;
|
|
84
|
+
}
|
|
85
|
+
::view-transition-new(.page-fade) {
|
|
86
|
+
animation: 200ms ease both fade-in;
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
@keyframes fade-out {
|
|
89
|
+
to {
|
|
90
|
+
opacity: 0;
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
@keyframes fade-in {
|
|
94
|
+
from {
|
|
95
|
+
opacity: 0;
|
|
96
|
+
}
|
|
86
97
|
}
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
Navigating between `/`, `/about`, and `/contact`
|
|
100
|
+
Navigating between `/`, `/about`, and `/contact` cross-fades the `<Outlet />` content with the `page-fade` class. The shell (NavBar, Footer) does not animate because the wrap sits inside the shell, not around it.
|
|
90
101
|
|
|
91
102
|
## Direction-aware transitions
|
|
92
103
|
|
|
93
|
-
`ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward
|
|
104
|
+
`ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward navigations restored from the client cache as `"navigation-back"` (a back/forward that has to refetch from the server is tagged `"navigation"`):
|
|
94
105
|
|
|
95
106
|
```tsx
|
|
96
107
|
layout(<ShopShell />, () => [
|
|
@@ -106,12 +117,19 @@ layout(<ShopShell />, () => [
|
|
|
106
117
|
```
|
|
107
118
|
|
|
108
119
|
```css
|
|
109
|
-
.slide-left {
|
|
110
|
-
animation
|
|
120
|
+
::view-transition-old(.slide-left) {
|
|
121
|
+
animation: 250ms ease both slide-out-to-left;
|
|
122
|
+
}
|
|
123
|
+
::view-transition-new(.slide-left) {
|
|
124
|
+
animation: 250ms ease both slide-in-from-right;
|
|
111
125
|
}
|
|
112
|
-
.slide-right {
|
|
113
|
-
animation
|
|
126
|
+
::view-transition-old(.slide-right) {
|
|
127
|
+
animation: 250ms ease both slide-out-to-right;
|
|
114
128
|
}
|
|
129
|
+
::view-transition-new(.slide-right) {
|
|
130
|
+
animation: 250ms ease both slide-in-from-left;
|
|
131
|
+
}
|
|
132
|
+
/* @keyframes slide-out-to-left, slide-in-from-right, ... defined as usual */
|
|
115
133
|
```
|
|
116
134
|
|
|
117
135
|
> Note: `"action"` is only tagged on partial-update action/refetch paths today; ordinary `server-action-bridge` commits (`useAction` / `useActionState` revalidations) are not currently tagged. Don't rely on an `action`-keyed class to fire on every form action.
|
|
@@ -147,8 +165,8 @@ function GalleryShell() {
|
|
|
147
165
|
<main>
|
|
148
166
|
<Outlet /> {/* page transition lands here */}
|
|
149
167
|
</main>
|
|
150
|
-
|
|
151
|
-
|
|
168
|
+
{/* modal mounts here, as a sibling of the VT */}
|
|
169
|
+
<ParallelOutlet name="@modal" />
|
|
152
170
|
</>
|
|
153
171
|
);
|
|
154
172
|
}
|
|
@@ -222,7 +240,7 @@ interface TransitionConfig {
|
|
|
222
240
|
|
|
223
241
|
## Conditional transitions (`when`)
|
|
224
242
|
|
|
225
|
-
`transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request
|
|
243
|
+
`transition({ when })` gates the hold per request. The predicate runs server-side and outside any cache scope; return `false` to drop this segment's transition for the request. The hold is navigation-wide: the navigation streams its `loading()` fallback instead of holding only when no other matched segment still has a transition (the common case is a single `transition()` on the route).
|
|
226
244
|
|
|
227
245
|
Timing follows the route's rendering contract:
|
|
228
246
|
|
|
@@ -280,7 +298,10 @@ urls(({ path, transition }) => [
|
|
|
280
298
|
]),
|
|
281
299
|
]);
|
|
282
300
|
|
|
283
|
-
// ProductPage renders the boundary itself, exactly where it's wanted
|
|
301
|
+
// ProductPage renders the boundary itself, exactly where it's wanted
|
|
302
|
+
// (ViewTransition is exported by React 19.3+ and experimental builds):
|
|
303
|
+
import { ViewTransition } from "react";
|
|
304
|
+
|
|
284
305
|
function ProductPage() {
|
|
285
306
|
return (
|
|
286
307
|
<ViewTransition name="hero">
|
|
@@ -311,7 +332,7 @@ const router = createRouter<AppEnv>({ viewTransition: false });
|
|
|
311
332
|
| `transition({ viewTransition: false })` | `"auto"` | no wrap (per-route wins) | **ST only** |
|
|
312
333
|
| `transition({ viewTransition: false })` | `false` | no wrap | ST only |
|
|
313
334
|
|
|
314
|
-
On
|
|
335
|
+
On React 19.2 (no `<ViewTransition>` export) the "VT" column is a no-op, so every row collapses to its `startTransition`-only behavior there.
|
|
315
336
|
|
|
316
337
|
| Config | Router boundary | startTransition driving (no skeleton flash) | Your own `<ViewTransition name>` |
|
|
317
338
|
| ---------------------------------------------------- | ---------------- | ------------------------------------------- | ---------------------------------- |
|
|
@@ -320,7 +341,7 @@ On stable React the "VT" column is always a no-op (there is no `<ViewTransition>
|
|
|
320
341
|
| `transition({ viewTransition: false })` | none | yes | fires alone |
|
|
321
342
|
| global `viewTransition: false`, route `transition()` | none | yes | fires alone |
|
|
322
343
|
|
|
323
|
-
> On
|
|
344
|
+
> On React 19.2 there is no `<ViewTransition>` at all, so `viewTransition: false` is visually a no-op there — but the startTransition driving and content-hold still apply, identical to `transition({})`.
|
|
324
345
|
|
|
325
346
|
## Testing a transition() route
|
|
326
347
|
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* turns these into tsc errors. Run via tsconfig.augment-check.json.
|
|
7
7
|
*/
|
|
8
8
|
import "./augment.js";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
Handler,
|
|
11
|
+
MiddlewareContext,
|
|
12
|
+
ResponseHandlerContext,
|
|
13
|
+
RouteParams,
|
|
14
|
+
RouteSearchParams,
|
|
15
|
+
} from "../index.js";
|
|
10
16
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
11
17
|
import { href } from "../href-client.js";
|
|
12
18
|
import type { Money, TestBindings } from "./augment.js";
|
|
@@ -39,6 +45,25 @@ const varsHandler: Handler<"home"> = (ctx) => {
|
|
|
39
45
|
};
|
|
40
46
|
void varsHandler;
|
|
41
47
|
|
|
48
|
+
// Response-route and middleware ctx.reverse resolve global names only (no
|
|
49
|
+
// include() scope): generated names and params are validated, `.name` is not.
|
|
50
|
+
function responseReverse(ctx: ResponseHandlerContext): void {
|
|
51
|
+
ctx.reverse("home");
|
|
52
|
+
ctx.reverse("blog.post", { slug: "hello" });
|
|
53
|
+
// @ts-expect-error - dot-local name; no include() scope to resolve it
|
|
54
|
+
ctx.reverse(".home");
|
|
55
|
+
// @ts-expect-error - unknown global name is rejected
|
|
56
|
+
ctx.reverse("nope");
|
|
57
|
+
}
|
|
58
|
+
void responseReverse;
|
|
59
|
+
|
|
60
|
+
function middlewareReverse(ctx: MiddlewareContext): void {
|
|
61
|
+
ctx.reverse("blog.post", { slug: "hello" });
|
|
62
|
+
// @ts-expect-error - dot-local name; no include() scope to resolve it
|
|
63
|
+
ctx.reverse(".home");
|
|
64
|
+
}
|
|
65
|
+
void middlewareReverse;
|
|
66
|
+
|
|
42
67
|
// routeName narrows to the generated route names.
|
|
43
68
|
type _routeName = Expect<
|
|
44
69
|
Equal<DefaultRouteName, "home" | "blog.post" | "search">
|
package/src/browser/index.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// import { initBrowserApp, Rango } from "
|
|
6
|
+
// import { initBrowserApp, Rango } from "@rangojs/router/browser";
|
|
7
7
|
//
|
|
8
8
|
// For React components (Link, useNavigation, etc.):
|
|
9
|
-
// import { Link, useNavigation, useAction, href } from "
|
|
9
|
+
// import { Link, useNavigation, useAction, href } from "@rangojs/router/client";
|
|
10
10
|
//
|
|
11
11
|
// ============================================================================
|
|
12
12
|
|
|
@@ -18,6 +18,7 @@ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
|
|
|
18
18
|
import {
|
|
19
19
|
handleNavigationStart,
|
|
20
20
|
handleNavigationEnd,
|
|
21
|
+
handleTraversalStart,
|
|
21
22
|
ensureHistoryKey,
|
|
22
23
|
} from "./scroll-restoration.js";
|
|
23
24
|
|
|
@@ -496,6 +497,7 @@ export function createNavigationBridge(
|
|
|
496
497
|
async handlePopstate(): Promise<void> {
|
|
497
498
|
// Abort any pending navigation to prevent race conditions
|
|
498
499
|
eventController.abortNavigation();
|
|
500
|
+
handleTraversalStart();
|
|
499
501
|
|
|
500
502
|
const url = window.location.href;
|
|
501
503
|
|
|
@@ -698,7 +700,7 @@ export function createNavigationBridge(
|
|
|
698
700
|
tx.handle.signal,
|
|
699
701
|
tx.with({
|
|
700
702
|
url,
|
|
701
|
-
|
|
703
|
+
traversal: true,
|
|
702
704
|
scroll: false,
|
|
703
705
|
intercept: isIntercept,
|
|
704
706
|
interceptSourceUrl,
|
|
@@ -771,9 +773,6 @@ export function createNavigationBridge(
|
|
|
771
773
|
"[Browser] Page restored from bfcache, resetting navigation state",
|
|
772
774
|
);
|
|
773
775
|
eventController.abortNavigation();
|
|
774
|
-
// pagehide flips scrollRestoration to "auto" for bfcache compat;
|
|
775
|
-
// restore "manual" so the router controls scroll on SPA navigations.
|
|
776
|
-
window.history.scrollRestoration = "manual";
|
|
777
776
|
}
|
|
778
777
|
};
|
|
779
778
|
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
} from "./scroll-restoration.js";
|
|
12
12
|
import type { EventController, NavigationHandle } from "./event-controller.js";
|
|
13
13
|
import { debugLog } from "./logging.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildHistoryState,
|
|
16
|
+
mergeLocationState,
|
|
17
|
+
pushHistoryWithIdx,
|
|
18
|
+
} from "./history-state.js";
|
|
15
19
|
|
|
16
20
|
export { resolveNavigationState } from "./history-state.js";
|
|
17
21
|
|
|
@@ -49,6 +53,12 @@ interface CommitOptions {
|
|
|
49
53
|
cacheOnly?: boolean;
|
|
50
54
|
/** Server-set location state to merge into history.pushState */
|
|
51
55
|
serverState?: Record<string, unknown>;
|
|
56
|
+
/**
|
|
57
|
+
* Back/forward: history is already at this entry. Its state (scroll key,
|
|
58
|
+
* location state, idx) stays as the browser restored it; serverState is
|
|
59
|
+
* merged in. The page left was saved by handleTraversalStart.
|
|
60
|
+
*/
|
|
61
|
+
traversal?: boolean;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
/**
|
|
@@ -131,6 +141,7 @@ export function createNavigationTransaction(
|
|
|
131
141
|
interceptSourceUrl,
|
|
132
142
|
cacheOnly,
|
|
133
143
|
serverState,
|
|
144
|
+
traversal,
|
|
134
145
|
} = opts;
|
|
135
146
|
|
|
136
147
|
const parsedUrl = new URL(url, window.location.origin);
|
|
@@ -145,7 +156,7 @@ export function createNavigationTransaction(
|
|
|
145
156
|
return { scroll: false };
|
|
146
157
|
}
|
|
147
158
|
|
|
148
|
-
handleNavigationStart();
|
|
159
|
+
if (!traversal) handleNavigationStart();
|
|
149
160
|
|
|
150
161
|
store.setSegmentIds(segmentIds);
|
|
151
162
|
store.setCurrentUrl(url);
|
|
@@ -162,6 +173,15 @@ export function createNavigationTransaction(
|
|
|
162
173
|
return { scroll: false };
|
|
163
174
|
}
|
|
164
175
|
|
|
176
|
+
if (traversal) {
|
|
177
|
+
if (serverState && Object.keys(serverState).length > 0) {
|
|
178
|
+
mergeLocationState(serverState);
|
|
179
|
+
}
|
|
180
|
+
handle.complete(parsedUrl);
|
|
181
|
+
debugLog("[Browser] Traversal committed, historyKey:", historyKey);
|
|
182
|
+
return { scroll };
|
|
183
|
+
}
|
|
184
|
+
|
|
165
185
|
const historyState = buildHistoryState(
|
|
166
186
|
opts.state,
|
|
167
187
|
{ intercept, sourceUrl: interceptSourceUrl },
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "./validate-redirect-origin.js";
|
|
32
32
|
import type { NavigationUpdate } from "./types.js";
|
|
33
33
|
import { OPTIMISTIC_COMMIT_TRANSITION_TYPE } from "./optimistic-commit.js";
|
|
34
|
+
import { loaderStore } from "../loader-store.js";
|
|
34
35
|
import {
|
|
35
36
|
collectClientRevalidationDecisions,
|
|
36
37
|
setActiveInterceptTargets,
|
|
@@ -42,7 +43,9 @@ function toScrollPayload(
|
|
|
42
43
|
return { enabled: scroll !== false ? scroll : false };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function shouldStartViewTransition(
|
|
46
|
+
export function shouldStartViewTransition(
|
|
47
|
+
segments: ResolvedSegment[],
|
|
48
|
+
): boolean {
|
|
46
49
|
let hasIntercept = false;
|
|
47
50
|
let hasTransition = false;
|
|
48
51
|
for (const s of segments) {
|
|
@@ -52,6 +55,29 @@ function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
|
52
55
|
return !hasIntercept && hasTransition;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Transition commit. Every held commit announces the loader streams the
|
|
60
|
+
* committed tree is still receiving (loader-store.ts announcePendingStreams)
|
|
61
|
+
* so held useLoader readers pin isLoading:true until this transition lands;
|
|
62
|
+
* settled/cached segments make it a no-op. Urgent commits call onUpdate
|
|
63
|
+
* directly: nothing is held there, a reader of a pending stream suspends to
|
|
64
|
+
* its fallback. Also used by renderRoute's navigate() (testing/render-route.tsx).
|
|
65
|
+
*/
|
|
66
|
+
export function commitInTransition(
|
|
67
|
+
onUpdate: UpdateSubscriber,
|
|
68
|
+
segments: readonly ResolvedSegment[],
|
|
69
|
+
update: NavigationUpdate,
|
|
70
|
+
transitionTypes: readonly string[],
|
|
71
|
+
): void {
|
|
72
|
+
startTransition(() => {
|
|
73
|
+
loaderStore.announcePendingStreams(segments);
|
|
74
|
+
if (addTransitionType) {
|
|
75
|
+
for (const type of transitionTypes) addTransitionType(type);
|
|
76
|
+
}
|
|
77
|
+
onUpdate(update);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
/**
|
|
56
82
|
* Configuration for creating a partial updater
|
|
57
83
|
*/
|
|
@@ -351,12 +377,9 @@ export function createPartialUpdater(
|
|
|
351
377
|
};
|
|
352
378
|
|
|
353
379
|
if (shouldStartViewTransition(existingSegments)) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
onUpdate(cachedUpdate);
|
|
359
|
-
});
|
|
380
|
+
commitInTransition(onUpdate, existingSegments, cachedUpdate, [
|
|
381
|
+
"navigation",
|
|
382
|
+
]);
|
|
360
383
|
} else {
|
|
361
384
|
onUpdate(cachedUpdate);
|
|
362
385
|
}
|
|
@@ -589,31 +612,27 @@ export function createPartialUpdater(
|
|
|
589
612
|
|
|
590
613
|
const scrollPayload = toScrollPayload(navScroll);
|
|
591
614
|
|
|
615
|
+
const update: NavigationUpdate = {
|
|
616
|
+
root: newTree,
|
|
617
|
+
metadata: payload.metadata!,
|
|
618
|
+
scroll: scrollPayload,
|
|
619
|
+
};
|
|
592
620
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
metadata: payload.metadata!,
|
|
600
|
-
scroll: scrollPayload,
|
|
601
|
-
});
|
|
602
|
-
});
|
|
621
|
+
commitInTransition(
|
|
622
|
+
onUpdate,
|
|
623
|
+
reconciled.mainSegments,
|
|
624
|
+
update,
|
|
625
|
+
hasTransition ? ["action"] : [],
|
|
626
|
+
);
|
|
603
627
|
} else if (hasTransition) {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
root: newTree,
|
|
613
|
-
metadata: payload.metadata!,
|
|
614
|
-
scroll: scrollPayload,
|
|
615
|
-
});
|
|
616
|
-
});
|
|
628
|
+
commitInTransition(
|
|
629
|
+
onUpdate,
|
|
630
|
+
reconciled.mainSegments,
|
|
631
|
+
update,
|
|
632
|
+
optimisticPresented
|
|
633
|
+
? ["navigation", OPTIMISTIC_COMMIT_TRANSITION_TYPE]
|
|
634
|
+
: ["navigation"],
|
|
635
|
+
);
|
|
617
636
|
} else if (fullyPrefetched || isSameStructureNav || optimisticPresented) {
|
|
618
637
|
// Content-hold commit, two triggers. Fully-prefetched nav: the payload
|
|
619
638
|
// is fully resolved (forceAwait above), so the transition commits
|
|
@@ -637,26 +656,18 @@ export function createPartialUpdater(
|
|
|
637
656
|
// optimistic clientUrls() presentation commits here too: the group
|
|
638
657
|
// segment reconciles in place (clientGroup key), so a read that still
|
|
639
658
|
// suspends must hold the presented content, not flash a fallback.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
metadata: payload.metadata!,
|
|
647
|
-
scroll: scrollPayload,
|
|
648
|
-
});
|
|
649
|
-
});
|
|
659
|
+
commitInTransition(
|
|
660
|
+
onUpdate,
|
|
661
|
+
reconciled.mainSegments,
|
|
662
|
+
update,
|
|
663
|
+
optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
|
|
664
|
+
);
|
|
650
665
|
} else {
|
|
651
666
|
// Cold/partially-prefetched nav that mounts NEW segments: normal
|
|
652
667
|
// commit so fallbacks stream like a first load and the click has
|
|
653
668
|
// visible feedback. Explicit transition() routes keep the
|
|
654
669
|
// content-hold via the hasTransition branch above (the opt-in).
|
|
655
|
-
onUpdate(
|
|
656
|
-
root: newTree,
|
|
657
|
-
metadata: payload.metadata!,
|
|
658
|
-
scroll: scrollPayload,
|
|
659
|
-
});
|
|
670
|
+
onUpdate(update);
|
|
660
671
|
}
|
|
661
672
|
|
|
662
673
|
debugLog("[Browser] Navigation complete");
|
|
@@ -689,6 +700,11 @@ export function createPartialUpdater(
|
|
|
689
700
|
|
|
690
701
|
const fullHasTransition = shouldStartViewTransition(segments);
|
|
691
702
|
const fullScrollPayload = toScrollPayload(fullScroll);
|
|
703
|
+
const fullUpdate: NavigationUpdate = {
|
|
704
|
+
root: newTree,
|
|
705
|
+
metadata: payload.metadata!,
|
|
706
|
+
scroll: fullScrollPayload,
|
|
707
|
+
};
|
|
692
708
|
|
|
693
709
|
if (mode.type === "stale-revalidation") {
|
|
694
710
|
await rawStreamComplete;
|
|
@@ -704,44 +720,23 @@ export function createPartialUpdater(
|
|
|
704
720
|
);
|
|
705
721
|
return;
|
|
706
722
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
metadata: payload.metadata!,
|
|
714
|
-
scroll: fullScrollPayload,
|
|
715
|
-
});
|
|
716
|
-
});
|
|
723
|
+
commitInTransition(
|
|
724
|
+
onUpdate,
|
|
725
|
+
segments,
|
|
726
|
+
fullUpdate,
|
|
727
|
+
fullHasTransition ? ["action"] : [],
|
|
728
|
+
);
|
|
717
729
|
} else if (mode.type === "action") {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
metadata: payload.metadata!,
|
|
725
|
-
scroll: fullScrollPayload,
|
|
726
|
-
});
|
|
727
|
-
});
|
|
730
|
+
commitInTransition(
|
|
731
|
+
onUpdate,
|
|
732
|
+
segments,
|
|
733
|
+
fullUpdate,
|
|
734
|
+
fullHasTransition ? ["action"] : [],
|
|
735
|
+
);
|
|
728
736
|
} else if (fullHasTransition) {
|
|
729
|
-
|
|
730
|
-
if (addTransitionType) {
|
|
731
|
-
addTransitionType("navigation");
|
|
732
|
-
}
|
|
733
|
-
onUpdate({
|
|
734
|
-
root: newTree,
|
|
735
|
-
metadata: payload.metadata!,
|
|
736
|
-
scroll: fullScrollPayload,
|
|
737
|
-
});
|
|
738
|
-
});
|
|
737
|
+
commitInTransition(onUpdate, segments, fullUpdate, ["navigation"]);
|
|
739
738
|
} else {
|
|
740
|
-
onUpdate(
|
|
741
|
-
root: newTree,
|
|
742
|
-
metadata: payload.metadata!,
|
|
743
|
-
scroll: fullScrollPayload,
|
|
744
|
-
});
|
|
739
|
+
onUpdate(fullUpdate);
|
|
745
740
|
}
|
|
746
741
|
|
|
747
742
|
return;
|
|
@@ -4,7 +4,7 @@ import { useEffect } from "react";
|
|
|
4
4
|
import { initScrollRestoration } from "../scroll-restoration.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Props for ScrollRestoration
|
|
7
|
+
* Props for `<Html.ScrollRestoration />`.
|
|
8
8
|
*/
|
|
9
9
|
export interface ScrollRestorationProps {
|
|
10
10
|
/**
|
|
@@ -25,10 +25,10 @@ export interface ScrollRestorationProps {
|
|
|
25
25
|
* const byPathname = (location) => location.pathname;
|
|
26
26
|
*
|
|
27
27
|
* // Restore based on pathname (same URL = same scroll)
|
|
28
|
-
* <ScrollRestoration getKey={byPathname} />
|
|
28
|
+
* <Html.ScrollRestoration getKey={byPathname} />
|
|
29
29
|
*
|
|
30
30
|
* // Restore based on unique history entry (default)
|
|
31
|
-
* // <ScrollRestoration /> — omit getKey to use location.key
|
|
31
|
+
* // <Html.ScrollRestoration /> — omit getKey to use location.key
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
getKey?: (location: {
|
|
@@ -40,7 +40,7 @@ export interface ScrollRestorationProps {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* ScrollRestoration
|
|
43
|
+
* Public as `Html.ScrollRestoration` (`@rangojs/router/client`).
|
|
44
44
|
*
|
|
45
45
|
* Enables scroll position restoration across navigations:
|
|
46
46
|
* - Saves scroll positions to sessionStorage
|
|
@@ -57,7 +57,7 @@ export interface ScrollRestorationProps {
|
|
|
57
57
|
* return (
|
|
58
58
|
* <html>
|
|
59
59
|
* <body>
|
|
60
|
-
* <ScrollRestoration />
|
|
60
|
+
* <Html.ScrollRestoration />
|
|
61
61
|
* {children}
|
|
62
62
|
* </body>
|
|
63
63
|
* </html>
|
|
@@ -78,7 +78,7 @@ export function ScrollRestoration({ getKey }: ScrollRestorationProps) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Hook to initialize scroll restoration
|
|
80
80
|
*
|
|
81
|
-
* Alternative to
|
|
81
|
+
* Alternative to `<Html.ScrollRestoration />` for more control.
|
|
82
82
|
*
|
|
83
83
|
* @example
|
|
84
84
|
* ```tsx
|
|
@@ -655,7 +655,7 @@ export interface RangoProps {}
|
|
|
655
655
|
*
|
|
656
656
|
* @example
|
|
657
657
|
* ```tsx
|
|
658
|
-
* import { initBrowserApp, Rango } from "
|
|
658
|
+
* import { initBrowserApp, Rango } from "@rangojs/router/browser";
|
|
659
659
|
* import { rscStream } from "rsc-html-stream/client";
|
|
660
660
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
661
661
|
*
|