@rangojs/router 0.18.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/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -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 +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +24 -4
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- 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/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/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/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- 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 +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +89 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- 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-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- 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 +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +960 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- 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 +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +64 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -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 +4 -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 +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +197 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +105 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- 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 +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- 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 +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +28 -3
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +150 -25
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- 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 +30 -10
- package/src/rsc/handler.ts +12 -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 +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +189 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +123 -84
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -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 +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- 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 +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -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 +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -121,15 +121,15 @@ expect(decision.segments?.[0].shouldRevalidate).toBe(true);
|
|
|
121
121
|
|
|
122
122
|
**DSL:** `ppr: true | PartialPrerenderProps` on a page route (see `/ppr`). **Not** the same header as `X-Rango-Cache` — the shell is cached separately from the segments.
|
|
123
123
|
|
|
124
|
-
| Helper | Import | Role
|
|
125
|
-
| ----------------------------------------------- | ------------------------------------ |
|
|
126
|
-
| `assertShellStatus(res, "HIT" \| "MISS")` | `@rangojs/router/testing` or `…/e2e` | Assert `x-rango-shell` on a **real document** Response
|
|
127
|
-
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized)
|
|
128
|
-
| `shellCacheKey(url, searchParams?, partition?)` | same | Production shell store key (`host+pathname+sorted search+:shell`, then `\|partition`) for `store.getShell` / custom stores. Pass your router's `cache.searchParams` config as the second argument when it sets one — the production key applies it. For a route whose `cache({ key })`
|
|
129
|
-
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant
|
|
130
|
-
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision
|
|
131
|
-
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header
|
|
132
|
-
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant
|
|
124
|
+
| Helper | Import | Role |
|
|
125
|
+
| ----------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
126
|
+
| `assertShellStatus(res, "HIT" \| "MISS")` | `@rangojs/router/testing` or `…/e2e` | Assert `x-rango-shell` on a **real document** Response |
|
|
127
|
+
| `parseShellStatus(res)` | same | `"HIT" \| "MISS" \| null` (null = header absent / unrecognized) |
|
|
128
|
+
| `shellCacheKey(url, searchParams?, partition?)` | same | Production shell store key (`host+pathname+sorted search+:shell`, then `\|partition`) for `store.getShell` / custom stores. Pass your router's `cache.searchParams` config as the second argument when it sets one — the production key applies it. For a route whose `cache({ key })` partitions its record, pass that function's result for the request as `partition`: the shell is partitioned the same way, the result namespaced as production stores it (`key:` plus its URI encoding). Under nested keyed `cache()` boundaries, pass their `key()` results as an array, outermost first. For a store `keyGenerator` that partitions it, pass `{ keys, generated }` (`ShellCachePartition`): `generated` lists the `keyGenerator` results one per store, outermost first: leave them out when every one returns the default key, else keep each position and pass `""` for one that returns it. |
|
|
129
|
+
| `SHELL_STATUS_HEADER` | same | `"x-rango-shell"` constant |
|
|
130
|
+
| `assertPprReplayStatus(res, expected)` | same | Assert fresh/stale replay or a bounded bypass decision |
|
|
131
|
+
| `parsePprReplayStatus(res)` | same | Structured replay/bypass status, or null for an absent/unrecognized header |
|
|
132
|
+
| `PPR_REPLAY_STATUS_HEADER` | same | `"x-rango-ppr-replay"` constant |
|
|
133
133
|
|
|
134
134
|
### What unit can prove vs e2e
|
|
135
135
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
| `outletPending` | `boolean` | Seed `useOutlet().pending` through each synthetic segment's production `OutletProvider`. Defaults to `false`; this is context seeding, not a simulated navigation/Suspense/action lifecycle. |
|
|
16
16
|
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>` | Seed by REFERENCE: `[loader, data]` pairs. Robust for real `createLoader()` handles whose `$$id` is empty in a bare test. Prefer over `loaderData`. |
|
|
17
17
|
| `params` | `Record<string, string>` | Explicit params, merged over (and overriding) params extracted from the `request` URL. |
|
|
18
|
-
| `locationState` | `ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>` | Seed `useLocationState(def)` by REFERENCE: `[def, value]` pairs;
|
|
18
|
+
| `locationState` | `ReadonlyArray<readonly [LocationStateDefinition<any, any>, unknown]>` | Seed `useLocationState(def)` by REFERENCE: `[def, value]` pairs; keys an unkeyed `def` (like `withLocationStateKey(def)`) and writes to `history.state`. See [Location state](#location-state). |
|
|
19
19
|
| `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown[]]>` | Seed `useHandle(handle)` by REFERENCE: `[handle, pushedValues[]]`. Accumulated GLOBALLY (not segment-scoped). |
|
|
20
20
|
| `handle` | `HandleDataSeed` | Advanced: raw wire format `{ [handleId]: { [segmentId]: pushedValues[] } }`. Prefer `handles`. Merged with it. |
|
|
21
21
|
| `routeMap` | `Record<string, string>` | Name -> pattern map (informational; client `useReverse` takes its map as an argument, so this is not consumed). |
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| `useSearchParams` | Search params from the `request` URL. |
|
|
42
42
|
| `useNonce` | SEEDED CSP nonce (`options.nonce`), else `undefined` (the browser default). |
|
|
43
43
|
| `useLoader` / `useFetchLoader` | SEEDED loader data (read path, not run path). Held-navigation `isLoading` is modeled (below). |
|
|
44
|
-
| `useLocationState` | SEEDED `history.state` value.
|
|
44
|
+
| `useLocationState` | SEEDED `history.state` value, or the `state` a push/replace/`<Link>` wrote (below). |
|
|
45
45
|
| `useHandle` | SEEDED handle output (globally accumulated). |
|
|
46
46
|
| `Outlet` | Renders the next segment in the chain (layout nesting). |
|
|
47
47
|
| `useOutlet` | Next-segment `content` plus SEEDED `options.outletPending`. |
|
|
@@ -54,13 +54,19 @@ Extends RTL's `RenderResult` (`getByTestId`, `getByText`, `getByRole`, `containe
|
|
|
54
54
|
```ts
|
|
55
55
|
type RenderRouteResult = RenderResult & {
|
|
56
56
|
router: {
|
|
57
|
-
// client-only nav, re-resolves the same routes; `loaders` seeds THIS nav
|
|
57
|
+
// client-only nav, re-resolves the same routes; `loaders` seeds THIS nav.
|
|
58
|
+
// A spec's transition({ when }) decides it as kind "push";
|
|
59
|
+
// `transition: false` commits urgently and calls no predicate.
|
|
58
60
|
navigate(
|
|
59
61
|
url: string,
|
|
60
62
|
options?: {
|
|
61
63
|
loaders?: ReadonlyArray<readonly [LoaderDefinition<any>, unknown]>;
|
|
64
|
+
transition?: boolean;
|
|
62
65
|
},
|
|
63
66
|
): Promise<void>;
|
|
67
|
+
// re-render the current location, as router.refresh() does:
|
|
68
|
+
// a spec's when gets kind "revalidate" (`to` is `from`)
|
|
69
|
+
refresh(): Promise<void>;
|
|
64
70
|
pathname(): string;
|
|
65
71
|
params(): Record<string, string>;
|
|
66
72
|
store: NavigationStore; // advanced
|
|
@@ -114,6 +120,41 @@ it("resolves params + reverse + Outlet through the layout chain", async () => {
|
|
|
114
120
|
});
|
|
115
121
|
```
|
|
116
122
|
|
|
123
|
+
## Location state
|
|
124
|
+
|
|
125
|
+
A `createLocationState()` definition gets its key from the rango Vite plugin, which a bare Vitest project does not run. Outside production an unkeyed definition throws on first use (`Def(value)`, `useLocationState(Def)`, `.read()`), with a message that points here. Key it with `withLocationStateKey` from `@rangojs/router/testing`:
|
|
126
|
+
|
|
127
|
+
- `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState` (stable across runs).
|
|
128
|
+
- `withLocationStateKey(GridState)` keeps a key that is already set, else assigns a synthetic `__rsc_ls_test_<n>` that stays the same for that definition.
|
|
129
|
+
- The `locationState` seed option keys an unkeyed definition the same way, so a seeded definition needs no call. Key every definition a component reads WITHOUT a seed.
|
|
130
|
+
|
|
131
|
+
A `useRouter().push/replace(url, { state })` or `<Link state>` navigation writes its history entry through production's path (`resolveNavigationState` -> `buildHistoryState` -> `pushHistoryWithIdx`, then the `__rsc_locationstate` event), so `useLocationState(Def)` re-reads after the click. The dev check for a bare entry or an uncalled definition runs too, and, as in production, a push or `<Link>` without `state` starts an entry with no location state. The URL in `window.location` does not change (renderRoute tracks location on its event controller), and `router.navigate()` from the test leaves `history.state` alone. The click starts an async navigation, so wait for the result with RTL's `waitFor`:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// @vitest-environment happy-dom
|
|
135
|
+
import { afterEach, expect, it } from "vitest";
|
|
136
|
+
import { cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
137
|
+
import { withLocationStateKey } from "@rangojs/router/testing";
|
|
138
|
+
import { renderRoute } from "@rangojs/router/testing/dom";
|
|
139
|
+
import { LoadMore } from "../src/components/LoadMore"; // router.replace(url, { state: [GridState(...)] })
|
|
140
|
+
import { GridState } from "../src/location-states";
|
|
141
|
+
|
|
142
|
+
afterEach(cleanup);
|
|
143
|
+
withLocationStateKey(GridState, "GridState");
|
|
144
|
+
|
|
145
|
+
it("shows the count it pushed", async () => {
|
|
146
|
+
const { getByTestId } = await renderRoute(
|
|
147
|
+
[{ path: "/grid", Component: LoadMore }],
|
|
148
|
+
{ request: "/grid" },
|
|
149
|
+
);
|
|
150
|
+
fireEvent.click(getByTestId("load-more"));
|
|
151
|
+
await waitFor(() => expect(getByTestId("count").textContent).toBe("40"));
|
|
152
|
+
expect(window.history.state).toMatchObject({
|
|
153
|
+
__rsc_ls_GridState: { count: 40 },
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
117
158
|
## Held navigation
|
|
118
159
|
|
|
119
160
|
A stale indicator driven by `useLoader().isLoading` (see `/hooks` data.md, "A held navigation flags the data it keeps on screen") IS unit-testable. Put `transition` on the spec that declares `transition()` in your `urls()`, and pass the next navigation's loader data to `router.navigate(url, { loaders })` as a PENDING Promise. navigate() then commits through production's `commitInTransition` (browser/partial-update.ts), which calls `loaderStore.announcePendingStreams` inside the `startTransition`. The harness does not fake the flag: React holds the reader on screen and the real `useLoader` pin reports `isLoading: true` until the promise settles.
|
|
@@ -154,6 +195,7 @@ it("dims the held price while the next product streams", async () => {
|
|
|
154
195
|
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
|
|
155
196
|
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
|
|
156
197
|
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
|
|
198
|
+
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
|
|
157
199
|
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
|
|
158
200
|
| `useNavigation().state`, `useLinkStatus().pending`, `useAction().state` | Stay `idle` — navigate() never starts the navigation lifecycle. e2e. |
|
|
159
201
|
|
package/skills/testing/loader.md
CHANGED
|
@@ -121,11 +121,12 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
|
121
121
|
- A loader has no `isAction()`: whether it re-runs after an action is decided by `revalidate()` on its registration, not inside the loader body. Assert post-action re-runs at e2e.
|
|
122
122
|
- Seeded `loaders` (by-reference tuples) are NOT executed — `ctx.use(OtherLoader)` returns the seeded value. The dynamic `use` resolver, by contrast, IS executed (it is a function called to compute the value). Either way the REAL loader body is not run; real loader execution and side-effects are e2e-only. `loaders` is checked before the `use` resolver.
|
|
123
123
|
- A `createLoader()` definition imported through the CLIENT build has its body dropped — `runLoader` throws a clear error pointing to the `rangoTestConfig()` preset or the raw body. A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`); only the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules).
|
|
124
|
+
- Location state needs a key: a `createLocationState()` definition gets it from the Vite plugin, which a test project does not run. In the node project (`NODE_ENV` is not `production`) an unkeyed `Flash(value)` throws, so a loader's `throw redirect(url, { state: [Flash(value)] })` surfaces on `runLoaderResult`'s `thrown` as that missing-key error, not the redirect; in the react-server project (`NODE_ENV=production`) the value lands on `locationState["undefined"]`. Call `withLocationStateKey(Flash, "Flash")` from `@rangojs/router/testing` once per definition and assert `locationState` as `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix).
|
|
124
125
|
- No `cookies`/`headers` option: seed a cookie by passing a full Request with a Cookie header — `{ request: new Request(url, { headers: { Cookie: "sid=abc" } }) }`. (`search`/`method` are baked onto this request for you.)
|
|
125
126
|
- `ctx.search` (typed) defaults to `{}`; `opts.search` only sets the raw `ctx.searchParams`. Seed the typed object with `searchData`. (The harness seeds `searchData` verbatim — it does NOT run a typed-search SCHEMA, so schema parsing/validation is e2e.)
|
|
126
|
-
- `ctx.theme`/`ctx.setTheme` are NOT on the loader context — they are handler/middleware-only. The `theme` option seeds the request context's theme config, so a loader body reads the visitor's theme with `getRequestContext().theme` (seed the cookie through `request`). That read is guarded like `cookies()` (#971): allowed in a loader body, but it throws inside a `"use cache"` function. `redirect()` does no basename prefixing unless you seed `basename`.
|
|
127
|
+
- `ctx.theme`/`ctx.setTheme` are NOT on the loader context — they are handler/middleware-only. The `theme` option seeds the request context's theme config, so a loader body reads the visitor's theme with `getRequestContext().theme` (seed the cookie through `request`). That read is guarded like `cookies()` (#971): allowed in a loader body, but it throws inside a `"use cache"` function. `ctx.request.headers` is guarded the same way. `redirect()` does no basename prefixing unless you seed `basename`.
|
|
127
128
|
- A `"use cache"` hit needs real Flight, which loads only under the react-server condition. In the react-server project (a `*.rsc-test.ts` file, see [`./setup.md`](./setup.md)) with `cacheStore` seeded, the body runs once across `runLoader` calls and the value round-trips. The write is a background (`waitUntil`) task `runLoader` does not await: a call before it lands joins the still-running execution, a call after it reads the store (wait with `vi.waitFor` on a `cacheStore.setItem` spy). In the node project a call looks up the store but never writes, so every call runs the body. A function written with the directive needs `rangoUseCacheTransform()` in the project's `plugins` (see [`./render-handler.md`](./render-handler.md#a-use-cache-hit)).
|
|
128
|
-
- The `cache` option runs the loader through the production loader-cache read-through, so it needs the react-server project for the same reason. A HIT returns the stored value without running the body and records the tags the body recorded when the entry was written, so `updateTag()` of one (inside `runInRequestContext(fn, { cacheStore })`) drops the entry. There is no in-flight join here: wait for the write before the next call, or that call misses too. A raw body is keyed by its function reference. `runLoaderResult(...).handlePushes` lists the body's pushes, so a HIT reports none (the replay into the page's handle store is not modeled).
|
|
129
|
+
- The `cache` option runs the loader through the production loader-cache read-through, so it needs the react-server project for the same reason. A HIT returns the stored value without running the body and records the tags the body recorded when the entry was written, so `updateTag()` of one (inside `runInRequestContext(fn, { cacheStore })`) drops the entry. There is no in-flight join here: wait for the write before the next call, or that call misses too. A raw body is keyed by its function reference. With no `key()`, a fill whose body read request identity (`cookies()`, `headers()`, `ctx.request.headers`, a `{ cache: false }` variable) fails with the identity error and stores nothing; add a `key()` that includes the value. `runLoaderResult(...).handlePushes` lists the body's pushes, so a HIT reports none (the replay into the page's handle store is not modeled).
|
|
129
130
|
|
|
130
131
|
```ts
|
|
131
132
|
const load = () =>
|
|
@@ -95,6 +95,7 @@ Pass an array to run several in order. Cookies set inside middleware via the sta
|
|
|
95
95
|
- A COMPONENT route's guard stack cannot be exercised through `dispatch` (it throws on component routes), and `renderToFlightString`/`renderRoute` don't run route middleware. Extract the middleware fn and unit-test it here, or assert the guard stack at e2e.
|
|
96
96
|
- Middleware-phase `ctx.reverse` is map-only (global names only, no auto-fill from current params), matching production — enable it with `routeMap`. `routeName` only feeds `ctx.routeName`; it does NOT scope `.name` reverse (the chain reverse stays map-only by design, and `MiddlewareContext["reverse"]` rejects `.name` at compile time).
|
|
97
97
|
- `ctx.theme` is `undefined` unless `theme` is passed; `redirect()` does no basename prefixing unless `basename` is seeded.
|
|
98
|
+
- Location state needs a key: a `createLocationState()` definition gets it from the Vite plugin, which a test project does not run, and outside production an unkeyed `Flash(value)` throws inside the middleware (before `redirect()` or `ctx.setLocationState()` runs). Call `withLocationStateKey(Flash, "Flash")` from `@rangojs/router/testing` once per definition and assert `locationState` as `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix).
|
|
98
99
|
- Platform bindings are yours to double via `env` (see `./bindings.md`).
|
|
99
100
|
|
|
100
101
|
## See also
|
|
@@ -2,66 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
**Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `transition({ when })` (see `/view-transitions`) and `clientUrls()` `revalidate()` (see `/client-urls`)
|
|
4
4
|
|
|
5
|
-
Both primitives are synchronous and run the router's own evaluation code on arguments you seed, so a predicate sees the same fields it sees at runtime. Neither renders anything: they answer "would this
|
|
6
|
-
|
|
7
|
-
## runTransitionWhen(config, opts?)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Options — `RunTransitionWhenOptions
|
|
12
|
-
|
|
13
|
-
Every field is optional.
|
|
14
|
-
|
|
15
|
-
| Field
|
|
16
|
-
|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `onError` | `OnErrorCallback` | Receives an error the predicate throws (the gate reports it with phase `"rendering"`). |
|
|
30
|
-
| `ppr` | `boolean` | Model a `ppr` route, where the predicate runs before route handlers and the cache lookup. |
|
|
31
|
-
|
|
32
|
-
### Returns — `RunTransitionWhenResult<TEnv>`
|
|
33
|
-
|
|
34
|
-
| Field | Type | Meaning |
|
|
35
|
-
| ------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
|
36
|
-
| `kept` | `boolean` | True when the transition applies (the predicate did not return `false`, or there is no `when`). |
|
|
37
|
-
| `dropped` | `boolean` | `!kept`. |
|
|
38
|
-
| `whenContext` | `TransitionWhenContext \| undefined` | The context the predicate received; `undefined` when the config has no `when`. |
|
|
39
|
-
| `ctx` | `RequestContext<TEnv>` | The underlying request context, for extra assertions. |
|
|
5
|
+
Both primitives are synchronous and run the router's own evaluation code on arguments you seed, so a predicate sees the same fields it sees at runtime. Neither renders anything: they answer "would this navigation hold?" and "would this `clientUrls()` loader re-run?". Both predicates run in the browser at runtime.
|
|
6
|
+
|
|
7
|
+
## runTransitionWhen(when | config, opts?)
|
|
8
|
+
|
|
9
|
+
Builds the `TransitionWhenContext` the browser builds and evaluates the predicate through the router's own browser code (`browser/transition-when.ts`): a throw counts as `false` and is logged with `console.error`, exactly as at navigation time. Accepts the predicate or a whole `transition()` config (a config without `when` always applies).
|
|
10
|
+
|
|
11
|
+
### Options — `RunTransitionWhenOptions`
|
|
12
|
+
|
|
13
|
+
Every field is optional. A location (`from` / `to`) is a URL string, a `URL`, or a partial `RouteLocation`: `{ url?, params?, routeName?, state? }`. A `state` given as location-state entries (`[Def(value)]`, what `Link` and `router.push` take) is stored the way a push stores it, so `Def.read(ctx.to)` reads it back; any other value is used as the raw `history.state`.
|
|
14
|
+
|
|
15
|
+
| Field | Type | Meaning |
|
|
16
|
+
| -------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `kind` | `"push" \| "replace" \| "pop" \| "action" \| "revalidate"` | Defaults to `"action"` when `action` is given, `"push"` otherwise. |
|
|
18
|
+
| `from` | location | The committed location being left. Defaults to `http://localhost/`. |
|
|
19
|
+
| `to` | location | The destination. Defaults to `from`; always `from` for `"action"` and `"revalidate"`. |
|
|
20
|
+
| `action` | imported action \| `string` \| `{ ref, formData, result, error }` | The triggering action (`isAction()` matches it; `action.id` is its id). Like `runClientRevalidate`, an imported action outside a built app needs its id passed as the string. |
|
|
21
|
+
|
|
22
|
+
### Returns — `RunTransitionWhenResult`
|
|
23
|
+
|
|
24
|
+
| Field | Type | Meaning |
|
|
25
|
+
| ---------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `applied` | `boolean` | True when the navigation holds: the predicate returned anything but `false` and did not throw, or there is no `when`. |
|
|
27
|
+
| `gatedOff` | `boolean` | `!applied`: the navigation commits urgently with no view transition. |
|
|
28
|
+
| `context` | `TransitionWhenContext` | The context the predicate received. |
|
|
40
29
|
|
|
41
30
|
### Recipe
|
|
42
31
|
|
|
43
32
|
```ts
|
|
44
33
|
import { it, expect } from "vitest";
|
|
45
|
-
import {
|
|
46
|
-
|
|
34
|
+
import {
|
|
35
|
+
runTransitionWhen,
|
|
36
|
+
withLocationStateKey,
|
|
37
|
+
} from "@rangojs/router/testing";
|
|
38
|
+
import { slideWhen } from "../src/transitions.js"; // a "use client" module
|
|
39
|
+
import { Slide } from "../src/location-states.js";
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
when: (c) => c.fromRouteName === "products",
|
|
50
|
-
};
|
|
41
|
+
withLocationStateKey(Slide, "Slide");
|
|
51
42
|
|
|
52
|
-
it("animates
|
|
43
|
+
it("animates unless the Link pushed { animate: false }", () => {
|
|
53
44
|
expect(
|
|
54
|
-
runTransitionWhen(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}).
|
|
45
|
+
runTransitionWhen(slideWhen, {
|
|
46
|
+
from: { url: "/photos/1", params: { id: "1" }, routeName: "photo" },
|
|
47
|
+
to: { url: "/photos/2", params: { id: "2" }, routeName: "photo" },
|
|
48
|
+
}).applied,
|
|
58
49
|
).toBe(true);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
|
|
51
|
+
const { gatedOff, context } = runTransitionWhen(slideWhen, {
|
|
52
|
+
from: "/photos/1",
|
|
53
|
+
to: { url: "/photos/2", state: [Slide({ animate: false })] },
|
|
54
|
+
});
|
|
55
|
+
expect(Slide.read(context.to)).toEqual({ animate: false });
|
|
56
|
+
expect(gatedOff).toBe(true);
|
|
62
57
|
});
|
|
63
58
|
```
|
|
64
59
|
|
|
60
|
+
### Known limits (Vitest)
|
|
61
|
+
|
|
62
|
+
- A Vitest project runs neither the Vite plugin's hoist nor its validation. An inline `when` in a `urls()` file stays a plain function there, and a `"use client"` import is the plain function outside the RSC graph, so a server function is not told apart and is accepted. The loud error for an invalid `when` belongs to dev startup, the build and HMR.
|
|
63
|
+
- `renderRoute` decides each commit with production's decision code: `router.navigate()` (and `useRouter().push/replace`, `<Link>`) as `kind: "push"` / `"replace"`, `router.refresh()` as `kind: "revalidate"`, and `navigate(url, { transition: false })` without calling the predicate. Pop, action and `clientUrls()` optimistic-swap decisions are e2e territory.
|
|
64
|
+
|
|
65
65
|
## runClientRevalidate(fn | fn[], opts?)
|
|
66
66
|
|
|
67
67
|
Evaluates one `clientUrls()` loader `revalidate()` predicate, or a chain in declaration order, through the production chain evaluator: the locked default, boolean short-circuit, soft verdicts (`{ defaultShouldRevalidate }`), and a throwing predicate deferring to the default all behave as in the browser. Returns the final `boolean` (the locked default when every predicate defers or throws).
|
|
@@ -111,7 +111,7 @@ it("revalidates only when the tab changes", () => {
|
|
|
111
111
|
## Caveats
|
|
112
112
|
|
|
113
113
|
- Server-tree `revalidate()` predicates have no dedicated primitive: they are plain functions, so call them with a hand-built args object, or assert the post-action result at e2e.
|
|
114
|
-
- `runTransitionWhen` proves the
|
|
114
|
+
- `runTransitionWhen` proves the decision only. Whether the browser actually runs a view transition is e2e territory (see `./e2e-parity.md`).
|
|
115
115
|
- Both are synchronous; a predicate returning a Promise is a bug in the predicate, not something to await here.
|
|
116
116
|
|
|
117
117
|
## See also
|
|
@@ -159,6 +159,7 @@ Without the plugin, wrap a function yourself with the call the transform emits,
|
|
|
159
159
|
- An unseeded `ctx.use(loader)` REJECTS with a setup error — seed every dependency via `{ loaders: [[OtherLoader, data]] }`, matched by reference. Loaders are SEEDED, not executed (same as `runLoader`).
|
|
160
160
|
- Same alias requirement as flight tests: without the `@rangojs/router -> index.rsc.ts` alias (see [`./setup.md`](./setup.md)), a handler reading `getRequestContext()`/`cookies()` hits the throwing out-of-react-server stub. `renderHandler` detects this and REJECTS with a setup error naming `rangoTestAliases` (it is not captured on `thrown`).
|
|
161
161
|
- A `throw redirect()` is captured on `thrown` (with `tree` undefined, since it produced a `Response`) — assert on `thrown`/`response`, no try/catch needed.
|
|
162
|
+
- Location state needs a key: a `createLocationState()` definition gets it from the Vite plugin, which a test project does not run. This react-server project runs with `NODE_ENV=production`, so an unkeyed `ctx.setLocationState(Flash(value))` does not throw; the value lands on `locationState["undefined"]`. Call `withLocationStateKey(Flash, "Flash")` from `@rangojs/router/testing` once per definition and assert `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key).
|
|
162
163
|
- No hydration and no interaction — for clicks, forms, and navigation use e2e.
|
|
163
164
|
- `renderHandler` runs a handler FUNCTION `(ctx) => rsc`; for a plain ELEMENT `<Page/>` use `renderServerTree` (see [`./server-tree.md`](./server-tree.md)).
|
|
164
165
|
- A handler that calls a `"use cache"` function runs UNCACHED unless you seed `cacheStore` (and `cacheProfiles` for a named profile). With nothing seeded the runtime bypasses to the live body and warns once under the test runner. With a store seeded, the body runs once across renders and a render after the background write reads the store — see [A `"use cache"` hit](#a-use-cache-hit), including `rangoUseCacheTransform()`, without which Vitest leaves the directive unwrapped.
|
|
@@ -135,6 +135,8 @@ expect(silver.shellStatus).toBe("MISS"); // its own capture, never gold's shell
|
|
|
135
135
|
const gold = await serveShellRequest(router, "/pricing", tier("gold"));
|
|
136
136
|
expect(gold.shellStatus).toBe("HIT");
|
|
137
137
|
expect(gold.key).toBe(shellCacheKey("/pricing", undefined, "tier:gold"));
|
|
138
|
+
// "localhost/pricing:shell|key%3Atier%253Agold": the key() result is
|
|
139
|
+
// namespaced as production stores it (issue #975).
|
|
138
140
|
expect(await gold.readEntry()).not.toBeNull();
|
|
139
141
|
```
|
|
140
142
|
|
|
@@ -151,6 +153,18 @@ expect(result.key).toBe(
|
|
|
151
153
|
);
|
|
152
154
|
```
|
|
153
155
|
|
|
156
|
+
A store `keyGenerator` result that partitions the record partitions the shell too, an enclosing `cache({ store })` on another store included (issue #974). Pass those results as `generated`, with any `key()` results as `keys`:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// createRouter({ cache: { store: localeStore } }), where localeStore's
|
|
160
|
+
// keyGenerator returns `${defaultKey}|${locale}`
|
|
161
|
+
expect(result.key).toBe(
|
|
162
|
+
shellCacheKey("/pricing", undefined, {
|
|
163
|
+
generated: ["doc:localhost/pricing|de"],
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
154
168
|
A partial request has no HTML step, so its `key` is the URL's key without a partition: read a partitioned route's entry from a document request's result.
|
|
155
169
|
|
|
156
170
|
## Handles and loader data
|
|
@@ -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
|
@@ -123,6 +123,7 @@ and `getRequestContext().theme` are guarded like `cookies()`:
|
|
|
123
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
124
|
| a route with no `ppr` or `cache()` above it, or middleware | Allowed: rendered per request |
|
|
125
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 |
|
|
126
127
|
|
|
127
128
|
The theme getters are read-only and non-enumerable: `{ ...ctx }` and
|
|
128
129
|
`Object.assign({}, ctx)` do not carry `theme`, and assigning it throws.
|
|
@@ -133,10 +134,13 @@ write there; call it from middleware.
|
|
|
133
134
|
Read the theme where it is per request instead:
|
|
134
135
|
|
|
135
136
|
- `useTheme()` in a client component. The `<html>` class is right before
|
|
136
|
-
paint (the theme script sets it).
|
|
137
|
-
no-cookie default (`defaultTheme`),
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
140
144
|
- A live loader (no `ssr: false`). Loaders do not get `ctx.theme` or
|
|
141
145
|
`ctx.setTheme`; read the cookie directly. A live loader runs on every
|
|
142
146
|
request, HITs included, and on a `ppr` route needs `loading()` or an inline
|
|
@@ -339,3 +339,11 @@ the value at click time; `ctx.setLocationState(entry)` attaches state from a
|
|
|
339
339
|
handler; `redirect(url, { state })` carries it through a redirect; and
|
|
340
340
|
`ProductPreview.read()` / `.write(value)` / `.delete()` access the current
|
|
341
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.
|
|
@@ -229,13 +229,32 @@ export const ProductLoader = createLoader(async (ctx) => getProduct(ctx));
|
|
|
229
229
|
per-request values (cookies, headers) are not reflected in the cache key. Without
|
|
230
230
|
this guard, one user's data would be served to another.
|
|
231
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
|
+
|
|
232
238
|
Extract the value before the cached function and pass it as an argument:
|
|
233
239
|
|
|
234
240
|
```typescript
|
|
235
241
|
const locale = cookies().get("locale")?.value ?? "en";
|
|
236
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);
|
|
237
247
|
```
|
|
238
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
|
+
|
|
239
258
|
`ctx.get()` of a **non-cacheable variable** (`createVar({ cache: false })`, or
|
|
240
259
|
a value written with `ctx.set(key, value, { cache: false })`) throws the same
|
|
241
260
|
way, whether it goes through `getRequestContext().get()` or a `ctx` passed in
|
|
@@ -259,17 +278,20 @@ Ordinary (cacheable) variables stay readable. Calling the cached function from
|
|
|
259
278
|
a loader does not exempt it: the loader re-runs on every request, but the
|
|
260
279
|
cached body does not.
|
|
261
280
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
async chain, loaders included.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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.
|
|
273
295
|
|
|
274
296
|
### Side-Effect Guards
|
|
275
297
|
|
|
@@ -532,11 +554,35 @@ on a navigation or action), not a cache bust.
|
|
|
532
554
|
A route `cache()` entry stores the tags of the `"use cache"` reads inside it,
|
|
533
555
|
so `updateTag()` of one evicts the enclosing entry and any `ppr` shell or
|
|
534
556
|
document built from it. A loader's own `cache()` entry does the same for the
|
|
535
|
-
reads in its body
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
540
586
|
|
|
541
587
|
## Interaction with Other Caching
|
|
542
588
|
|
|
@@ -552,9 +598,8 @@ from joining or storing an outer call that started before the invalidation
|
|
|
552
598
|
Inside a `ppr` shell, a `"use cache"` value that renders as shell material is
|
|
553
599
|
pinned at capture time for the life of that shell (see `/ppr` → Pitfalls). A
|
|
554
600
|
hole's own read (in a loader without `ssr: false` under `loading()` or an
|
|
555
|
-
inline `<Suspense>`) returns the current entry,
|
|
556
|
-
|
|
557
|
-
the same entry.
|
|
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.
|
|
558
603
|
|
|
559
604
|
## Dev Mode and tests
|
|
560
605
|
|