@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -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-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- 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 +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- 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 +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -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/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- 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 +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- 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/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.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-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- 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/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- 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/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -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 +75 -57
- 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 +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -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 +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- 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 +26 -6
- 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/serve-shell-request.md +188 -0
- 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 +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- 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 +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- 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 +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -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 +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- 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 +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -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 +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -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 +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
|
@@ -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. While the hold lasts, `useLoader` readers of the loaders being re-run report `isLoading: true` —
|
|
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,12 +240,12 @@ 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
|
|
|
229
247
|
- On an ordinary route it runs after the route handler during fresh resolution, so `get()` can read handler- and middleware-set context. Cache/prerender hits replay the stored decision.
|
|
230
|
-
- On a `ppr` route it is automatically hoisted before route handlers and runs on every match, including runtime-cache, prerender, document-shell, and partial-navigation replay. It can read URL/params/action metadata, `env`, and middleware-set context, but not values set by route handlers
|
|
248
|
+
- On a `ppr` route it is automatically hoisted before route handlers and runs on every match, including runtime-cache, prerender, document-shell, and partial-navigation replay. It can read URL/params/action metadata, `env`, and middleware-set context, but not values set by route handlers, because a shell HIT never runs a handler.
|
|
231
249
|
|
|
232
250
|
Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for request-context reads:
|
|
233
251
|
|
|
@@ -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">
|
|
@@ -14,6 +14,29 @@
|
|
|
14
14
|
/** Default prefix when `stateCookiePrefix` is unset or empty after sanitization. */
|
|
15
15
|
export const DEFAULT_STATE_COOKIE_PREFIX = "rango-state";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The fresh-reads cookie: the state cookie PREFIX plus this suffix
|
|
19
|
+
* (`rango-state-fresh` by default). A response whose request ran
|
|
20
|
+
* updateTag()/revalidateTag() sets it (HttpOnly, SameSite=Lax, Path=/,
|
|
21
|
+
* Max-Age = the stores' longest memo staleness), and the same user's requests
|
|
22
|
+
* carrying it skip the stores' isolate memos (src/cache/isolate-tag-memo.ts).
|
|
23
|
+
* Keyed by prefix, not by router, so a mutation through one router also
|
|
24
|
+
* reaches the memos another router on the host reads (they often share a
|
|
25
|
+
* store); a router with its own `stateCookiePrefix` gets its own cookie.
|
|
26
|
+
*/
|
|
27
|
+
const FRESH_READS_COOKIE_SUFFIX = "-fresh";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The fresh-reads cookie name for a router's resolved state cookie name
|
|
31
|
+
* (`{prefix}_{routerId}`; the prefix never contains `_`, see
|
|
32
|
+
* router/state-cookie-name.ts).
|
|
33
|
+
*/
|
|
34
|
+
export function freshReadsCookieName(stateCookieName: string): string {
|
|
35
|
+
const cut = stateCookieName.indexOf("_");
|
|
36
|
+
const prefix = cut < 0 ? stateCookieName : stateCookieName.slice(0, cut);
|
|
37
|
+
return `${prefix}${FRESH_READS_COOKIE_SUFFIX}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
/** Internal response header carrying the keepClientCache() directive. */
|
|
18
41
|
export const KEEP_CACHE_HEADER = "x-rango-keep-cache";
|
|
19
42
|
|
|
@@ -82,6 +82,29 @@ export function pushHistoryWithIdx(
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The forced-MISS reload marker (`SHELL_MISS_PARAM` in rsc/shell-serve.ts,
|
|
87
|
+
* which the browser bundle must not import): a degraded PPR shell HIT
|
|
88
|
+
* reloads the page with it, and the server renders a marked request like a
|
|
89
|
+
* cache miss.
|
|
90
|
+
*/
|
|
91
|
+
export const SHELL_MISS_MARKER: string = "_rsc_shell";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Drop the forced-MISS marker from the address bar at boot, keeping the
|
|
95
|
+
* history entry's state (initBrowserApp calls it before any location read).
|
|
96
|
+
* Left in place, a refresh or a shared link would stay off the shell and the
|
|
97
|
+
* app's search params would carry it. The server strips the same marker from
|
|
98
|
+
* the request before rendering (shell-serve.ts withoutShellMissMarker), so
|
|
99
|
+
* the page was rendered for the clean URL and the hydrating client agrees.
|
|
100
|
+
*/
|
|
101
|
+
export function stripShellMissMarker(): void {
|
|
102
|
+
const url = new URL(window.location.href);
|
|
103
|
+
if (!url.searchParams.has(SHELL_MISS_MARKER)) return;
|
|
104
|
+
url.searchParams.delete(SHELL_MISS_MARKER);
|
|
105
|
+
window.history.replaceState(window.history.state, "", url.href);
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
/**
|
|
86
109
|
* Merge server-set location state into the current history entry.
|
|
87
110
|
* Replaces the current history state and dispatches notification event
|
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 },
|
|
@@ -43,7 +43,9 @@ function toScrollPayload(
|
|
|
43
43
|
return { enabled: scroll !== false ? scroll : false };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function shouldStartViewTransition(
|
|
46
|
+
export function shouldStartViewTransition(
|
|
47
|
+
segments: ResolvedSegment[],
|
|
48
|
+
): boolean {
|
|
47
49
|
let hasIntercept = false;
|
|
48
50
|
let hasTransition = false;
|
|
49
51
|
for (const s of segments) {
|
|
@@ -53,6 +55,29 @@ function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
|
|
|
53
55
|
return !hasIntercept && hasTransition;
|
|
54
56
|
}
|
|
55
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
|
+
|
|
56
81
|
/**
|
|
57
82
|
* Configuration for creating a partial updater
|
|
58
83
|
*/
|
|
@@ -141,28 +166,6 @@ export function createPartialUpdater(
|
|
|
141
166
|
return cached?.segments || [];
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
/**
|
|
145
|
-
* Transition commit. Every held commit announces the loader streams the
|
|
146
|
-
* committed tree is still receiving (loader-store.ts announcePendingStreams)
|
|
147
|
-
* so held useLoader readers pin isLoading:true until this transition lands;
|
|
148
|
-
* settled/cached segments make it a no-op. Urgent commits call onUpdate
|
|
149
|
-
* directly: nothing is held there, a reader of a pending stream suspends to
|
|
150
|
-
* its fallback.
|
|
151
|
-
*/
|
|
152
|
-
function commitInTransition(
|
|
153
|
-
segments: readonly ResolvedSegment[],
|
|
154
|
-
update: NavigationUpdate,
|
|
155
|
-
transitionTypes: readonly string[],
|
|
156
|
-
): void {
|
|
157
|
-
startTransition(() => {
|
|
158
|
-
loaderStore.announcePendingStreams(segments);
|
|
159
|
-
if (addTransitionType) {
|
|
160
|
-
for (const type of transitionTypes) addTransitionType(type);
|
|
161
|
-
}
|
|
162
|
-
onUpdate(update);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
169
|
async function fetchPartialUpdate(
|
|
167
170
|
targetUrl: string,
|
|
168
171
|
segmentIds: string[] | undefined,
|
|
@@ -374,7 +377,9 @@ export function createPartialUpdater(
|
|
|
374
377
|
};
|
|
375
378
|
|
|
376
379
|
if (shouldStartViewTransition(existingSegments)) {
|
|
377
|
-
commitInTransition(existingSegments, cachedUpdate, [
|
|
380
|
+
commitInTransition(onUpdate, existingSegments, cachedUpdate, [
|
|
381
|
+
"navigation",
|
|
382
|
+
]);
|
|
378
383
|
} else {
|
|
379
384
|
onUpdate(cachedUpdate);
|
|
380
385
|
}
|
|
@@ -614,12 +619,14 @@ export function createPartialUpdater(
|
|
|
614
619
|
};
|
|
615
620
|
if (mode.type === "action" || mode.type === "stale-revalidation") {
|
|
616
621
|
commitInTransition(
|
|
622
|
+
onUpdate,
|
|
617
623
|
reconciled.mainSegments,
|
|
618
624
|
update,
|
|
619
625
|
hasTransition ? ["action"] : [],
|
|
620
626
|
);
|
|
621
627
|
} else if (hasTransition) {
|
|
622
628
|
commitInTransition(
|
|
629
|
+
onUpdate,
|
|
623
630
|
reconciled.mainSegments,
|
|
624
631
|
update,
|
|
625
632
|
optimisticPresented
|
|
@@ -650,6 +657,7 @@ export function createPartialUpdater(
|
|
|
650
657
|
// segment reconciles in place (clientGroup key), so a read that still
|
|
651
658
|
// suspends must hold the presented content, not flash a fallback.
|
|
652
659
|
commitInTransition(
|
|
660
|
+
onUpdate,
|
|
653
661
|
reconciled.mainSegments,
|
|
654
662
|
update,
|
|
655
663
|
optimisticPresented ? [OPTIMISTIC_COMMIT_TRANSITION_TYPE] : [],
|
|
@@ -713,18 +721,20 @@ export function createPartialUpdater(
|
|
|
713
721
|
return;
|
|
714
722
|
}
|
|
715
723
|
commitInTransition(
|
|
724
|
+
onUpdate,
|
|
716
725
|
segments,
|
|
717
726
|
fullUpdate,
|
|
718
727
|
fullHasTransition ? ["action"] : [],
|
|
719
728
|
);
|
|
720
729
|
} else if (mode.type === "action") {
|
|
721
730
|
commitInTransition(
|
|
731
|
+
onUpdate,
|
|
722
732
|
segments,
|
|
723
733
|
fullUpdate,
|
|
724
734
|
fullHasTransition ? ["action"] : [],
|
|
725
735
|
);
|
|
726
736
|
} else if (fullHasTransition) {
|
|
727
|
-
commitInTransition(segments, fullUpdate, ["navigation"]);
|
|
737
|
+
commitInTransition(onUpdate, segments, fullUpdate, ["navigation"]);
|
|
728
738
|
} else {
|
|
729
739
|
onUpdate(fullUpdate);
|
|
730
740
|
}
|
|
@@ -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
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "./intercept-utils.js";
|
|
40
40
|
import { createAppShellRef } from "./app-shell.js";
|
|
41
41
|
import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
42
|
+
import { stripShellMissMarker } from "./history-state.js";
|
|
42
43
|
import { setActiveInterceptTargets } from "../client-urls/navigation.js";
|
|
43
44
|
|
|
44
45
|
// Vite HMR types are provided by vite/client
|
|
@@ -169,6 +170,10 @@ export async function initBrowserApp(
|
|
|
169
170
|
} = options;
|
|
170
171
|
|
|
171
172
|
bootLog("initBrowserApp start");
|
|
173
|
+
// Before any location read (the history key, the navigation store, the
|
|
174
|
+
// event controller): the server rendered the marked request for the clean
|
|
175
|
+
// URL, so the client starts from the same one and hydration agrees.
|
|
176
|
+
stripShellMissMarker();
|
|
172
177
|
bootLog("flight decode: awaiting initial payload from document stream");
|
|
173
178
|
const initialPayload =
|
|
174
179
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
@@ -655,7 +660,7 @@ export interface RangoProps {}
|
|
|
655
660
|
*
|
|
656
661
|
* @example
|
|
657
662
|
* ```tsx
|
|
658
|
-
* import { initBrowserApp, Rango } from "
|
|
663
|
+
* import { initBrowserApp, Rango } from "@rangojs/router/browser";
|
|
659
664
|
* import { rscStream } from "rsc-html-stream/client";
|
|
660
665
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
661
666
|
*
|
|
@@ -72,6 +72,13 @@ type GetScrollKeyFunction = (location: {
|
|
|
72
72
|
|
|
73
73
|
let customGetKey: GetScrollKeyFunction | null = null;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Scroll key of the entry on screen. At popstate history.state already belongs
|
|
77
|
+
* to the destination, so this is the only way to save the page being left
|
|
78
|
+
* under its own key (see handleTraversalStart).
|
|
79
|
+
*/
|
|
80
|
+
let displayedKey: string | null = null;
|
|
81
|
+
|
|
75
82
|
/**
|
|
76
83
|
* Generate a unique key for the current history entry.
|
|
77
84
|
* Uses history.state.key if available, otherwise generates and stores a new one.
|
|
@@ -138,7 +145,7 @@ export function initScrollRestoration(options?: {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
// Ensure current history entry has a key
|
|
141
|
-
|
|
148
|
+
trackDisplayedEntry();
|
|
142
149
|
|
|
143
150
|
// Save scroll positions on pagehide (before leaving/refreshing)
|
|
144
151
|
const handlePageHide = () => {
|
|
@@ -148,7 +155,14 @@ export function initScrollRestoration(options?: {
|
|
|
148
155
|
window.history.scrollRestoration = "auto";
|
|
149
156
|
};
|
|
150
157
|
|
|
158
|
+
// A bfcache restore keeps the "auto" set on pagehide; take scroll back.
|
|
159
|
+
// Only here: without <Html.ScrollRestoration> the browser owns scroll.
|
|
160
|
+
const handlePageShow = (event: PageTransitionEvent) => {
|
|
161
|
+
if (event.persisted) window.history.scrollRestoration = "manual";
|
|
162
|
+
};
|
|
163
|
+
|
|
151
164
|
window.addEventListener("pagehide", handlePageHide);
|
|
165
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
152
166
|
|
|
153
167
|
if (IS_BROWSER_DEBUG) {
|
|
154
168
|
debugLog(
|
|
@@ -160,10 +174,12 @@ export function initScrollRestoration(options?: {
|
|
|
160
174
|
return () => {
|
|
161
175
|
cancelScrollRestorationPolling();
|
|
162
176
|
window.removeEventListener("pagehide", handlePageHide);
|
|
177
|
+
window.removeEventListener("pageshow", handlePageShow);
|
|
163
178
|
window.history.scrollRestoration = "auto";
|
|
164
179
|
initialized = false;
|
|
165
180
|
savedScrollPositions = {};
|
|
166
181
|
scrollKeyOrder = [];
|
|
182
|
+
displayedKey = null;
|
|
167
183
|
};
|
|
168
184
|
}
|
|
169
185
|
|
|
@@ -172,8 +188,10 @@ export function initScrollRestoration(options?: {
|
|
|
172
188
|
* Maintains bounded size by evicting oldest entries when the limit is exceeded.
|
|
173
189
|
*/
|
|
174
190
|
export function saveCurrentScrollPosition(): void {
|
|
175
|
-
|
|
191
|
+
saveScrollPosition(getScrollKey(), window.scrollY);
|
|
192
|
+
}
|
|
176
193
|
|
|
194
|
+
function saveScrollPosition(key: string, y: number): void {
|
|
177
195
|
// If this key already exists, remove it from its current position
|
|
178
196
|
// in the order array so it can be re-appended at the end (most recent).
|
|
179
197
|
const existingIndex = scrollKeyOrder.indexOf(key);
|
|
@@ -181,7 +199,7 @@ export function saveCurrentScrollPosition(): void {
|
|
|
181
199
|
scrollKeyOrder.splice(existingIndex, 1);
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
savedScrollPositions[key] =
|
|
202
|
+
savedScrollPositions[key] = y;
|
|
185
203
|
scrollKeyOrder.push(key);
|
|
186
204
|
|
|
187
205
|
// Evict oldest entries if we exceed the limit
|
|
@@ -354,6 +372,23 @@ export function handleNavigationStart(): void {
|
|
|
354
372
|
saveCurrentScrollPosition();
|
|
355
373
|
}
|
|
356
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Handle scroll for a back/forward traversal, at popstate before anything
|
|
377
|
+
* renders: save the page being left under ITS key (history has already moved,
|
|
378
|
+
* so saveCurrentScrollPosition would use the destination's key) and track the
|
|
379
|
+
* destination as the displayed entry.
|
|
380
|
+
*/
|
|
381
|
+
export function handleTraversalStart(): void {
|
|
382
|
+
if (!initialized) return;
|
|
383
|
+
const leftKey = displayedKey;
|
|
384
|
+
trackDisplayedEntry();
|
|
385
|
+
// A key shared with the destination (a getKey that collapses entries, e.g.
|
|
386
|
+
// by pathname) keeps the destination's saved position.
|
|
387
|
+
if (leftKey !== null && leftKey !== displayedKey) {
|
|
388
|
+
saveScrollPosition(leftKey, window.scrollY);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
357
392
|
/**
|
|
358
393
|
* Handle scroll after navigation completes.
|
|
359
394
|
* @param options.restore - If true, restore saved position (for popstate)
|
|
@@ -376,10 +411,17 @@ export function handleNavigationEnd(options: {
|
|
|
376
411
|
// But basic scroll-to-top and hash scrolling work without it — this
|
|
377
412
|
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
378
413
|
// and remounts, creating a brief window where initialized is false.
|
|
379
|
-
if (restore
|
|
380
|
-
if (
|
|
414
|
+
if (restore) {
|
|
415
|
+
if (
|
|
416
|
+
initialized &&
|
|
417
|
+
restoreScrollPosition({ retryIfStreaming: true, isStreaming })
|
|
418
|
+
) {
|
|
381
419
|
return;
|
|
382
420
|
}
|
|
421
|
+
// An "auto" entry (no <Html.ScrollRestoration>, or created before it mounted)
|
|
422
|
+
// is restored by the browser. A scrollTo here races that restore, and
|
|
423
|
+
// when it runs second the page lands at the top.
|
|
424
|
+
if (window.history.scrollRestoration === "auto") return;
|
|
383
425
|
// Fall through to hash or top if no saved position
|
|
384
426
|
}
|
|
385
427
|
|
|
@@ -410,5 +452,10 @@ export function handleNavigationEnd(options: {
|
|
|
410
452
|
* Call this after changing history to ensure new entry has a key.
|
|
411
453
|
*/
|
|
412
454
|
export function ensureHistoryKey(): void {
|
|
413
|
-
|
|
455
|
+
trackDisplayedEntry();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Mints the current entry's key if missing and records it as displayed. */
|
|
459
|
+
function trackDisplayedEntry(): void {
|
|
460
|
+
displayedKey = getScrollKey();
|
|
414
461
|
}
|