@rangojs/router 0.22.0 → 0.23.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/bin/rango.js +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +843 -309
- package/package.json +2 -2
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/loader-registry.ts +6 -1
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
|
@@ -84,21 +84,24 @@ per app (both compose in one `urls()` tree via `include()`):
|
|
|
84
84
|
| component (client, hooks) | stays a client component — no conversion |
|
|
85
85
|
| `loader` (throws `redirect`/404) | `createLoader()` — thrown `redirect()`/`notFound()` kept |
|
|
86
86
|
| `useLoaderData()` | `useLoader(Loader)` at the read site, under `<Suspense>` |
|
|
87
|
-
| `shouldRevalidate` (runs client) | `revalidate()` predicate —
|
|
87
|
+
| `shouldRevalidate` (runs client) | `revalidate()` predicate — runs on the server |
|
|
88
88
|
| `meta({ data })` | `ctx.use(Meta)` push from the loader body |
|
|
89
89
|
| `defer` / `Await` | loaders stream; `<Suspense>` above each read |
|
|
90
90
|
|
|
91
|
-
Note the `shouldRevalidate` row:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
Note the `shouldRevalidate` row: RR runs it in the browser, but a
|
|
92
|
+
`clientUrls()` loader's predicate runs on the server like a server-tree
|
|
93
|
+
`revalidate()`, with the same arguments (`actionResult` and `formData`
|
|
94
|
+
included). The build moves it out of the `"use client"` module, so it may
|
|
95
|
+
reference imports and globals only: move a predicate that captures
|
|
96
|
+
module-local values into its own module and import it. A group ported this
|
|
97
|
+
way can still be re-migrated to server handlers later, route by route.
|
|
95
98
|
|
|
96
99
|
Start with server handlers; reach for `clientUrls()` when a route group's
|
|
97
100
|
conversion cost is dominated by rewriting interactive components rather than
|
|
98
101
|
by moving data fetching — or when the group is a **high-navigation-speed
|
|
99
102
|
surface** (dashboard, admin panel, settings): browser-local matching gives
|
|
100
|
-
instant optimistic pending, and
|
|
101
|
-
tab/param switches, so transitions are the fastest Rango offers. See
|
|
103
|
+
instant optimistic pending, and per-loader `revalidate()` predicates (run on
|
|
104
|
+
the server) hold data across tab/param switches, so transitions are the fastest Rango offers. See
|
|
102
105
|
`/client-urls`.
|
|
103
106
|
|
|
104
107
|
## Migration Strategy
|
|
@@ -55,7 +55,7 @@ Use it only when you need capabilities beyond what the handler provides:
|
|
|
55
55
|
If the React Router loader just fetches data for its page component AND the
|
|
56
56
|
component can become a server component, merge it into the handler. If the
|
|
57
57
|
component stays a client component, port the whole group with `clientUrls()`
|
|
58
|
-
instead — loader, `useLoader` read, and
|
|
58
|
+
instead — loader, `useLoader` read, and per-loader `revalidate()` keep the RR
|
|
59
59
|
route-module shape (see the "Two target shapes" section in the main skill and
|
|
60
60
|
`/client-urls`). See `/loader` for when the live data layer is useful.
|
|
61
61
|
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -1069,6 +1069,19 @@ Four hard edges (each e2e/unit-pinned):
|
|
|
1069
1069
|
must be fresh on every serve, it belongs on the live lane (no `ssr: false`) or
|
|
1070
1070
|
in a nested promise.
|
|
1071
1071
|
|
|
1072
|
+
### `prefetch: false` on a `ppr` route
|
|
1073
|
+
|
|
1074
|
+
A `ppr` route's handler output is stored, so a `loading()` flagged with
|
|
1075
|
+
`prefetch: false` does not skip the handler in a `<Link>` prefetch: the shell
|
|
1076
|
+
and its partial replay are served as usual and only the live loaders behind
|
|
1077
|
+
the fallback are deferred. A live loader registered with `{ prefetch: false }` is
|
|
1078
|
+
skipped the same way, and the click that adopts the prefetch sends one fill
|
|
1079
|
+
request for it. An `ssr: false` loader is the bake lane and is served from the
|
|
1080
|
+
shell, so on a `ppr` route `prefetch: false` is ignored for it, and
|
|
1081
|
+
development logs a warning that the loader "is registered with ssr: false
|
|
1082
|
+
and prefetch: false on a ppr route"; drop one of the two options. See
|
|
1083
|
+
`/loader` → "`prefetch: false`".
|
|
1084
|
+
|
|
1072
1085
|
### The layout-with-loaders playbook (the storefront case)
|
|
1073
1086
|
|
|
1074
1087
|
The most common real-app shape: an app-wide layout registers per-user loaders
|
|
@@ -666,6 +666,16 @@ build-time shell capture runs it and bakes its settled data into the shell,
|
|
|
666
666
|
and a shell HIT runs it only when its return carries promises (`/ppr` → "The
|
|
667
667
|
bake lane"). The Flight payload collection itself still skips loaders.
|
|
668
668
|
|
|
669
|
+
### `prefetch: false` skips only the loaders
|
|
670
|
+
|
|
671
|
+
A `Prerender` handler's output is stored, so a `loading()` flagged with
|
|
672
|
+
`prefetch: false` on the route does not skip the handler in a `<Link>` prefetch: the
|
|
673
|
+
stored output is served as usual, and only the loaders behind the fallback are
|
|
674
|
+
deferred (a loader registered with `{ prefetch: false }` likewise). The click
|
|
675
|
+
that adopts the prefetch shows the fallback and sends one fill request for
|
|
676
|
+
them. This holds in development, where there is no build artifact, as in
|
|
677
|
+
production. See `/loader` → "`prefetch: false`".
|
|
678
|
+
|
|
669
679
|
### Build-time handle data is frozen
|
|
670
680
|
|
|
671
681
|
Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
|
|
@@ -1283,8 +1293,8 @@ fresh.
|
|
|
1283
1293
|
A warm replaces the entry where it runs, and on a shared store that is the
|
|
1284
1294
|
entry other locations read on their next miss. It does not reach a copy
|
|
1285
1295
|
another location already holds: on `CFCacheStore`, each colo keeps serving its
|
|
1286
|
-
own Cache API copy until it expires
|
|
1287
|
-
60 seconds never reaches KV at all.
|
|
1296
|
+
own Cache API copy until it expires or a tag invalidation rejects it, and an
|
|
1297
|
+
entry whose `ttl + swr` is under 60 seconds never reaches KV at all.
|
|
1288
1298
|
|
|
1289
1299
|
So after a deploy (new keys: no location holds a copy) a warm alone makes
|
|
1290
1300
|
the route ready everywhere. For a content change, invalidate first, then
|
|
@@ -1306,12 +1316,16 @@ path.json("/hooks/product-changed", async (ctx) => {
|
|
|
1306
1316
|
|
|
1307
1317
|
The invalidation makes every location's copy unservable, so no visitor is
|
|
1308
1318
|
served the old content, and the warm writes the new entry where it runs and
|
|
1309
|
-
to the shared store.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1319
|
+
to the shared store. Every other location reads that entry on its next
|
|
1320
|
+
request and renders nothing: a location with no copy of its own, and, on
|
|
1321
|
+
`CFCacheStore` with `kvFallThrough: true` (default false), a colo that was
|
|
1322
|
+
still holding its own (now invalidated) copy, which reads KV as if it held
|
|
1323
|
+
none. Without the option such a colo renders once for itself, unless the store
|
|
1324
|
+
uses `tagPurge`, which evicts those copies. With the option, a colo renders
|
|
1325
|
+
for itself only while KV still hands it the entry from before the invalidation,
|
|
1326
|
+
which it refuses: Cloudflare documents up to 60
|
|
1327
|
+
seconds for a KV write to spread. Await `updateTag()` before the warm: a
|
|
1328
|
+
write that started before an invalidation of one of its tags is refused.
|
|
1315
1329
|
|
|
1316
1330
|
### Limits worth knowing
|
|
1317
1331
|
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -168,13 +168,16 @@ stated, greppable contract.
|
|
|
168
168
|
only applies when the enclosing cache recomputes; put a value in a loader if it
|
|
169
169
|
must be fresher. See `/cache-guide` → "Combining Both".
|
|
170
170
|
- **A predicate runs where its effect lands.** A server-tree `revalidate()`
|
|
171
|
-
|
|
171
|
+
and a `clientUrls()` loader's `revalidate()` decide what the server re-sends,
|
|
172
|
+
so they run on the server (the build hoists a group's predicate out of its
|
|
173
|
+
`"use client"` module and passes it the `urls()` arguments: `/client-urls`);
|
|
174
|
+
an `intercept({
|
|
172
175
|
when })` selector decides what the server renders, so it runs on the server;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
`transition({ when })` decides a browser commit, so it runs in the browser.
|
|
177
|
+
A `transition({ when })` predicate sees URLs, params, route names, history
|
|
178
|
+
state and action data, never `get()`, `env` or handler state; in a server
|
|
179
|
+
`urls()` it is written inline (the build hoists it into a client module) or
|
|
180
|
+
imported from a `"use client"` module (`/view-transitions`).
|
|
178
181
|
- **Vocabulary.** `revalidate()` keeps React Router's `shouldRevalidate`
|
|
179
182
|
dialect (`currentUrl`/`nextUrl`, `currentParams`/`nextParams`). Navigation
|
|
180
183
|
selectors (`intercept({ when })`, `transition({ when })`) use `from`/`to`
|
|
@@ -270,6 +273,9 @@ the `path()` re-run; layouts and parallels above the route are skipped.
|
|
|
270
273
|
revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
|
|
271
274
|
```
|
|
272
275
|
|
|
276
|
+
The same `actionResult` idiom works in a `clientUrls()` group's loader
|
|
277
|
+
`revalidate()`, which runs on the server with these arguments.
|
|
278
|
+
|
|
273
279
|
**The source is the source of truth.** Structure, types, and update policy are
|
|
274
280
|
visible and local in the tree — read top-down, no hidden global model to hold in
|
|
275
281
|
your head. A snippet earns its place only if, from the code alone, you can answer:
|
package/skills/route/SKILL.md
CHANGED
|
@@ -238,6 +238,145 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
238
238
|
])
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
+
### `loading()` options
|
|
242
|
+
|
|
243
|
+
`loading(fallback, options?)` takes two options:
|
|
244
|
+
|
|
245
|
+
| Option | Effect |
|
|
246
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `ssr: false` | Skip the fallback on document requests; it shows on client navigations only. |
|
|
248
|
+
| `prefetch: false` | Keep everything behind this fallback out of `<Link>` prefetches; the click shows the fallback and fetches that part itself. |
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
path("/orders", OrdersPage, { name: "orders" }, () => [
|
|
252
|
+
loader(OrdersLoader),
|
|
253
|
+
loading(<OrdersSkeleton />, { prefetch: false }),
|
|
254
|
+
]),
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**What `prefetch: false` defers.** The flag applies when the entry is new to
|
|
258
|
+
the browser: the page the prefetch is sent from does not show it. A prefetch
|
|
259
|
+
then skips the entry's handler, its loaders, its orphan layouts and its
|
|
260
|
+
parallel slots. On a `layout()` the fallback covers the whole outlet, so every
|
|
261
|
+
deeper entry is skipped with it. A parallel slot with its own flagged
|
|
262
|
+
`loading()` is a separate unit: the route beside it still renders in the
|
|
263
|
+
prefetch. The prefetch payload carries a placeholder for each skipped piece,
|
|
264
|
+
marked deferred, with its fallback: the page's skeleton.
|
|
265
|
+
|
|
266
|
+
**An entry the browser already holds is never deferred.** Inside a section
|
|
267
|
+
whose `layout()` has a flagged `loading()`, the layout is on screen, so its
|
|
268
|
+
flag has no say: a prefetch of a sibling route renders that route's handler
|
|
269
|
+
and loaders as usual, and the click sends no second request. The same goes for
|
|
270
|
+
a same-route navigation (`/product/a` to `/product/b`). The rule behind it:
|
|
271
|
+
a click that adopts a prefetch is never worse than the same click with no
|
|
272
|
+
prefetch at all (two exceptions, under "Limits": a click before its
|
|
273
|
+
prefetch has answered, and a flagged `layout()` entered from outside). It
|
|
274
|
+
shows the next page, or its skeleton, at the click and never later than that
|
|
275
|
+
plain navigation, the router adds no wait of its own, and it never replaces
|
|
276
|
+
content that is on screen with a fallback. The deferred part arrives after
|
|
277
|
+
the click and React reveals it as it reveals any streaming content: a read
|
|
278
|
+
inside it can show its own fallback where the plain navigation showed the
|
|
279
|
+
whole page a little later. For
|
|
280
|
+
the same reason a prefetch that deferred something is used only on the page
|
|
281
|
+
that made it: a page that already shows the entry makes its own prefetch,
|
|
282
|
+
which defers nothing for it. To keep navigation _inside_ a section out of
|
|
283
|
+
prefetches, flag the `loading()` of the routes in it:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
layout(AccountLayout, () => [
|
|
287
|
+
// From outside /account: the layout and the route below wait together.
|
|
288
|
+
loading(<AccountSkeleton />, { prefetch: false }),
|
|
289
|
+
// From another /account page: prefetched whole, the layout is on screen.
|
|
290
|
+
path("/account/profile", ProfilePage, { name: "account.profile" }),
|
|
291
|
+
// From another /account page: still deferred, by its own flag.
|
|
292
|
+
path("/account/orders", OrdersPage, { name: "account.orders" }, () => [
|
|
293
|
+
loader(OrdersLoader),
|
|
294
|
+
loading(<OrdersSkeleton />, { prefetch: false }),
|
|
295
|
+
]),
|
|
296
|
+
]),
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**What the click looks like.** The click that adopts the prefetch commits at
|
|
300
|
+
once: the URL changes, the prefetched content shows, and the fallback shows
|
|
301
|
+
where the deferred part goes. The browser sends one second request (the fill)
|
|
302
|
+
that runs only the deferred work. The page is built once, from the skeleton:
|
|
303
|
+
the fill resolves what it is waiting for, and nothing on it is mounted a
|
|
304
|
+
second time. The fill reads `cache()` and `"use cache"`
|
|
305
|
+
as any request does; the prefetch read neither for the work it skipped. A hard
|
|
306
|
+
load, a navigation with nothing prefetched, an action and back/forward run
|
|
307
|
+
everything as before.
|
|
308
|
+
|
|
309
|
+
**Under `transition()` the click commits with the fill.** A route with
|
|
310
|
+
`transition()` commits in a transition: with no prefetch, React keeps the page
|
|
311
|
+
being left until the response starts and then shows the new page with its
|
|
312
|
+
fallback. A prefetched click to a flagged `loading()` on such a route does the
|
|
313
|
+
same with its fill: the fallback shows with the fill's first chunk, as a plain
|
|
314
|
+
click's shows with its response, not at the click. The same holds when the
|
|
315
|
+
`transition()` is on something the fallback covers, such as a route under a
|
|
316
|
+
flagged layout. Nothing changes at the click: `useNavigation()` reads
|
|
317
|
+
`loading`, and the URL, the URL hooks and the page change together, fallback
|
|
318
|
+
included, when the fill starts answering. So the click is the plain click's equal
|
|
319
|
+
there, not ahead of it; what the prefetch saves is the rest of the page,
|
|
320
|
+
which is not fetched again.
|
|
321
|
+
(Shown with the click, the content could only arrive in a second commit,
|
|
322
|
+
which under a `<ViewTransition>` runs a view transition of its own and delays
|
|
323
|
+
the reveal.) `loader(Def, { prefetch: false })` on a `transition()` route is
|
|
324
|
+
not affected: the page shows with the click.
|
|
325
|
+
|
|
326
|
+
**Stored output is still served.** Under `cache()`, on a `ppr` route and for a
|
|
327
|
+
`Prerender`/`Static` handler, the handler output is stored, so the prefetch
|
|
328
|
+
looks it up, renders it and writes it exactly as before, hit or miss. Only the
|
|
329
|
+
loaders behind the fallback are deferred: for a flagged `layout()` that is
|
|
330
|
+
every loader below it, although the stored layout is served. The flag keeps
|
|
331
|
+
the block's work, its loaders included, out of prefetches. See `/caching`,
|
|
332
|
+
`/ppr` and `/prerender`.
|
|
333
|
+
|
|
334
|
+
**Limits.**
|
|
335
|
+
|
|
336
|
+
- A click before its prefetch has answered reuses that prefetch, waits for
|
|
337
|
+
it and then fetches the deferred part. The page shows no later than with no
|
|
338
|
+
prefetch; the deferred part can complete later, by up to what was left of
|
|
339
|
+
the prefetch. That is the price of keeping expensive or slow work out of
|
|
340
|
+
every prefetch, which is what the flag is for. See `/loader` →
|
|
341
|
+
"`prefetch: false`".
|
|
342
|
+
- A navigation started before the fill answers cancels it, and `navigate()`
|
|
343
|
+
resolves only once the fill has answered. A newer navigation that ends
|
|
344
|
+
without replacing the page leaves the fallback up until the next one. See
|
|
345
|
+
`/loader` → "`prefetch: false`".
|
|
346
|
+
- It needs a renderable fallback. `loading(false)` has nothing to show, so the
|
|
347
|
+
flag does nothing there. The last `loading()` on an entry wins, flag
|
|
348
|
+
included.
|
|
349
|
+
- A loader behind the fallback cannot call `ctx.rendered()`; it throws on every
|
|
350
|
+
request kind. See `/loader` → "`prefetch: false`" for this and the other
|
|
351
|
+
loader limits (`ctx.set()` values, late `redirect()`/`notFound()`, a loader a
|
|
352
|
+
handler awaits).
|
|
353
|
+
- A layout the client already holds is not deferred (its fallback would not
|
|
354
|
+
show). To keep navigation inside it out of prefetches, flag the route's own
|
|
355
|
+
`loading()`.
|
|
356
|
+
- On a `layout()` entered from a page without it, the prefetch skips the
|
|
357
|
+
layout's own handler too. This is the one place where a prefetched click
|
|
358
|
+
shows a different page from a click with no prefetch, by design: it shows
|
|
359
|
+
the skeleton at once, over the layout's whole area, and the layout appears
|
|
360
|
+
once what its own `loading()` covers has arrived (the layout, its own
|
|
361
|
+
loaders, and the components below it with no `loading()` of their own), at
|
|
362
|
+
least React's 300 ms after the skeleton. A click with no prefetch, in a
|
|
363
|
+
production build, shows the layout with its first chunk and the fallback
|
|
364
|
+
inside it. The covered content is no later with the prefetch. A loader
|
|
365
|
+
below the layout streams as with no prefetch, and what is still pending
|
|
366
|
+
shows its fallback inside the layout: a route's own `loading()`, a
|
|
367
|
+
`<Suspense>` in its content, or, for a read with no boundary of its own,
|
|
368
|
+
the layout's own fallback a second time. React keeps that fallback up at
|
|
369
|
+
least 300 ms and holds the page's other reveals as long, so content that
|
|
370
|
+
would have shown sooner shows up to 300 ms later than with no prefetch.
|
|
371
|
+
- An orphan layout's flagged `loading()` defers its loaders but not its
|
|
372
|
+
handler: the route's handler has already run by then.
|
|
373
|
+
- `clientUrls()` routes and `intercept()` ignore the flag.
|
|
374
|
+
|
|
375
|
+
To defer a single loader and keep the handler in the prefetch, use
|
|
376
|
+
`loader(Def, { prefetch: false })` instead (see `/loader`).
|
|
377
|
+
|
|
378
|
+
A `loading()` skeleton that has appeared stays at least about 300 ms before React reveals what replaces it (React's fallback throttle, [#1078](https://github.com/rangojs/rango/issues/1078)); see "Streaming with Suspense" in [loader](../loader/SKILL.md).
|
|
379
|
+
|
|
241
380
|
## Handler Data Ownership
|
|
242
381
|
|
|
243
382
|
When a route has children (orphan layouts, parallels), the handler executes
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -97,7 +97,7 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
97
97
|
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
98
98
|
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
99
99
|
| a `transition({ when })` browser predicate against `{ kind, from, to, action }` | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ applied, gatedOff, context }`) | `@rangojs/router/testing` |
|
|
100
|
-
| a `
|
|
100
|
+
| a `revalidate()` / `isAction(ref)` predicate, `urls()` or `clientUrls()` | unit (node) | [`runRevalidate`](./predicates.md) (production `evaluateRevalidation`, async) | `@rangojs/router/testing` |
|
|
101
101
|
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
|
|
102
102
|
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
103
103
|
| a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
|
|
@@ -116,6 +116,7 @@ Each primitive links to its sub-file (API + recipe + caveats).
|
|
|
116
116
|
| cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
|
|
117
117
|
| a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
|
|
118
118
|
| a form submitted before hydration: a `useActionState` result the no-JS POST renders, the form state its payload carries | RSC unit | [`createActionForm` + `serveShellRequest({ form })`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
|
|
119
|
+
| a JS server action call: the segments it re-renders, what `revalidate()` sees on it | RSC unit | [`serveShellRequest({ action })`](./serve-shell-request.md#a-server-action-called-from-the-page) | `@rangojs/router/testing/flight` |
|
|
119
120
|
| PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
|
|
120
121
|
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
121
122
|
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
@@ -147,7 +148,7 @@ beside their function):
|
|
|
147
148
|
| `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
|
|
148
149
|
| | `runMiddleware` | [`middleware.md`](./middleware.md) |
|
|
149
150
|
| | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
|
|
150
|
-
| | `runTransitionWhen`, `
|
|
151
|
+
| | `runTransitionWhen`, `runRevalidate` | [`predicates.md`](./predicates.md) |
|
|
151
152
|
| | `collectHandle` | [`handles.md`](./handles.md) |
|
|
152
153
|
| | `withLocationStateKey` | [`client-components.md`](./client-components.md) |
|
|
153
154
|
| | `dispatch` | [`response-routes.md`](./response-routes.md) |
|
|
@@ -299,7 +299,7 @@ it("hydrates as the server rendered it, then shows the stored count", async () =
|
|
|
299
299
|
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
300
300
|
| `typeof window` / `typeof document` in render | `"undefined"` in the server pass: both globals are removed for it and restored before hydration. A branch renders its server side; if the client side differs, React reports the mismatch. |
|
|
301
301
|
| An unguarded `window.x` / `document.x` in render | Throws `ReferenceError` in the server pass, as in SSR. Inside a `<Suspense>` the boundary is client-rendered and reported; outside one `renderRoute` rejects. |
|
|
302
|
-
| `
|
|
302
|
+
| Hydration adopts the server HTML | `hydrateRoot` adopts the server-rendered DOM nodes instead of replacing them, and `recoverableErrors` stays empty; a client render of the same tree would replace the nodes. |
|
|
303
303
|
| A text or element mismatch | React re-renders that tree on the client and reports it: `recoverableErrors` gets `"Hydration failed because ..."` with the diff. |
|
|
304
304
|
| An attribute-only mismatch (`className`, `href`) | NOT in `recoverableErrors`. React keeps the server attribute and logs `console.error` in development; assert it with `vi.spyOn(console, "error")`. |
|
|
305
305
|
| `<Suspense>` whose content renders synchronously | In `serverHtml`. It hydrates in a later pass than the tree above it, after that tree's effects, before `renderRoute` resolves. |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Testing navigation predicates — runTransitionWhen,
|
|
1
|
+
# Testing navigation predicates — runTransitionWhen, runRevalidate
|
|
2
2
|
|
|
3
|
-
**Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `transition({ when })` (see `/view-transitions`) and `
|
|
3
|
+
**Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `transition({ when })` (see `/view-transitions`) and `revalidate()` in `urls()` and in `clientUrls()` (see `/client-urls`)
|
|
4
4
|
|
|
5
|
-
Both primitives
|
|
5
|
+
Both primitives 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 loader re-run?". `runTransitionWhen` is synchronous; `runRevalidate` returns a Promise. A `transition({ when })` predicate runs in the browser at runtime; a `revalidate()` predicate runs on the server, a `clientUrls()` loader's included (the build moves it there).
|
|
6
6
|
|
|
7
7
|
## runTransitionWhen(when | config, opts?)
|
|
8
8
|
|
|
@@ -12,12 +12,12 @@ Builds the `TransitionWhenContext` the browser builds and evaluates the predicat
|
|
|
12
12
|
|
|
13
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
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 `
|
|
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 `runRevalidate`, an imported action outside a built app needs its id passed as the string. |
|
|
21
21
|
|
|
22
22
|
### Returns — `RunTransitionWhenResult`
|
|
23
23
|
|
|
@@ -62,57 +62,92 @@ it("animates unless the Link pushed { animate: false }", () => {
|
|
|
62
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
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
64
|
|
|
65
|
-
##
|
|
65
|
+
## runRevalidate(fn | fn[], opts?)
|
|
66
66
|
|
|
67
|
-
Evaluates one `
|
|
67
|
+
Evaluates one `revalidate()` predicate, or a chain in declaration order, through the router's production `evaluateRevalidation` inside a seeded request context. It serves `urls()` and `clientUrls()` predicates alike: a `clientUrls()` loader's predicate runs on the server with the `urls()` arguments, so the argument object (`currentParams`, `currentUrl`, `nextParams`, `nextUrl`, `defaultShouldRevalidate`, `context`, `segmentType`, `actionId`, `isAction`, `actionUrl`, `actionResult`, `formData`, `method`, `routeName`/`fromRouteName`/`toRouteName`, `stale`) is the production one. Returns `Promise<boolean>`: the final decision (true re-runs the segment). The locked default, boolean short-circuit and soft verdicts are the production code paths. A throwing predicate fails open to the default (logged with `console.error`); a thrown `Response` is rethrown.
|
|
68
68
|
|
|
69
|
-
### Options — `
|
|
69
|
+
### Options — `RunRevalidateOptions`
|
|
70
70
|
|
|
71
|
-
Defaults model a same-URL navigation with no action
|
|
71
|
+
Defaults model a same-URL navigation with no action, for a route-owned loader: a predicate that defers resolves `false`, and with an `action` it resolves `true`.
|
|
72
72
|
|
|
73
|
-
| Field
|
|
74
|
-
|
|
|
75
|
-
| `currentUrl`
|
|
76
|
-
| `nextUrl`
|
|
77
|
-
| `currentParams`
|
|
78
|
-
| `nextParams`
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
73
|
+
| Field | Type | Meaning |
|
|
74
|
+
| ---------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `currentUrl` | `string \| URL` | The page being left. Defaults to `http://localhost/`. |
|
|
76
|
+
| `nextUrl` | `string \| URL` | The destination. Defaults to `currentUrl`. |
|
|
77
|
+
| `currentParams` | `Record<string, string>` | Source params. Defaults to `{}`. |
|
|
78
|
+
| `nextParams` | `Record<string, string>` | Target params. Defaults to `currentParams`. |
|
|
79
|
+
| `action` | imported action \| `string` \| `{ ref, result, formData, url }` | The triggering action. `ref` is an imported server action (its id is read from `$id ?? $$id`) or an action id string; `result` is `actionResult`, `formData` is `formData`, `url` is `actionUrl`. Makes the request a POST. |
|
|
80
|
+
| `method` | `string` | Overrides the request method (`POST` with an `action`, otherwise `GET`). |
|
|
81
|
+
| `segmentType` | `"layout" \| "route" \| "parallel" \| "loader"` | The segment type the predicate is attached to. Defaults to `"loader"`. |
|
|
82
|
+
| `belongsToRoute` | `boolean` | Defaults to `true`; `false` models a layout's loader, which a navigation keeps by default. |
|
|
83
|
+
| `stale` | `boolean` | The `stale` arg (a history-entry background revalidation). Defaults to `false`. |
|
|
84
|
+
| `routeName` | `string` | `toRouteName` (and `routeName`). |
|
|
85
|
+
| `fromRouteName` | `string` | `fromRouteName`. |
|
|
86
|
+
| `env` | `TEnv` | Environment bindings of the seeded context. |
|
|
87
|
+
| `vars` | `VarsInit` | Variables a middleware would have set, readable with `context.get()`. |
|
|
88
|
+
| `context` | the predicate's `context` | Replaces the seeded context. |
|
|
82
89
|
|
|
83
|
-
Outside a built app, an imported action carries no id
|
|
90
|
+
Outside a built app, an imported action carries no `$id`: pass the id string your predicate matches (for example `"src/actions/cart.ts#addToCart"`), or stamp `$id` on the imported function in the test (`Object.assign(addToCart, { $id: "src/actions/cart.ts#addToCart" })`); `runRevalidate` throws a clear error otherwise.
|
|
84
91
|
|
|
85
92
|
### Recipe
|
|
86
93
|
|
|
94
|
+
The build moves a `clientUrls()` predicate to the server and accepts only imports and globals in it, so keep a predicate you want to test in its own module without `"use client"` and import it both in the group and here. Vitest does not run the plugin, so there the predicate is a plain function. To run it through the real handler instead, mount the group in the Flight project with `rangoUseClientTransform()` wired, and send a `serveShellRequest` navigation or `action` request ([`./serve-shell-request.md`](./serve-shell-request.md)).
|
|
95
|
+
|
|
87
96
|
```ts
|
|
88
|
-
|
|
89
|
-
import {
|
|
90
|
-
import
|
|
97
|
+
// src/predicates.ts — no "use client"
|
|
98
|
+
import type { ShouldRevalidateFn } from "@rangojs/router";
|
|
99
|
+
import { checkout } from "./actions/cart.js";
|
|
91
100
|
|
|
92
|
-
const
|
|
101
|
+
export const onTabChange: ShouldRevalidateFn = ({ currentUrl, nextUrl }) =>
|
|
93
102
|
currentUrl.searchParams.get("tab") !== nextUrl.searchParams.get("tab");
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
export const onCheckoutOk: ShouldRevalidateFn = (ctx) =>
|
|
105
|
+
ctx.isAction(checkout) ? ctx.actionResult?.ok === true : undefined;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { it, expect } from "vitest";
|
|
110
|
+
import { runRevalidate } from "@rangojs/router/testing";
|
|
111
|
+
import { checkout } from "../src/actions/cart.js";
|
|
112
|
+
import { onTabChange, onCheckoutOk } from "../src/predicates.js";
|
|
113
|
+
|
|
114
|
+
// The build stamps a "use server" export with its id; Vitest does not.
|
|
115
|
+
Object.assign(checkout, { $id: "src/actions/cart.ts#checkout" });
|
|
116
|
+
|
|
117
|
+
it("revalidates only when the tab changes", async () => {
|
|
96
118
|
expect(
|
|
97
|
-
|
|
119
|
+
await runRevalidate(onTabChange, {
|
|
98
120
|
currentUrl: "/settings?tab=profile",
|
|
99
121
|
nextUrl: "/settings?tab=billing",
|
|
100
122
|
}),
|
|
101
123
|
).toBe(true);
|
|
102
124
|
expect(
|
|
103
|
-
|
|
125
|
+
await runRevalidate(onTabChange, {
|
|
104
126
|
currentUrl: "/settings?tab=profile",
|
|
105
127
|
nextUrl: "/settings?tab=profile&sort=asc",
|
|
106
128
|
}),
|
|
107
129
|
).toBe(false);
|
|
108
130
|
});
|
|
131
|
+
|
|
132
|
+
it("re-runs after checkout only when it succeeded", async () => {
|
|
133
|
+
expect(
|
|
134
|
+
await runRevalidate(onCheckoutOk, {
|
|
135
|
+
action: { ref: checkout, result: { ok: true } },
|
|
136
|
+
}),
|
|
137
|
+
).toBe(true);
|
|
138
|
+
expect(
|
|
139
|
+
await runRevalidate(onCheckoutOk, {
|
|
140
|
+
action: { ref: checkout, result: { ok: false } },
|
|
141
|
+
}),
|
|
142
|
+
).toBe(false);
|
|
143
|
+
});
|
|
109
144
|
```
|
|
110
145
|
|
|
111
146
|
## Caveats
|
|
112
147
|
|
|
113
|
-
-
|
|
148
|
+
- `runRevalidate` proves the predicate's decision, not the build: the hoist's rules (imports and globals only, no browser globals) are checked by the Vite plugin at dev startup and build, not in Vitest. Assert the post-action result of a real group at e2e.
|
|
114
149
|
- `runTransitionWhen` proves the decision only. Whether the browser actually runs a view transition is e2e territory (see `./e2e-parity.md`).
|
|
115
|
-
-
|
|
150
|
+
- `runTransitionWhen` is synchronous. `runRevalidate` returns a Promise because it awaits the router's `evaluateRevalidation`, but a predicate itself must be synchronous: one returning a Promise is a bug in the predicate (the router keeps the default and warns in development), and the `clientUrls()` build rejects an `async` one.
|
|
116
151
|
|
|
117
152
|
## See also
|
|
118
153
|
|