@rangojs/router 0.16.0 → 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 +7 -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/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/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 +1381 -1346
- package/package.json +2 -2
- 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 +23 -3
- 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 +35 -25
- 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 +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- 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 +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/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/ThemeScript.tsx +3 -3
- 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 +12 -3
- 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. 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,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 },
|
|
@@ -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
|
|
@@ -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
|
*
|
|
@@ -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
|
}
|
|
@@ -26,19 +26,52 @@
|
|
|
26
26
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
27
|
import { _setCacheExecScopeProbe } from "./taint.js";
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* One "use cache" execution. Identity matters: the execution's handle capture
|
|
31
|
+
* (cache-runtime.ts) records only pushes from its own chain, including cached
|
|
32
|
+
* functions it calls (`parent` links them), not concurrent pushes into the
|
|
33
|
+
* same request store.
|
|
34
|
+
*/
|
|
35
|
+
export interface CacheExecScope {
|
|
36
|
+
readonly parent?: CacheExecScope;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* Run fn with the "use cache" execution scope active. Continuations spawned
|
|
33
43
|
* from fn's synchronous kickoff inherit the scope; parallel chains do not.
|
|
44
|
+
* Pass `scope` (createCacheExecScope) to test membership with
|
|
45
|
+
* isInCacheExecChain.
|
|
34
46
|
*/
|
|
35
|
-
export function runWithCacheExecScope<T>(
|
|
36
|
-
|
|
47
|
+
export function runWithCacheExecScope<T>(
|
|
48
|
+
fn: () => T,
|
|
49
|
+
scope: CacheExecScope = createCacheExecScope(),
|
|
50
|
+
): T {
|
|
51
|
+
return cacheExecStorage.run(scope, fn);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A scope nested in the calling chain's current one, if any. */
|
|
55
|
+
export function createCacheExecScope(): CacheExecScope {
|
|
56
|
+
return { parent: cacheExecStorage.getStore() };
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
/** True when the calling async chain is inside a "use cache" body. */
|
|
40
60
|
export function isInsideCacheExecScope(): boolean {
|
|
41
|
-
return cacheExecStorage.getStore()
|
|
61
|
+
return cacheExecStorage.getStore() !== undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The calling chain's innermost "use cache" scope, if any. */
|
|
65
|
+
export function getCacheExecScope(): CacheExecScope | undefined {
|
|
66
|
+
return cacheExecStorage.getStore();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when the calling async chain runs inside `scope`, at any depth. */
|
|
70
|
+
export function isInCacheExecChain(scope: CacheExecScope): boolean {
|
|
71
|
+
for (let s = cacheExecStorage.getStore(); s; s = s.parent) {
|
|
72
|
+
if (s === scope) return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
// Wire the ambient ctx-method guards (assertNotInsideCacheExec in taint.ts)
|
|
@@ -20,12 +20,7 @@ import type { SearchParamsFilter } from "./search-params-filter.js";
|
|
|
20
20
|
* slot; an allowlist keeps the router's own params out of the key while leaving
|
|
21
21
|
* consumer `__` params intact.
|
|
22
22
|
*/
|
|
23
|
-
const RESERVED_SEARCH_PARAMS = new Set([
|
|
24
|
-
"__no_cache",
|
|
25
|
-
"__rsc",
|
|
26
|
-
"__html",
|
|
27
|
-
"__prerender_collect",
|
|
28
|
-
]);
|
|
23
|
+
const RESERVED_SEARCH_PARAMS = new Set(["__no_cache", "__rsc", "__html"]);
|
|
29
24
|
|
|
30
25
|
function isReservedSearchParam(key: string): boolean {
|
|
31
26
|
return key.startsWith("_rsc") || RESERVED_SEARCH_PARAMS.has(key);
|