@rangojs/router 0.19.1 → 0.21.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 +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +47 -6
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +21 -2
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +24 -2
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1544 -420
- package/package.json +1 -2
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +23 -3
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +48 -6
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +40 -7
- package/src/vite/rango.ts +30 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +84 -14
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
package/skills/hooks/urls.md
CHANGED
|
@@ -47,7 +47,7 @@ function CurrentPage() {
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Returns the pathname string without search params or hash.
|
|
50
|
+
Returns the pathname string without search params or hash. It is the pathname of the page on screen: it changes when a navigation's destination commits, Back/Forward included (see [When the URL hooks change](#when-the-url-hooks-change)).
|
|
51
51
|
|
|
52
52
|
### useSearchParams()
|
|
53
53
|
|
|
@@ -109,6 +109,45 @@ Options: `replace` (default false — push), `scroll` (default true),
|
|
|
109
109
|
`revalidate` (default true; `false` skips the server fetch — legal because
|
|
110
110
|
the setter never changes the pathname).
|
|
111
111
|
|
|
112
|
+
### When the URL hooks change
|
|
113
|
+
|
|
114
|
+
`usePathname()`, `useSearchParams()` and `useParams()` describe the page on
|
|
115
|
+
screen. They change in the commit that shows a navigation's destination, not
|
|
116
|
+
when the navigation starts. That holds for Back/Forward as it does for a link
|
|
117
|
+
click or `router.push()`.
|
|
118
|
+
|
|
119
|
+
| Navigation | The hooks change |
|
|
120
|
+
| ------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
121
|
+
| `<Link>`, `router.push()` / `router.replace()` | when the destination commits |
|
|
122
|
+
| Back/Forward, entry in the client's history cache | with the restored page, in one commit |
|
|
123
|
+
| Back/Forward, entry fetched again | when the fetched page commits; until then the page being left is on screen |
|
|
124
|
+
| a Back/Forward whose fetch fails | with the error boundary: they report the entry the browser is on |
|
|
125
|
+
| `router.push(url, { revalidate: false })` | at once: nothing is fetched and the page stays |
|
|
126
|
+
| inside an optimistic `clientUrls()` destination | at the click, for that branch only (see `/client-urls`) |
|
|
127
|
+
|
|
128
|
+
**The hooks can disagree with the address bar.** On Back/Forward the browser
|
|
129
|
+
changes the URL and `history.state` before it tells the router. When the
|
|
130
|
+
entry has to be fetched (it left the client's history cache, or the cache was
|
|
131
|
+
cleared), the page being left stays on screen for the wait and the hooks keep
|
|
132
|
+
reporting its URL, while `window.location` is already the destination's. That
|
|
133
|
+
is deliberate: an active-link highlight, a breadcrumb or a filter panel
|
|
134
|
+
derived from the hooks stays in step with the content next to it. For
|
|
135
|
+
anything you render, read the hooks, not `window.location`.
|
|
136
|
+
|
|
137
|
+
For pending UI during that wait read `useNavigation()` (see
|
|
138
|
+
[`./navigation.md`](./navigation.md)): `state === "loading"` and `pendingUrl`
|
|
139
|
+
(the entry being fetched) while the request is out, `isStreaming` for the
|
|
140
|
+
whole wait. A Back/Forward served from the history cache has no wait.
|
|
141
|
+
|
|
142
|
+
`setSearchParams((prev) => ...)` builds on the same location: called during a
|
|
143
|
+
pending Back/Forward, `prev` is the search of the page on screen.
|
|
144
|
+
|
|
145
|
+
One gap (issue #1046): a component that first mounts in the page being left
|
|
146
|
+
after the navigation's response has arrived, while React still holds the
|
|
147
|
+
destination behind a loader or a `transition()`, reads the destination's URL
|
|
148
|
+
and params from these hooks. Components that were already mounted are not
|
|
149
|
+
affected. It applies to a push and to Back/Forward alike.
|
|
150
|
+
|
|
112
151
|
### useHref()
|
|
113
152
|
|
|
114
153
|
Mount-aware href for client components inside `include()` scopes:
|
|
@@ -370,3 +370,16 @@ apps; each app owns its own document.
|
|
|
370
370
|
|
|
371
371
|
**Within-app** navigation is unchanged — a normal soft SPA update (the document
|
|
372
372
|
stays mounted). Only crossing an app boundary triggers the reload.
|
|
373
|
+
|
|
374
|
+
## Deploys and the cache
|
|
375
|
+
|
|
376
|
+
Each `createRouter()` app has its own cache version, computed by `vite build` from that app's built code, so deploying a change to one app does not clear the cache of the others. Lazily mounting sub-apps (`.lazy(() => import("./apps/admin"))`) is what keeps them independent: an app's version covers everything the modules that statically import its router can run, and a lazy mount is where that stops. Routers that one module imports statically share that module's code, so a change to either moves both versions.
|
|
377
|
+
|
|
378
|
+
| Deploy | Cached data | Stored HTML | Open tabs |
|
|
379
|
+
| ------------------------------------- | ------------------- | --------------------- | ----------------------- |
|
|
380
|
+
| Rebuild, no code change | kept | kept | untouched |
|
|
381
|
+
| Server code of app A changes | cleared for A only | cleared for A only | A's tabs reload |
|
|
382
|
+
| Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
|
|
383
|
+
| Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
|
|
384
|
+
|
|
385
|
+
The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, change that app's `version` or invalidate its tags. See `/cache-guide`.
|
|
@@ -238,6 +238,12 @@ always activates. `when` is not re-evaluated during action revalidation, so
|
|
|
238
238
|
an open modal stays open after an action. A selector that throws does not
|
|
239
239
|
intercept: the full page renders and the error is logged with the route name.
|
|
240
240
|
|
|
241
|
+
The server learns `from` from the `X-RSC-Router-Client-Path` header, which the
|
|
242
|
+
rango client sends on every navigation. A partial request without it falls
|
|
243
|
+
back to `Referer`, and only when that `Referer` is on the request's own origin
|
|
244
|
+
(scheme, host and port). A request with no usable navigation context is not
|
|
245
|
+
intercepted: it gets the full page.
|
|
246
|
+
|
|
241
247
|
A prefetch of a route an intercept targets is stored per source page, whether
|
|
242
248
|
or not the intercept applies from that page (`rsc-rendering.ts`
|
|
243
249
|
`x-rsc-prefetch-scope: source`, set from `MatchResult.interceptTargeted`). A
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -612,6 +612,10 @@ included. A handler's `ctx.use(Loader)` read never caches by itself (see
|
|
|
612
612
|
body, gets the binding's value: one value per loader per request, a HIT
|
|
613
613
|
included. A reader that runs before the binding starts (a layout's loader
|
|
614
614
|
reading a route's cached loader) gets the value of its own run, as before.
|
|
615
|
+
This is by design: that loader runs live while the binding serves its entry,
|
|
616
|
+
so its data and its handle push can come from different runs on that request.
|
|
617
|
+
To keep them together, declare the cached loader first or read it from the
|
|
618
|
+
handler.
|
|
615
619
|
|
|
616
620
|
The loader's data is cached independently from the route's segment cache,
|
|
617
621
|
using the same `SegmentCacheStore` (app-level or per-loader override).
|
|
@@ -648,6 +652,14 @@ rejects) skips the whole entry, as above. Entries written
|
|
|
648
652
|
before handles were recorded replay none until they are rewritten (expiry or
|
|
649
653
|
SWR revalidation).
|
|
650
654
|
|
|
655
|
+
On a `ppr` route the shell keeps a copy of an `ssr: false` loader's settled
|
|
656
|
+
pushes too. The two never disagree on the page: where a replay of the shell
|
|
657
|
+
serves the loader's data from the shell, the shell's copy is the one shown
|
|
658
|
+
and the entry only adds the deferred push the shell could not keep; where it
|
|
659
|
+
does not (the loader runs or reads its entry), the entry's pushes take the
|
|
660
|
+
shell copy's place, none if the entry recorded none. See `/ppr` → "On a shell
|
|
661
|
+
HIT".
|
|
662
|
+
|
|
651
663
|
### Cache Key
|
|
652
664
|
|
|
653
665
|
The default cache key is `loader:{loaderId}:{host}{pathname}:{sortedParams}`
|
|
@@ -993,7 +1005,13 @@ on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
|
993
1005
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
994
1006
|
the fetched data usually does not. When it MUST be in the document, use
|
|
995
1007
|
`ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
|
|
996
|
-
they are replayed from the entry (see "Opting a Loader into Caching").
|
|
1008
|
+
they are replayed from the entry (see "Opting a Loader into Caching"). On a
|
|
1009
|
+
`ppr` route the race does not apply to a document served from its shell:
|
|
1010
|
+
there every push a loader makes on that request applies post-hydration (see
|
|
1011
|
+
`/ppr`, "Handles on a shell HIT"). A `useHandle` reader inside this loader's
|
|
1012
|
+
`loading()` or `<Suspense>` hydrates with the data its HTML was rendered
|
|
1013
|
+
from and shows a later push right after, so a push after an `await` causes
|
|
1014
|
+
no hydration mismatch there.
|
|
997
1015
|
|
|
998
1016
|
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
999
1017
|
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
@@ -426,6 +426,8 @@ export const requireAuthMiddleware: Middleware = async (ctx, next) => {
|
|
|
426
426
|
|
|
427
427
|
Read the flash on the target page with `useLocationState(FlashMessage)`. The `{ flash: true }` option makes it auto-clear after first render. See `/hooks`.
|
|
428
428
|
|
|
429
|
+
A redirect is followed on a link click, `router.push()`, and Back/Forward to an entry the client has to fetch. On Back/Forward the redirect replaces the history entry it was triggered from: Back from the target lands on the entry before the redirecting one, not on the redirecting one again.
|
|
430
|
+
|
|
429
431
|
## Authentication Middleware
|
|
430
432
|
|
|
431
433
|
```typescript
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -53,7 +53,8 @@ implements the shell family (`getShell`/`putShell`): `MemorySegmentCacheStore`
|
|
|
53
53
|
`VercelCacheStore` (Vercel Runtime Cache). A ppr route on a store without the
|
|
54
54
|
family renders normally (no shell) with a once-per-key warning. A custom store
|
|
55
55
|
returns each `ShellCacheEntry` field as `putShell` received it, including
|
|
56
|
-
`buildVersion` and `snapshot` (both required;
|
|
56
|
+
`buildVersion` (the router's document version) and `snapshot` (both required;
|
|
57
|
+
the snapshot can be empty).
|
|
57
58
|
|
|
58
59
|
```typescript
|
|
59
60
|
import { createRouter, urls } from "@rangojs/router";
|
|
@@ -250,7 +251,9 @@ under `cache()` and under the shell alike.
|
|
|
250
251
|
|
|
251
252
|
Invalidation crosses layers: `updateTag()`/`revalidateTag()` reach segment,
|
|
252
253
|
shell, loader, and item entries in the same store, and shell entries
|
|
253
|
-
additionally self-invalidate on `React.version` change
|
|
254
|
+
additionally self-invalidate on `React.version` change and when the router's
|
|
255
|
+
document version changes (server code, SSR output, client asset names, `base`
|
|
256
|
+
or Prerender payloads changed), not on every deploy.
|
|
254
257
|
|
|
255
258
|
## The serve pipeline: commit after ALL middleware
|
|
256
259
|
|
|
@@ -383,10 +386,12 @@ encountered by the shell capture.
|
|
|
383
386
|
Two more decisions are made before any shell-store read, so probes and
|
|
384
387
|
prerendered routes never spend passive `getShell` I/O:
|
|
385
388
|
|
|
386
|
-
- A partial request carrying neither `X-RSC-Router-Client-Path` nor
|
|
387
|
-
(a curl probe, a synthetic monitor) can never produce
|
|
388
|
-
reports `BYPASS; reason=no-navigation-context`.
|
|
389
|
-
|
|
389
|
+
- A partial request carrying neither `X-RSC-Router-Client-Path` nor a
|
|
390
|
+
same-origin `Referer` (a curl probe, a synthetic monitor) can never produce
|
|
391
|
+
a partial match; it reports `BYPASS; reason=no-navigation-context`. A
|
|
392
|
+
`Referer` on another origin counts as absent: only its pathname would be
|
|
393
|
+
matched, as if it were a route of this app. Alert on replay hit-rate with
|
|
394
|
+
this in mind — such probes are not cache misses.
|
|
390
395
|
- A `Prerender()` route's partial is served from the build-time prerender
|
|
391
396
|
store inside matching (a better-than-HIT outcome); it reports
|
|
392
397
|
`BYPASS; reason=prerender-store`. Its captures never record the page's
|
|
@@ -436,7 +441,7 @@ The bounded bypass tokens, grouped by when they are decided:
|
|
|
436
441
|
| Token | Decided | Meaning |
|
|
437
442
|
| ----------------------------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
438
443
|
| `method`, `dynamic`, `nonce`, `store-unavailable`, `passive-read-unsupported` | pre-read | request/route/store ineligible for replay |
|
|
439
|
-
| `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path
|
|
444
|
+
| `no-navigation-context` | pre-read | no `X-RSC-Router-Client-Path` and no same-origin `Referer`; a partial match is impossible |
|
|
440
445
|
| `prerender-store` | pre-read or match | `Prerender()` route served by its baked artifact (pre-read probe of the normal variant; reclassified post-match when the store actually served, either variant) |
|
|
441
446
|
| `intercept` | match | the navigation resolved to an intercept — replay is never armed for intercepts (they keep their normal cache path); no heal capture |
|
|
442
447
|
| `cache-disabled` | pre-read or match | `cache(false)` (pre-read, static) or `condition()` false (decided at the lookup); consumer opt-out is absolute |
|
|
@@ -896,31 +901,104 @@ value once it refreshes, while the baked container keeps the capture's. If a
|
|
|
896
901
|
hole needs a stable value, give it its own cache (`"use cache"` with a
|
|
897
902
|
profile, or a loader `cache()`); the shell never provides one.
|
|
898
903
|
|
|
899
|
-
**Handle pushes from a bake-lane loader appear once
|
|
900
|
-
|
|
904
|
+
**Handle pushes from a bake-lane loader appear once, and they match its
|
|
905
|
+
data.** A loader's title, meta or breadcrumb describes the data next to it,
|
|
906
|
+
so both come from the same run on every way a shell can be replayed: a
|
|
907
|
+
document HIT, a client navigation, a prefetch. The capture records the
|
|
908
|
+
settled, thenable-free handle pushes of every loader body it runs (the
|
|
901
909
|
prelude rendered them): the `ssr: false` loader's own, those of the loaders
|
|
902
|
-
it awaits with `ctx.use()`, and those its own `cache()` entry replays.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
910
|
+
it awaits with `ctx.use()`, and those its own `cache()` entry replays. What
|
|
911
|
+
a replay does with a loader's recorded pushes follows what it does with that
|
|
912
|
+
loader's data:
|
|
913
|
+
|
|
914
|
+
| The replay serves the loader's data from | Its settled pushes are |
|
|
915
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
916
|
+
| the shell (its pinned container) | the shell's: restored with the handler layer, and they stand. A loader that does run on the replay (a hole-carrying one) reads the store, and the settled pushes that run makes are dropped |
|
|
917
|
+
| a run on this request (a live-lane loader, or one the shell has no pin for) | that run's: they replace the shell's copies in place; a run that throws or makes no push shows none |
|
|
918
|
+
| the loader's own `cache()` entry | that entry's: the pushes it recorded, none if it recorded none |
|
|
919
|
+
|
|
920
|
+
So any handle, deduping or not, shows each value once. A pinned loader's
|
|
921
|
+
pushes are the values the prelude rendered, also for the pushes a `"use
|
|
922
|
+
cache"` hit inside its body replays. A loader the route also registers on
|
|
923
|
+
the live lane is a hole: its live run replaces its restored pushes, and those
|
|
924
|
+
of the loaders it awaits, so they show its live value, even when a bake-lane
|
|
925
|
+
loader running on the replay awaits it. On a document HIT the page hydrates
|
|
926
|
+
with the shell's copy and the hole's value takes its place after hydration
|
|
927
|
+
("Handles on a shell HIT" below). An
|
|
928
|
+
`ssr: false` loader is served from the shell only while the entry holds its
|
|
929
|
+
pin: a snapshot captured by a navigation alone carries none, and a shell over
|
|
930
|
+
`ppr.maxSnapshotBytes` drops them. There the loader runs fresh, and its
|
|
931
|
+
pushes are that run's, like its data.
|
|
932
|
+
|
|
933
|
+
A dependency the route does not register, which an `ssr: false` loader
|
|
934
|
+
awaited at capture outside any live loader, is on neither lane: its settled
|
|
935
|
+
pushes stay as the prelude rendered them, even where a live loader also
|
|
936
|
+
awaits it, while its data is fresh. (They follow the run instead once the
|
|
937
|
+
entry has lost a pin, because the loader that awaits it then runs.) To keep
|
|
938
|
+
its pushes live, declare it as its own `loader()` on the route. A push the
|
|
939
|
+
capture cannot record (a deferred push, or one holding a promise) marks every
|
|
940
|
+
loader record of the page to run: those bodies then still run on each replay,
|
|
941
|
+
in the background, and that push reaches the page (from the loader's own
|
|
942
|
+
`cache()` entry when it hits) with the run's value, on a document HIT and on
|
|
943
|
+
a client navigation alike. On a document HIT it arrives after hydration.
|
|
944
|
+
|
|
945
|
+
**Handles on a shell HIT.** A promise pushed from a loader is live and is
|
|
946
|
+
never in a shell. To have a value in the shell's HTML, push it settled
|
|
947
|
+
(await it) from the `ssr: false` loader, or push it from the handler.
|
|
948
|
+
|
|
949
|
+
Three things follow, in this order of scope:
|
|
950
|
+
|
|
951
|
+
1. **What is in a shell's HTML** changes only for promise pushes of
|
|
952
|
+
`ssr: false` loaders and of the loaders they await: `push(promise)`, or a
|
|
953
|
+
pushed value that holds a promise. Those run at capture, and the shell
|
|
954
|
+
renders no element for such a push. A loader without `ssr: false` never
|
|
955
|
+
ran at capture, so nothing of it was ever in the shell.
|
|
956
|
+
2. **On a shell HIT every loader push made during the request arrives after
|
|
957
|
+
hydration**, a live-lane loader's included. The document hydrates with
|
|
958
|
+
exactly the handle data the shell's HTML was rendered from (the handlers'
|
|
959
|
+
pushes and the settled pushes of the loaders that ran at capture), and
|
|
960
|
+
the request's own pushes follow as a normal `useHandle` update.
|
|
961
|
+
3. **`useHandle` on every document, `ppr` or not:** a reader that React is
|
|
962
|
+
hydrating reads the handle data its HTML was rendered from, wherever its
|
|
963
|
+
boundary hydrates, and takes the newer data right after. So a reader
|
|
964
|
+
inside the `loading()` or `<Suspense>` of a loader that pushes after an
|
|
965
|
+
`await` hydrates clean.
|
|
966
|
+
|
|
967
|
+
| On the HIT's request | In the HIT's HTML | The client |
|
|
968
|
+
| --------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------- |
|
|
969
|
+
| a handler's push, a pinned `ssr: false` loader's settled push | yes (the shell has it) | hydrates with it |
|
|
970
|
+
| a deferred push (`push(promise)`), or a push holding a promise, by a loader | no | gets it right after hydration |
|
|
971
|
+
| a live-lane loader's push | no | gets it right after hydration |
|
|
972
|
+
| the run of an `ssr: false` loader the shell has no pin for | the capture's push | hydrates with the capture's, then shows the run's |
|
|
973
|
+
|
|
974
|
+
A value a loader pushes as a promise is therefore in the HTML of a document
|
|
975
|
+
MISS (the render awaits it) and of a route without `ppr`, and not in the
|
|
976
|
+
HTML of a shell HIT. When a crawler or the first paint needs it, change one
|
|
977
|
+
line:
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
ctx.use(Notes)(db.stockNote(id)); // a promise: live, after hydration on a HIT
|
|
981
|
+
ctx.use(Notes)(await db.stockNote(id)); // settled: baked into the shell
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
The settled push bakes with the shell and is invalidated with it (its TTL,
|
|
985
|
+
its tags). A promise is not baked, whenever it settles: it is the shape that
|
|
986
|
+
says "per request", the same as a promise in the loader's data.
|
|
987
|
+
|
|
988
|
+
A pinned loader's pushes are exactly the ones its capture made. If its body
|
|
989
|
+
pushes a settled value only on some runs (behind a condition), a replay that
|
|
990
|
+
runs it shows the capture's answer, a push or none, never a later run's next
|
|
991
|
+
to the pinned data. A deferred push is the exception by design: it is
|
|
992
|
+
delivered on every replay.
|
|
993
|
+
|
|
994
|
+
One arrangement mixes sources by design. A loader that reads a
|
|
995
|
+
`cache()`-bound loader with `ctx.use()` before that binding starts (a loader
|
|
996
|
+
declared ahead of it, a parent layout) runs it live while the binding serves
|
|
997
|
+
its entry, so the cached loader's data and its handle push can come from
|
|
998
|
+
different runs on that request. To keep them together, declare the cached
|
|
999
|
+
loader first, or read it from the handler: a handler's `ctx.use()` is where a
|
|
1000
|
+
loader read is baked. An entry such a request writes holds no pushes, and a
|
|
1001
|
+
later HIT of it then shows none for that loader.
|
|
924
1002
|
|
|
925
1003
|
Four hard edges (each e2e/unit-pinned):
|
|
926
1004
|
|
|
@@ -1187,8 +1265,9 @@ eligible captured segment snapshot, per-request CSP nonce, `streamMode:
|
|
|
1187
1265
|
request the route's own `cache()` scope refuses (`cache(false)`, or a
|
|
1188
1266
|
`condition()` returning false for it), and any store without the shell family.
|
|
1189
1267
|
A stored shell is invalidated when
|
|
1190
|
-
`React.version` changes (postponed state is build-coupled)
|
|
1191
|
-
self-
|
|
1268
|
+
`React.version` changes (postponed state is build-coupled) or when the router's
|
|
1269
|
+
document version changes, so a deploy that changes the router self-heals via
|
|
1270
|
+
recapture, and a deploy that does not keeps its shells.
|
|
1192
1271
|
|
|
1193
1272
|
The per-request CSP nonce guarantee covers BOTH ways a nonce arrives — the
|
|
1194
1273
|
`createRouter({ nonce })` provider AND a direct `ctx.set(nonce, value)` token
|
|
@@ -1234,13 +1313,13 @@ path(
|
|
|
1234
1313
|
);
|
|
1235
1314
|
```
|
|
1236
1315
|
|
|
1237
|
-
| Field | Default | Notes
|
|
1238
|
-
| ------------------ | ------- |
|
|
1239
|
-
| `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default)
|
|
1240
|
-
| `swr` | — | stale window: serve the stale shell + background recapture
|
|
1241
|
-
| `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below
|
|
1242
|
-
| `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then read the live store, and drift is repaired client-side |
|
|
1243
|
-
| `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell
|
|
1316
|
+
| Field | Default | Notes |
|
|
1317
|
+
| ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1318
|
+
| `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
|
|
1319
|
+
| `swr` | — | stale window: serve the stale shell + background recapture |
|
|
1320
|
+
| `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
|
|
1321
|
+
| `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then run on every replay and read the live store, their handle pushes come from that run like their data, and drift from the prelude is repaired client-side |
|
|
1322
|
+
| `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
|
|
1244
1323
|
|
|
1245
1324
|
A shell never outlives the route `cache()` entry it was captured from: under
|
|
1246
1325
|
a route `cache()`, the shell is fresh no longer than that entry is (at most
|
|
@@ -359,8 +359,12 @@ first, so a runtime capture supersedes the baked entry once it exists.
|
|
|
359
359
|
A build-baked shell is not on a wall clock the way a pure runtime `ppr` shell is.
|
|
360
360
|
It serves from the first request after a deploy and keeps serving until one of:
|
|
361
361
|
|
|
362
|
-
- **a redeploy** — the
|
|
363
|
-
|
|
362
|
+
- **a redeploy** — the new build ships its own build shell manifest and
|
|
363
|
+
re-bakes. A shell stored at runtime for the route is replaced when the
|
|
364
|
+
router's document version changes, and a change of the prerendered payload
|
|
365
|
+
changes it (a Prerender payload is part of its router's document version, not
|
|
366
|
+
its data version, so cached data is kept); a React-version bump also retires
|
|
367
|
+
it;
|
|
364
368
|
- **`updateTag`** on a tag the shell carries — drops it, so the next request
|
|
365
369
|
MISSes and a runtime capture takes over.
|
|
366
370
|
|
|
@@ -697,7 +701,7 @@ emitted — wrap the route in `Passthrough()` when its segments must re-render.
|
|
|
697
701
|
`updateTag()`/`revalidateTag()` evict runtime cache entries. They do not refresh
|
|
698
702
|
a prerendered route's build-time Flight payload, which is served before the tag
|
|
699
703
|
system is consulted, and a `cacheTag()` inside a `"use cache"` function that runs
|
|
700
|
-
during the build has no runtime effect. A redeploy replaces the payload. To make
|
|
704
|
+
during the build has no runtime effect. A redeploy replaces the payload; because a Prerender payload is part of its router's document version, a deploy whose only change is new prerendered content keeps cached data and replaces stored HTML (shells, document-cache responses) for that router. To make
|
|
701
705
|
a route tag-invalidatable, serve it from the runtime cache instead (`cache()`,
|
|
702
706
|
or a `Passthrough()` live handler). A build-baked **ppr shell** is different: a
|
|
703
707
|
tag it carries does drop it (see "Freshness of a build shell").
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -157,8 +157,9 @@ stated, greppable contract.
|
|
|
157
157
|
`cacheTag(...tags)`, then `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)`
|
|
158
158
|
(background). Built-in stores index by tag.
|
|
159
159
|
- `useLoader` / `useHandle` / `useFetchLoader` are client-only.
|
|
160
|
-
- Caches are correctness-first: persistent store keys are version-segmented
|
|
161
|
-
|
|
160
|
+
- Caches are correctness-first: persistent store keys are version-segmented per
|
|
161
|
+
`createRouter()` by a hash of its built code (a deploy that does not change a
|
|
162
|
+
router keeps its cache; one that does reads under new keys), the forward/back cache is mutation-aware, and
|
|
162
163
|
`createVar({ cache: false })` throws on a **direct** read inside a `cache()`
|
|
163
164
|
boundary or a `"use cache"` function (a deliberately non-propagating guard). See `/cache-guide` →
|
|
164
165
|
"Correctness & invalidation".
|
|
@@ -422,11 +423,11 @@ npx rango generate src/urls.tsx src/api/
|
|
|
422
423
|
|
|
423
424
|
Three modes:
|
|
424
425
|
|
|
425
|
-
| Flag | Behavior
|
|
426
|
-
| ----------- |
|
|
427
|
-
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic).
|
|
428
|
-
| `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings.
|
|
429
|
-
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite
|
|
426
|
+
| Flag | Behavior |
|
|
427
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
428
|
+
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic). |
|
|
429
|
+
| `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings. |
|
|
430
|
+
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite`; `@vitejs/plugin-rsc` comes with the router). `--config <path>` picks the config. |
|
|
430
431
|
|
|
431
432
|
### Auto-detection
|
|
432
433
|
|
|
@@ -170,7 +170,7 @@ interface RangoOptions<TEnv> {
|
|
|
170
170
|
request: Request,
|
|
171
171
|
env: TEnv,
|
|
172
172
|
) => string | boolean | Promise<string | boolean>;
|
|
173
|
-
version?: string; //
|
|
173
|
+
version?: string; // Cache version and client/server version; replaces both computed versions (data and document); defaults to the versions `vite build` computes per router
|
|
174
174
|
originCheck?:
|
|
175
175
|
| boolean
|
|
176
176
|
| ((
|
|
@@ -215,6 +215,7 @@ rango({
|
|
|
215
215
|
headScripts: "preinit", // "preinit" (executing head module scripts) | "preload" (modulepreload hints only)
|
|
216
216
|
prerender: { onError: "fail" }, // "fail" | "warn" when a Prerender/Static render throws (/prerender)
|
|
217
217
|
buildEnv: false, // Build-time ctx.env for Prerender/Static handlers (/prerender)
|
|
218
|
+
encryptionKey: process.env.RANGO_ENCRYPTION_KEY, // Stable server-action encryption key (see Notes)
|
|
218
219
|
});
|
|
219
220
|
```
|
|
220
221
|
|
|
@@ -234,8 +235,14 @@ Notes:
|
|
|
234
235
|
|
|
235
236
|
- `clientChunks` default groups app client components by the directory after a
|
|
236
237
|
route root (`routes/`, `app/`, `pages/`, `features/`, `handlers/`, ...), so
|
|
237
|
-
`routes/dashboard/**` becomes chunk `app-dashboard`.
|
|
238
|
-
|
|
238
|
+
`routes/dashboard/**` becomes chunk `app-dashboard`. An `app/` directly
|
|
239
|
+
followed by another marker with a directory after it is a source root, so
|
|
240
|
+
`app/routes/product/**` is `app-product`; `app/<segment>/**`,
|
|
241
|
+
`app/components/**` and files directly in `app/routes/` keep `app-<segment>`
|
|
242
|
+
/ `app-components` / `app-routes`. Flat folders such as
|
|
243
|
+
`src/components/` stay in the shared chunk. A group is the loading unit:
|
|
244
|
+
rendering any member downloads the whole group's chunk;
|
|
245
|
+
`DEBUG=rango:chunks vite build` logs each group's size. `false` restores
|
|
239
246
|
`@vitejs/plugin-rsc`'s grouping (one client chunk per router). A function
|
|
240
247
|
`(meta) => string | undefined` names the group per module (`undefined` keeps
|
|
241
248
|
the default); `directoryClientChunks(meta)`, the default strategy, is
|
|
@@ -248,6 +255,19 @@ Notes:
|
|
|
248
255
|
renders and PPR shell capture. Unset, documents whose route has a
|
|
249
256
|
`loader(Def, { ssr: false })` raise it automatically so that content stays
|
|
250
257
|
in place; see `/loader`.
|
|
258
|
+
- `encryptionKey` is the key that encrypts inline server-action bound
|
|
259
|
+
arguments: base64-encoded 32 bytes (`openssl rand -base64 32`). Pass it as
|
|
260
|
+
`rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })`, never as a
|
|
261
|
+
literal. It is validated when `rango()` is called (invalid base64 or a wrong
|
|
262
|
+
size throws). `undefined` falls back to the `RANGO_ENCRYPTION_KEY`
|
|
263
|
+
environment variable, then to a key generated per build. The key is part of
|
|
264
|
+
the cache version of every router whose server code encrypts inline-action
|
|
265
|
+
bound arguments, so without a stable key those routers get a new version (and
|
|
266
|
+
a cleared cache) on every build; the build prints one note when that applies.
|
|
267
|
+
A router that renders no such action keeps its cache without one (`/use-cache`,
|
|
268
|
+
`/cache-guide`).
|
|
269
|
+
- `createRouter({ version })` takes effect on every preset, including `node`
|
|
270
|
+
and `vercel`. It replaces both computed cache versions with that exact value.
|
|
251
271
|
- `@rangojs/router/vite` also exports `poke()`, a dev-server plugin: type `e`
|
|
252
272
|
then Enter (or Ctrl+R where the terminal passes it through) to full-reload
|
|
253
273
|
the browser.
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -89,32 +89,35 @@ The single rule that drives everything:
|
|
|
89
89
|
|
|
90
90
|
Each primitive links to its sub-file (API + recipe + caveats).
|
|
91
91
|
|
|
92
|
-
| The behavior is… | Layer | Primitive
|
|
93
|
-
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
|
94
|
-
| a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md)
|
|
95
|
-
| one loader's data logic | unit (node) | [`runLoader`](./loader.md)
|
|
96
|
-
| a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md)
|
|
97
|
-
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md)
|
|
98
|
-
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md)
|
|
99
|
-
| a `transition({ when })` browser predicate against `{ kind, from, to, action }` | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ applied, gatedOff, context }`)
|
|
100
|
-
| a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults)
|
|
101
|
-
| a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md)
|
|
102
|
-
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md)
|
|
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)
|
|
104
|
-
| a component reading `useLocationState(Def)` after `router.push(url, { state: [Def(value)] })` | unit (DOM) | [`renderRoute` + `withLocationStateKey`](./client-components.md#location-state)
|
|
105
|
-
|
|
|
106
|
-
| a
|
|
107
|
-
|
|
|
108
|
-
| a
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
| a
|
|
112
|
-
|
|
|
113
|
-
| cache hit
|
|
114
|
-
| a `
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| a
|
|
92
|
+
| The behavior is… | Layer | Primitive | Import root |
|
|
93
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
94
|
+
| a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
|
|
95
|
+
| one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
|
|
96
|
+
| a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
|
|
97
|
+
| one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
|
|
98
|
+
| a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
|
|
99
|
+
| a `transition({ when })` browser predicate against `{ kind, from, to, action }` | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ applied, gatedOff, context }`) | `@rangojs/router/testing` |
|
|
100
|
+
| a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
|
|
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
|
+
| a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
|
|
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` |
|
|
104
|
+
| a component reading `useLocationState(Def)` after `router.push(url, { state: [Def(value)] })` | unit (DOM) | [`renderRoute` + `withLocationStateKey`](./client-components.md#location-state) | `@rangojs/router/testing[/dom]` |
|
|
105
|
+
| location state while a navigation is pending (a loader the test holds), or after a back/forward | unit (DOM) | [`navigate(url, { state, loaders })`, a `popstate` event](./client-components.md#state-while-a-navigation-is-pending) | `@rangojs/router/testing[/dom]` |
|
|
106
|
+
| a CLIENT component's hydration: its first client render against its own server HTML (`typeof window`, a late `<Suspense>`) | unit (DOM) | [`renderRoute` + `hydrate: true`](./client-components.md#hydration) | `@rangojs/router/testing/dom` |
|
|
107
|
+
| location state with `clearOnReload`: state after a refresh vs a client navigation | unit (DOM) | [`renderRoute` seeds, with and without `hydrate: true`](./client-components.md#location-state) | `@rangojs/router/testing[/dom]` |
|
|
108
|
+
| a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
|
|
109
|
+
| a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
|
|
110
|
+
| the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
|
|
111
|
+
| a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
|
|
112
|
+
| navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
|
|
113
|
+
| a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
|
|
114
|
+
| a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
|
|
115
|
+
| a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
|
|
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
|
+
| 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
|
+
| 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]` |
|
|
119
|
+
| generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
|
|
120
|
+
| a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
|
|
118
121
|
|
|
119
122
|
Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
|
|
120
123
|
`/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
|
|
@@ -147,6 +150,7 @@ beside their function):
|
|
|
147
150
|
| | `collectHandle` | [`handles.md`](./handles.md) |
|
|
148
151
|
| | `withLocationStateKey` | [`client-components.md`](./client-components.md) |
|
|
149
152
|
| | `dispatch` | [`response-routes.md`](./response-routes.md) |
|
|
153
|
+
| | `setBuildVersions` | [`serve-shell-request.md`](./serve-shell-request.md) |
|
|
150
154
|
| | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
151
155
|
| | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
152
156
|
| | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
|