@rangojs/router 0.17.0 → 0.19.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- 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 +224 -72
- 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 +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- 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 +1094 -376
- 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 +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -64,11 +64,16 @@ Low-level building blocks (all from `@rangojs/router/testing`):
|
|
|
64
64
|
|
|
65
65
|
```ts
|
|
66
66
|
import { it, expect } from "vitest";
|
|
67
|
-
import {
|
|
68
|
-
|
|
67
|
+
import {
|
|
68
|
+
runInRequestContext,
|
|
69
|
+
withLocationStateKey,
|
|
70
|
+
} from "@rangojs/router/testing";
|
|
71
|
+
import { loginAction } from "../src/actions/login"; // sets a session cookie, then throw redirect("/app", { state: [Flash({ text: "Welcome back" })] })
|
|
69
72
|
import { logoutAction, dismissBannerAction } from "../src/actions/session"; // invalidateClientCache() / keepClientCache()
|
|
73
|
+
import { Flash } from "../src/location-states"; // createLocationState<{ text: string }>({ flash: true })
|
|
70
74
|
|
|
71
75
|
const env = { DB: fakeDb }; // your binding double (see ./bindings.md)
|
|
76
|
+
withLocationStateKey(Flash, "Flash"); // no Vite plugin here: key -> "__rsc_ls_Flash"
|
|
72
77
|
|
|
73
78
|
it("sets the session cookie + flash and redirects", async () => {
|
|
74
79
|
const { thrown, cookies, locationState } = await runInRequestContext(
|
|
@@ -82,7 +87,7 @@ it("sets the session cookie + flash and redirects", async () => {
|
|
|
82
87
|
);
|
|
83
88
|
expect((thrown as Response).headers.get("Location")).toBe("/app"); // redirected
|
|
84
89
|
expect(cookies.session).toBeDefined(); // cookie set before the throw, no @internal cast
|
|
85
|
-
expect(locationState).toEqual({
|
|
90
|
+
expect(locationState).toEqual({ __rsc_ls_Flash: { text: "Welcome back" } });
|
|
86
91
|
});
|
|
87
92
|
|
|
88
93
|
it("asserts the client-cache directives an action issued", async () => {
|
|
@@ -105,6 +110,7 @@ it("asserts the client-cache directives an action issued", async () => {
|
|
|
105
110
|
## Caveats
|
|
106
111
|
|
|
107
112
|
- The snapshot fires whether `fn` RETURNS or THROWS. A `throw redirect("/app")` on the success path is captured on `thrown` (NOT re-thrown), so no try/catch is needed; assert on `thrown` for a throwing action.
|
|
113
|
+
- Location state needs a key. A `createLocationState()` definition gets it from the Vite plugin, which a unit-test project does not run, and outside production an unkeyed `Flash(value)` throws before `redirect()` runs: `thrown` is that missing-key error, not the redirect. Call `withLocationStateKey(Flash, "Flash")` once per definition; `locationState` then holds `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key).
|
|
108
114
|
- There is no cookies / headers option. Seed a request cookie by passing a full `Request` with the `Cookie` header (as in the recipe).
|
|
109
115
|
- An action has no loader context, so `runLoader` is the wrong shape for it even when the action reads cookies or vars; use `runInRequestContext`.
|
|
110
116
|
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
package/skills/theme/SKILL.md
CHANGED
|
@@ -86,10 +86,13 @@ from `@rangojs/router/theme` in `<head>` instead. It takes the resolved config
|
|
|
86
86
|
are rejected with a console warning. Both are typed optional because they only
|
|
87
87
|
exist when `theme` is configured.
|
|
88
88
|
|
|
89
|
+
`ctx.theme` is the visitor's theme cookie, so a handler reads it only on a
|
|
90
|
+
route whose output is rendered per request:
|
|
91
|
+
|
|
89
92
|
```typescript
|
|
90
93
|
import type { Middleware } from "@rangojs/router";
|
|
91
94
|
|
|
92
|
-
// In a handler
|
|
95
|
+
// In a handler on a route without ppr or cache()
|
|
93
96
|
path("/settings", (ctx) => {
|
|
94
97
|
const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
|
|
95
98
|
return <SettingsPage theme={currentTheme} />;
|
|
@@ -105,9 +108,45 @@ export const themeFromQuery: Middleware = async (ctx, next) => {
|
|
|
105
108
|
};
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
### On `ppr` and `cache()` routes
|
|
112
|
+
|
|
113
|
+
A `ppr` shell and a `cache()` entry are shared by every visitor, so a handler
|
|
114
|
+
there must not read the visitor's theme. `ctx.theme` (handler and middleware)
|
|
115
|
+
and `getRequestContext().theme` are guarded like `cookies()`:
|
|
116
|
+
|
|
117
|
+
| Where the theme is read | What happens |
|
|
118
|
+
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| a handler on a `ppr` route | The shell capture is refused (warned once per URL); every request stays a MISS |
|
|
120
|
+
| inside a `cache()` boundary | Throws on a cache miss, like `cookies()` |
|
|
121
|
+
| inside a `"use cache"` function (handler or middleware) | Throws; read it outside and pass it in as an argument |
|
|
122
|
+
| after `ctx.dynamic()` on `ppr` | Allowed: that render is never captured |
|
|
123
|
+
| middleware `ctx.set()` of the theme, read by a handler | Not guarded: the first visitor's theme bakes into the shell or `cache()` entry, like a session object |
|
|
124
|
+
| a route with no `ppr` or `cache()` above it, or middleware | Allowed: rendered per request |
|
|
125
|
+
| a loader with its own `cache()` and no `key()` | The miss fails and stores nothing (#972), like a `cookies()` read there; key the theme |
|
|
126
|
+
| a handler on a document-cached route (`s-maxage`) | Not guarded: the stored document shows that visitor's theme to everyone |
|
|
127
|
+
|
|
128
|
+
The theme getters are read-only and non-enumerable: `{ ...ctx }` and
|
|
129
|
+
`Object.assign({}, ctx)` do not carry `theme`, and assigning it throws.
|
|
130
|
+
|
|
131
|
+
`ctx.setTheme()` throws in a handler on these routes too, like any response
|
|
132
|
+
write there; call it from middleware.
|
|
133
|
+
|
|
134
|
+
Read the theme where it is per request instead:
|
|
135
|
+
|
|
136
|
+
- `useTheme()` in a client component. The `<html>` class is right before
|
|
137
|
+
paint (the theme script sets it). A render that other visitors may be
|
|
138
|
+
served starts `useTheme()` at the no-cookie default (`defaultTheme`),
|
|
139
|
+
whoever rendered it: a `ppr` shell, and a page whose response opted in to
|
|
140
|
+
the document cache before `next()` (#978, see `/document-cache`). A
|
|
141
|
+
visitor with no stored theme sees the default, and one with a stored theme
|
|
142
|
+
sees it after hydration. Any other render, a 404 included, starts at the
|
|
143
|
+
visitor's own theme.
|
|
144
|
+
- A live loader (no `ssr: false`). Loaders do not get `ctx.theme` or
|
|
145
|
+
`ctx.setTheme`; read the cookie directly. A live loader runs on every
|
|
146
|
+
request, HITs included, and on a `ppr` route needs `loading()` or an inline
|
|
147
|
+
`<Suspense>` above its reader. A bake-lane loader (`ssr: false`) runs at
|
|
148
|
+
capture, where this `cookies()` read refuses the capture, and so does a
|
|
149
|
+
loader a handler awaits with `ctx.use()`.
|
|
111
150
|
|
|
112
151
|
```typescript
|
|
113
152
|
import { cookies, createLoader } from "@rangojs/router";
|
|
@@ -118,6 +157,19 @@ export const ThemedLoader = createLoader(async () => {
|
|
|
118
157
|
});
|
|
119
158
|
```
|
|
120
159
|
|
|
160
|
+
If you bind it with its own `cache()`, the entry is shared across users, so the
|
|
161
|
+
miss fails unless it has a `key()`. Any `key()` switches that check off, so
|
|
162
|
+
make it include the theme:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
loader(ThemedLoader, () => [
|
|
166
|
+
cache({
|
|
167
|
+
ttl: 300,
|
|
168
|
+
key: () => `themed:${cookies().get("theme")?.value ?? "system"}`,
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
```
|
|
172
|
+
|
|
121
173
|
## Client
|
|
122
174
|
|
|
123
175
|
```tsx
|
|
@@ -103,7 +103,8 @@ interface PaginationData {
|
|
|
103
103
|
}
|
|
104
104
|
export const Pagination = createVar<PaginationData>();
|
|
105
105
|
|
|
106
|
-
// Non-cacheable var — reading inside cache()
|
|
106
|
+
// Non-cacheable var — reading inside cache(), "use cache", or a PPR shell
|
|
107
|
+
// capture throws at runtime
|
|
107
108
|
interface SessionData {
|
|
108
109
|
userId: string;
|
|
109
110
|
}
|
|
@@ -115,7 +116,14 @@ export const Session = createVar<SessionData>({ cache: false });
|
|
|
115
116
|
Write-level escalation is also supported: `ctx.set(Var, value, { cache: false })`
|
|
116
117
|
marks a specific write as non-cacheable even if the var itself is cacheable.
|
|
117
118
|
"Least cacheable wins" — if either says `cache: false`, the value throws on
|
|
118
|
-
read inside `cache()` or `"use cache"`.
|
|
119
|
+
read inside `cache()` or `"use cache"`. On a `ppr` route the read also throws
|
|
120
|
+
anywhere the shell capture waits for it (a handler, a promise it passes or
|
|
121
|
+
pushes, an async server component, an `ssr: false` loader, a loader a handler
|
|
122
|
+
awaits) and refuses the capture: the route keeps rendering without a shell,
|
|
123
|
+
and a warning fires once per key. A loader without `ssr: false` that no
|
|
124
|
+
handler awaits may read it, unless it is bound with its own `cache()` and no
|
|
125
|
+
`key()`: that miss fails, since the entry is shared (`/loader` → "Cache
|
|
126
|
+
Key").
|
|
119
127
|
|
|
120
128
|
### Producer (handler, layout, or middleware)
|
|
121
129
|
|
|
@@ -331,3 +339,11 @@ the value at click time; `ctx.setLocationState(entry)` attaches state from a
|
|
|
331
339
|
handler; `redirect(url, { state })` carries it through a redirect; and
|
|
332
340
|
`ProductPreview.read()` / `.write(value)` / `.delete()` access the current
|
|
333
341
|
history entry outside React (client-only, non-reactive).
|
|
342
|
+
`ProductPreview.read(location)` reads the slot from any `{ state }` snapshot,
|
|
343
|
+
such as a `transition({ when })` context's `from` / `to`, and works without
|
|
344
|
+
`window`.
|
|
345
|
+
|
|
346
|
+
The `state` option on `<Link>`, `router.push()` and `router.replace()` is typed
|
|
347
|
+
`HistoryState`: an array of called entries (`[ProductPreview({ ... })]`) or
|
|
348
|
+
plain structured-clone data. A bare entry (`state: ProductPreview({ ... })`) or
|
|
349
|
+
an uncalled definition (`state: [ProductPreview]`) is a compile error.
|
|
@@ -142,6 +142,11 @@ Different functions always produce different cache keys, even for the same route
|
|
|
142
142
|
This is important for intercepted routes -- the path handler and intercept handler
|
|
143
143
|
each have their own `functionId` and therefore their own cache entries.
|
|
144
144
|
|
|
145
|
+
The key is only what the function declares: its id and arguments. A call made
|
|
146
|
+
inside a route `cache()` with a `key()` does not inherit that key, so a value
|
|
147
|
+
the route partitions by (a tier header, a session) must be passed in as an
|
|
148
|
+
argument. The same holds for a loader's own `cache()` (`/loader` → "Cache Key").
|
|
149
|
+
|
|
145
150
|
### Route context is folded into the key
|
|
146
151
|
|
|
147
152
|
The tainted `ctx` object is excluded from arg serialization (see below), but
|
|
@@ -224,13 +229,32 @@ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
|
|
|
224
229
|
per-request values (cookies, headers) are not reflected in the cache key. Without
|
|
225
230
|
this guard, one user's data would be served to another.
|
|
226
231
|
|
|
232
|
+
The raw reads throw the same way: `ctx.request.headers` on any `ctx` passed in
|
|
233
|
+
(handler, loader, middleware; it is one `Request` object, also
|
|
234
|
+
`getRequestContext().request`), and `getRequestContext().cookie()` /
|
|
235
|
+
`.cookies()`. A `ctx` or a `Request` argument keys the entry by its route and
|
|
236
|
+
URL only, never by its headers.
|
|
237
|
+
|
|
227
238
|
Extract the value before the cached function and pass it as an argument:
|
|
228
239
|
|
|
229
240
|
```typescript
|
|
230
241
|
const locale = cookies().get("locale")?.value ?? "en";
|
|
231
242
|
const data = await getCachedData(locale); // locale is now in the cache key
|
|
243
|
+
|
|
244
|
+
// the same for a header
|
|
245
|
+
const language = ctx.request.headers.get("accept-language") ?? "en";
|
|
246
|
+
const greeting = await getGreeting(language);
|
|
232
247
|
```
|
|
233
248
|
|
|
249
|
+
Building the key from a `Request` argument reads only its URL.
|
|
250
|
+
`ctx.request.clone()` is guarded like `ctx.request.headers`. One gap remains:
|
|
251
|
+
`fetch(ctx.request)` and `new Request(ctx.request)` don't throw, and the guard
|
|
252
|
+
can't see through them. The fetch forwards the visitor's `Cookie` and
|
|
253
|
+
`Authorization`, so its response is per visitor, and the copy's headers are
|
|
254
|
+
the visitor's; either one stored in the entry serves one visitor's data to
|
|
255
|
+
the next. Don't make them in a cached body: fetch outside and pass the
|
|
256
|
+
result in, or pass in the values the fetch depends on.
|
|
257
|
+
|
|
234
258
|
`ctx.get()` of a **non-cacheable variable** (`createVar({ cache: false })`, or
|
|
235
259
|
a value written with `ctx.set(key, value, { cache: false })`) throws the same
|
|
236
260
|
way, whether it goes through `getRequestContext().get()` or a `ctx` passed in
|
|
@@ -254,14 +278,20 @@ Ordinary (cacheable) variables stay readable. Calling the cached function from
|
|
|
254
278
|
a loader does not exempt it: the loader re-runs on every request, but the
|
|
255
279
|
cached body does not.
|
|
256
280
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
async chain, loaders included.
|
|
263
|
-
|
|
264
|
-
|
|
281
|
+
A LOADER body consumed inside the cached function (`await ctx.use(loader)`)
|
|
282
|
+
is part of that body: its value is captured into the shared cache entry like
|
|
283
|
+
any other computed data, so a `{ cache: false }` variable read there throws,
|
|
284
|
+
exactly as `cookies()`, `headers()`, `ctx.request.headers` and a theme read do
|
|
285
|
+
(one guard follows the
|
|
286
|
+
cached body's whole async chain, loaders included). The check runs only when
|
|
287
|
+
the loader body starts inside the cached function: a loader something else in
|
|
288
|
+
the request already started (a handler's `ctx.use()`, a route `loader()`)
|
|
289
|
+
returns its memoized value without it, and that value is still stored in the
|
|
290
|
+
entry. Read the value outside and pass it in as an argument. Handler/cached-scope consumption = baked copy,
|
|
291
|
+
client-side `useLoader` = live (the consumption-lane rule, `/rango` →
|
|
292
|
+
Invariants). Under a route `cache()` a loader body's reads stay allowed (the
|
|
293
|
+
route entry never stores a loader's value); during a PPR capture they refuse
|
|
294
|
+
the capture, a handler-consumed loader's included.
|
|
265
295
|
|
|
266
296
|
### Side-Effect Guards
|
|
267
297
|
|
|
@@ -504,20 +534,55 @@ built into the server bundle, see `/prerender`.)
|
|
|
504
534
|
Entries are tagged by the profile's `tags` plus any runtime `cacheTag(...tags)`
|
|
505
535
|
calls in the function body. `cacheTag` has two forms: inside a `"use cache"`
|
|
506
536
|
function it tags that entry; called during a request render outside
|
|
507
|
-
`"use cache"` it tags the
|
|
508
|
-
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
537
|
+
`"use cache"` it tags what the render stores: the enclosing route `cache()`
|
|
538
|
+
entry, a cached loader's own entry when called in its body, and the `ppr`
|
|
539
|
+
shell or document-cache entry built from them (on a `ppr` route, only when
|
|
540
|
+
shell material calls it). See `/caching` → "Tag-Based Invalidation". All
|
|
541
|
+
built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`,
|
|
542
|
+
`VercelCacheStore`) index by tag. Invalidate on demand with
|
|
543
|
+
`updateTag(...tags)` (awaitable; for server actions) or
|
|
544
|
+
`revalidateTag(...tags)` (background, non-blocking; for route
|
|
545
|
+
handlers/webhooks). Both evict rather than mark stale, and the request that
|
|
546
|
+
calls either reads its own writes; `updateTag()` also waits for the durable
|
|
547
|
+
write. PPR shell reads on `CFCacheStore`/`VercelCacheStore` go through
|
|
548
|
+
per-isolate memos, which the invalidating user skips via the fresh-reads
|
|
549
|
+
cookie. For `CFCacheStore`, cross-colo invalidation needs a `kv` namespace
|
|
550
|
+
(markers live in that same namespace) or `tagPurge`. The separate
|
|
551
|
+
`revalidate()` export is the client-update control (which segments re-render
|
|
552
|
+
on a navigation or action), not a cache bust.
|
|
553
|
+
|
|
554
|
+
A route `cache()` entry stores the tags of the `"use cache"` reads inside it,
|
|
555
|
+
so `updateTag()` of one evicts the enclosing entry and any `ppr` shell or
|
|
556
|
+
document built from it. A loader's own `cache()` entry does the same for the
|
|
557
|
+
reads in its body, and a `"use cache"` function for the `"use cache"`
|
|
558
|
+
functions it calls: the outer entry bakes the inner value, so it carries the
|
|
559
|
+
inner tags, whether the inner call ran, was a hit, or joined another call's execution. That includes calls made
|
|
560
|
+
by the server components a cached component returns, which render when the
|
|
561
|
+
value is stored.
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
async function getStock(sku: string) {
|
|
565
|
+
"use cache";
|
|
566
|
+
cacheTag("stock");
|
|
567
|
+
return db.stock(sku);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function getProductCard(sku: string) {
|
|
571
|
+
"use cache";
|
|
572
|
+
return { sku, stock: await getStock(sku) };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// getProductCard's entry is tagged "stock": updateTag("stock") evicts both.
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
A call that started before `updateTag()`/`revalidateTag()` of one of its tags
|
|
579
|
+
returns what it read but does not store it, in any request: written after the
|
|
580
|
+
invalidation, the old value would be served as newer than it. The next call
|
|
581
|
+
runs the function again. The same holds for a stale entry's background
|
|
582
|
+
refresh, a loader's own `cache()`, route `cache()` entries and the document
|
|
583
|
+
cache. Another isolate's invalidation is caught through the store's markers:
|
|
584
|
+
`CFCacheStore` with KV, `VercelCacheStore`. On a KV-less `CFCacheStore` only
|
|
585
|
+
this isolate's invalidations are caught; ttl+swr bounds the rest.
|
|
521
586
|
|
|
522
587
|
## Interaction with Other Caching
|
|
523
588
|
|
|
@@ -531,7 +596,10 @@ boundary, not before (see `/cache-guide` → "Nesting rule").
|
|
|
531
596
|
| `Prerender()` | Route segment tree | Build-time | Pre-render known params, optional live fallback |
|
|
532
597
|
|
|
533
598
|
Inside a `ppr` shell, a `"use cache"` value that renders as shell material is
|
|
534
|
-
pinned at capture time for the life of that shell (see `/ppr` → Pitfalls).
|
|
599
|
+
pinned at capture time for the life of that shell (see `/ppr` → Pitfalls). A
|
|
600
|
+
hole's own read (in a loader without `ssr: false` under `loading()` or an
|
|
601
|
+
inline `<Suspense>`) always returns the store's current entry, never the
|
|
602
|
+
shell's copy, even when a loader the capture ran read the same entry.
|
|
535
603
|
|
|
536
604
|
## Dev Mode and tests
|
|
537
605
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -89,12 +89,24 @@ import { getCache, waitUntil } from "@vercel/functions";
|
|
|
89
89
|
|
|
90
90
|
const defaults = { ttl: 60, swr: 300 };
|
|
91
91
|
const memoryStore = new MemorySegmentCacheStore({ defaults });
|
|
92
|
+
// One handle per process: getCache() resolves the platform cache on every
|
|
93
|
+
// call, and VercelCacheStore keeps its PPR shell and tag-marker memos per
|
|
94
|
+
// handle. After updateTag(), the same process misses at once, the rest of the
|
|
95
|
+
// region once its marker memo refreshes (memo.markerMaxStaleMs, default 2000),
|
|
96
|
+
// and the mutating user at once (the fresh-reads cookie skips the memos).
|
|
97
|
+
// Another region serves a memoized shell until memo.shellMs (default 2000)
|
|
98
|
+
// passes, since the tag markers are regional and only expireTag is global. A
|
|
99
|
+
// platform expireTag issued outside rango writes no marker, so every memo
|
|
100
|
+
// serves the shell until its window passes.
|
|
101
|
+
const runtimeCache = process.env.VERCEL
|
|
102
|
+
? getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })
|
|
103
|
+
: undefined;
|
|
92
104
|
|
|
93
105
|
function resolveCache() {
|
|
94
|
-
if (
|
|
106
|
+
if (runtimeCache) {
|
|
95
107
|
return {
|
|
96
108
|
store: new VercelCacheStore({
|
|
97
|
-
cache:
|
|
109
|
+
cache: runtimeCache,
|
|
98
110
|
waitUntil,
|
|
99
111
|
defaults,
|
|
100
112
|
}),
|
|
@@ -227,8 +227,8 @@ interface TransitionConfig {
|
|
|
227
227
|
default?: string | Record<string, string>; // fallback for any phase
|
|
228
228
|
name?: string; // explicit view-transition-name
|
|
229
229
|
viewTransition?: "auto" | false; // boundary opt-out (see below)
|
|
230
|
-
//
|
|
231
|
-
//
|
|
230
|
+
// Browser predicate, once per navigation: false commits it urgently with no
|
|
231
|
+
// view transition. See "Conditional transitions".
|
|
232
232
|
when?: (ctx: TransitionWhenContext) => boolean;
|
|
233
233
|
}
|
|
234
234
|
```
|
|
@@ -240,47 +240,162 @@ interface TransitionConfig {
|
|
|
240
240
|
|
|
241
241
|
## Conditional transitions (`when`)
|
|
242
242
|
|
|
243
|
-
`transition({ when })`
|
|
243
|
+
`transition({ when })` decides, per navigation, whether the navigation holds.
|
|
244
|
+
It is a **browser** predicate: it runs once per navigation, at the first
|
|
245
|
+
commit that presents the destination, and never on the server, on the
|
|
246
|
+
document load, or on progressive-enhancement paths. Return `false` and the
|
|
247
|
+
navigation commits urgently: the `loading()` skeleton streams and no view
|
|
248
|
+
transition runs.
|
|
249
|
+
|
|
250
|
+
Write it inline in `urls()`. The Vite plugin hoists the literal into a
|
|
251
|
+
`"use client"` module, and the route carries it to the browser as a client
|
|
252
|
+
reference. The hoist applies to the router's `transition`: the `urls()`
|
|
253
|
+
helper (renamed or not) or `transition` imported from `@rangojs/router`,
|
|
254
|
+
with the function written directly as the `when` value (an arrow, a
|
|
255
|
+
`function`, or a `when() {}` method; `as` / `satisfies` wrappers are
|
|
256
|
+
fine). A config built elsewhere, such as `transition(cfg)`, is not hoisted:
|
|
257
|
+
export that predicate from a `"use client"` module instead.
|
|
244
258
|
|
|
245
|
-
|
|
259
|
+
```ts
|
|
260
|
+
// Hold unless the navigation leaves the "b" tab (the navigation SOURCE):
|
|
261
|
+
transition({ when: ({ from }) => from.params.tab !== "b" });
|
|
262
|
+
|
|
263
|
+
// Hold only for forward navigations inside /list:
|
|
264
|
+
transition({
|
|
265
|
+
when: ({ kind, to }) => kind !== "pop" && to.url.pathname.startsWith("/list"),
|
|
266
|
+
});
|
|
246
267
|
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
// Hold only after a specific action revalidated the route:
|
|
269
|
+
transition({ when: ({ isAction }) => isAction(addToCart) });
|
|
270
|
+
```
|
|
249
271
|
|
|
250
|
-
|
|
272
|
+
For a predicate you share between routes or unit-test, export it from a
|
|
273
|
+
`"use client"` module and import it into the urls file:
|
|
251
274
|
|
|
252
275
|
```ts
|
|
276
|
+
// src/transitions.ts
|
|
277
|
+
"use client";
|
|
253
278
|
import type { TransitionWhenContext } from "@rangojs/router";
|
|
279
|
+
import { Slide } from "./location-states.js";
|
|
254
280
|
|
|
255
|
-
|
|
256
|
-
|
|
281
|
+
export function slideWhen({ to }: TransitionWhenContext): boolean {
|
|
282
|
+
return Slide.read(to)?.animate !== false;
|
|
283
|
+
}
|
|
257
284
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
})
|
|
262
|
-
|
|
285
|
+
// src/urls.tsx
|
|
286
|
+
import { slideWhen } from "./transitions.js";
|
|
287
|
+
path("/photos/:id", PhotoPage, { name: "photo" }, () => [
|
|
288
|
+
transition({ default: "slide", when: slideWhen }),
|
|
289
|
+
]);
|
|
263
290
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
when: ({ actionId }) => actionId === "src/actions/cart.ts#addToCart",
|
|
267
|
-
});
|
|
291
|
+
// A Link that opts this navigation out through location state:
|
|
292
|
+
<Link to="/photos/2" state={[Slide({ animate: false })]}>Next</Link>;
|
|
268
293
|
```
|
|
269
294
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
295
|
+
Predicate modules (a `"use client"` export, or a hoisted inline literal) load
|
|
296
|
+
with the client entry chunk, because the router must be able to call them at
|
|
297
|
+
the first navigation. Keep them small: type imports and location-state
|
|
298
|
+
definitions only, and nothing that touches `window` at module scope (the
|
|
299
|
+
module is also evaluated during SSR).
|
|
300
|
+
|
|
301
|
+
The rule is "a predicate runs where its effect lands": the hold is a browser
|
|
302
|
+
commit decision, so the predicate runs in the browser. An inline literal may
|
|
303
|
+
reference module-level imports only when they are client-safe (they are
|
|
304
|
+
re-emitted into the hoisted module); a free server binding (a `const` in the
|
|
305
|
+
urls file, a handler variable, a component used as JSX) or a server-only
|
|
306
|
+
import is a build error that names it, and so are `import()`, `import.meta`
|
|
307
|
+
other than `import.meta.env`, `this` / `arguments` of the enclosing
|
|
308
|
+
function, and a `get when()` accessor. A server function passed by name, or a non-function, fails dev
|
|
309
|
+
startup, the build and HMR re-discovery with the route name and pattern.
|
|
278
310
|
|
|
279
|
-
|
|
311
|
+
### Context
|
|
280
312
|
|
|
281
|
-
|
|
313
|
+
```ts
|
|
314
|
+
interface RouteLocation {
|
|
315
|
+
url: URL;
|
|
316
|
+
params: Record<string, string>;
|
|
317
|
+
routeName: string | undefined;
|
|
318
|
+
state: unknown; // the entry's history.state; read slots with Def.read(location)
|
|
319
|
+
}
|
|
282
320
|
|
|
283
|
-
|
|
321
|
+
interface TransitionWhenContext {
|
|
322
|
+
kind: "push" | "replace" | "pop" | "action" | "revalidate";
|
|
323
|
+
from: RouteLocation; // the committed location being left
|
|
324
|
+
to: RouteLocation; // the destination; === from for "action" and "revalidate"
|
|
325
|
+
isAction: IsActionFn; // the revalidate() matcher; false off-action
|
|
326
|
+
action?: { id; formData; result; error }; // kind "action" only
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
| `kind` | started by | `to.state` |
|
|
331
|
+
| -------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
332
|
+
| `"push"` | `Link`, `router.push()` | the pushed state plus server `ctx.setLocationState()` (handler, middleware, action) |
|
|
333
|
+
| `"replace"` | `Link replace`, `router.replace()` | same as push |
|
|
334
|
+
| `"pop"` | back/forward | the target entry's stored state |
|
|
335
|
+
| `"action"` | a server action's revalidation, error boundary incl. | the current entry's state plus action-set state (none on the error lane: an error response sets no location state); `action.*` set |
|
|
336
|
+
| `"revalidate"` | `router.refresh()`, a stale entry's revalidation | the current entry's state |
|
|
337
|
+
|
|
338
|
+
- `Def.read(location)` reads a location-state slot from `from` or `to`
|
|
339
|
+
without clearing flash state.
|
|
340
|
+
- Middleware-set location state reaches `to.state` on a PPR replay HIT too
|
|
341
|
+
(middleware runs on every HIT); a route handler's does not, because a HIT
|
|
342
|
+
runs no handler. Set it from middleware when a `ppr` route decides on it.
|
|
343
|
+
- A `clientUrls()` cross-route navigation decides at its optimistic swap,
|
|
344
|
+
before the server responds, so its `to.state` holds only the pushed
|
|
345
|
+
state.
|
|
346
|
+
- On `"pop"`, `from.state` and `from.routeName` come from the router's
|
|
347
|
+
per-history-entry memory; after a reload they are `undefined`.
|
|
348
|
+
- `get()`, `env` and handler state do not exist in the browser.
|
|
349
|
+
|
|
350
|
+
### Combination and `false`
|
|
351
|
+
|
|
352
|
+
- The navigation holds only when **every** committed segment's `when`
|
|
353
|
+
returns true, kept or re-sent: a layout the navigation keeps still gates
|
|
354
|
+
it ([#989](https://github.com/rangojs/rango/issues/989)). The same function
|
|
355
|
+
on several segments is called once.
|
|
356
|
+
- `false` makes the whole navigation urgent, so no segment animates. Every
|
|
357
|
+
`<ViewTransition>` stays in the tree with its classes set to `"none"`, so a
|
|
358
|
+
same-route navigation that flips `true -> false -> true` never remounts
|
|
359
|
+
the route or loses its state
|
|
360
|
+
([#995](https://github.com/rangojs/rango/issues/995)).
|
|
361
|
+
- That holds for every kind, actions and revalidations included: a gated-off
|
|
362
|
+
action or revalidation commit is urgent too, so a segment it re-renders
|
|
363
|
+
streams its `loading()`. The router commits an action's result after the
|
|
364
|
+
response arrives, outside the transition React opens for a form action or
|
|
365
|
+
`useActionState` call, so React does not hold it either.
|
|
366
|
+
- A predicate that throws counts as `false` and is logged with
|
|
367
|
+
`console.error` (the conservative default: no hold).
|
|
368
|
+
- Intercept (modal) commits skip the evaluation, including a back/forward
|
|
369
|
+
that opens or closes a modal.
|
|
370
|
+
- A stale revalidation the user navigated away from before it landed is
|
|
371
|
+
discarded without calling the predicate.
|
|
372
|
+
- Intercept `from` differs: while a modal is open, an intercept selector's
|
|
373
|
+
`from` is the page under the modal (see the intercept skill); here it is
|
|
374
|
+
always the location on screen.
|
|
375
|
+
- In a `clientUrls()` group, `false` also opts a same-route navigation out of
|
|
376
|
+
the group's default hold.
|
|
377
|
+
|
|
378
|
+
`kind` lines up with the class keys in "Direction-aware transitions": a
|
|
379
|
+
`"pop"` restored from the history cache animates with `"navigation-back"`
|
|
380
|
+
(a pop that refetches uses `"navigation"`); `"push"`, `"replace"` and a
|
|
381
|
+
`router.refresh()` (`"revalidate"`) use `"navigation"`; a stale entry's
|
|
382
|
+
revalidation (`"revalidate"`) and an action's refetch (`"action"`) use
|
|
383
|
+
`"action"`. An action's own commit (`useActionState`, a form action) carries
|
|
384
|
+
no transition type (see the note under "Direction-aware transitions").
|
|
385
|
+
|
|
386
|
+
### Opting out per navigation
|
|
387
|
+
|
|
388
|
+
`router.push(url, { transition: false })`, `router.replace(url, {
|
|
389
|
+
transition: false })` and `<Link transition={false}>` present one
|
|
390
|
+
navigation the way `when` returning `false` does, without calling any
|
|
391
|
+
predicate. Back/forward, actions and revalidation have no per-navigation
|
|
392
|
+
options and are unaffected.
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
<Link to="/photos/2" transition={false}>
|
|
396
|
+
Jump without the slide
|
|
397
|
+
</Link>
|
|
398
|
+
```
|
|
284
399
|
|
|
285
400
|
## Opting out of the router boundary (place your own `<ViewTransition>`)
|
|
286
401
|
|
|
@@ -361,6 +476,12 @@ await expect(page$).toBeVisible();
|
|
|
361
476
|
Waiting for `toHaveCount(1)` first is not enough: the count passes through 1
|
|
362
477
|
before the second host is inserted.
|
|
363
478
|
|
|
479
|
+
Unit-test a `when` with `runTransitionWhen(when, { kind, from, to, action })`
|
|
480
|
+
from `@rangojs/router/testing`, and a route's hold with `renderRoute`:
|
|
481
|
+
`router.navigate()` decides with `kind: "push"`, `router.refresh()` with
|
|
482
|
+
`kind: "revalidate"`. Pop, action and `clientUrls()` optimistic-swap
|
|
483
|
+
decisions are e2e territory. See the testing skill, `predicates.md`.
|
|
484
|
+
|
|
364
485
|
## Recommendations
|
|
365
486
|
|
|
366
487
|
**Put `<ParallelOutlet />` in layouts, not routes.** A route-level `transition` wraps the route component itself, so a `<ParallelOutlet />` rendered directly inside that route component remains inside the route VT subtree — modal opens on a route with a parallel outlet _will_ trigger the route's VT walker. The narrowing fix only applies at layout boundaries. If you combine intercept modals with route-level transitions, mount the slot one level up in a layout.
|
|
@@ -10,10 +10,14 @@ import type {
|
|
|
10
10
|
Handler,
|
|
11
11
|
MiddlewareContext,
|
|
12
12
|
ResponseHandlerContext,
|
|
13
|
+
RouteLocation,
|
|
13
14
|
RouteParams,
|
|
14
15
|
RouteSearchParams,
|
|
16
|
+
ServerRouteLocation,
|
|
17
|
+
TransitionWhenFn,
|
|
15
18
|
} from "../index.js";
|
|
16
19
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
20
|
+
import type { InterceptWhenFn } from "../server/context.js";
|
|
17
21
|
import { href } from "../href-client.js";
|
|
18
22
|
import type { Money, TestBindings } from "./augment.js";
|
|
19
23
|
|
|
@@ -69,6 +73,23 @@ type _routeName = Expect<
|
|
|
69
73
|
Equal<DefaultRouteName, "home" | "blog.post" | "search">
|
|
70
74
|
>;
|
|
71
75
|
|
|
76
|
+
// transition({ when }) / intercept({ when }) locations carry the public route
|
|
77
|
+
// name (internal names never reach them), typed like every routeName.
|
|
78
|
+
type _locationRouteName = Expect<
|
|
79
|
+
Equal<RouteLocation["routeName"], DefaultRouteName | undefined>
|
|
80
|
+
>;
|
|
81
|
+
type _serverLocationRouteName = Expect<
|
|
82
|
+
Equal<ServerRouteLocation["routeName"], DefaultRouteName | undefined>
|
|
83
|
+
>;
|
|
84
|
+
const transitionWhenTypo: TransitionWhenFn = ({ to }) =>
|
|
85
|
+
// @ts-expect-error - "blog.pots" is not a generated route name
|
|
86
|
+
to.routeName === "blog.pots";
|
|
87
|
+
const interceptWhenTypo: InterceptWhenFn = ({ from }) =>
|
|
88
|
+
// @ts-expect-error - "hom" is not a generated route name
|
|
89
|
+
from.routeName === "hom";
|
|
90
|
+
void transitionWhenTypo;
|
|
91
|
+
void interceptWhenTypo;
|
|
92
|
+
|
|
72
93
|
// RouteParams / RouteSearchParams resolve from the generated map with no
|
|
73
94
|
// explicit route map argument.
|
|
74
95
|
type _params = Expect<Equal<RouteParams<"blog.post">, { slug: string }>>;
|
|
@@ -131,6 +152,8 @@ type _flightOverride = Expect<Equal<Rango.FlightSerialize<Money>, number>>;
|
|
|
131
152
|
// Reference the top-level assertion aliases so they are unambiguously evaluated.
|
|
132
153
|
export type _Assertions = [
|
|
133
154
|
_routeName,
|
|
155
|
+
_locationRouteName,
|
|
156
|
+
_serverLocationRouteName,
|
|
134
157
|
_params,
|
|
135
158
|
_search,
|
|
136
159
|
_orderWireByPattern,
|