@rangojs/router 0.24.4 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
package/skills/ppr/SKILL.md
CHANGED
|
@@ -151,12 +151,42 @@ cache miss (no shell).
|
|
|
151
151
|
|
|
152
152
|
### A request middleware rewrote
|
|
153
153
|
|
|
154
|
-
A
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
A URL's shared shell is read and written only by requests on that URL. A
|
|
155
|
+
request middleware rewrote (`throw rewrite()`, `/middleware`) to a `ppr` route
|
|
156
|
+
neither reads the target's shell nor captures it: it renders live, at the
|
|
157
|
+
visitor's location (`usePathname()`, `ctx.rewrittenFrom`), on every request.
|
|
158
|
+
The target's shell is captured by a direct visit or a `router.prerender()` warm
|
|
159
|
+
of the target's URL, so nothing a rewriting middleware set (a variable, the
|
|
160
|
+
visitor's URL) ends up in the shell every direct visitor gets.
|
|
161
|
+
|
|
162
|
+
The rewrite chooses a shell for the rewritten request with `.shell()`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// A routing catch-all: each public URL gets a shell of its own.
|
|
166
|
+
throw rewrite("item", { id }).shell("visitor");
|
|
167
|
+
|
|
168
|
+
// A lock or promo page that renders nothing of the visitor's URL. A lock
|
|
169
|
+
// also refuses mutations, which a rewrite otherwise moves to the target.
|
|
170
|
+
throw rewrite("maintenance").shell("target").disallowMutations();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- `.shell("visitor")` keys the shell by the visitor's URL and the target
|
|
174
|
+
(`visitorShellCacheKey(router, "/summer", "/item/1")` in a test; the target
|
|
175
|
+
route's `cache.searchParams` filter and its own partition apply). The
|
|
176
|
+
capture renders the visitor's location and `ctx.rewrittenFrom`, and a
|
|
177
|
+
variable set before the throw may be baked in, since the key covers the URL
|
|
178
|
+
it came from. Navigation replay uses the same key. A build shell of the
|
|
179
|
+
target is never served to it. Store growth is one shell per public URL;
|
|
180
|
+
`updateTag()` reaches every one of them through the shell's tags.
|
|
181
|
+
- `.shell("target")` reads and captures the target's shared shell from the
|
|
182
|
+
rewritten request. The shell then shows the target's URL to a rewritten
|
|
183
|
+
visitor on a HIT (`usePathname()` hydrates against the address bar, so a
|
|
184
|
+
component that renders it mismatches), and a variable set before the throw
|
|
185
|
+
is baked into the shell direct visitors get. Use it only for a page that
|
|
186
|
+
renders neither.
|
|
187
|
+
|
|
188
|
+
A rewritten navigation without `.shell()` reports
|
|
189
|
+
`x-rango-ppr-replay: BYPASS; reason=rewritten`.
|
|
160
190
|
|
|
161
191
|
### Request-partitioned shells
|
|
162
192
|
|
|
@@ -1299,7 +1329,10 @@ shell-cache only public/shared pages, keep per-user content in live-lane
|
|
|
1299
1329
|
loaders (no `ssr: false`) or nested promises in bake-lane loader data — NOT in
|
|
1300
1330
|
handler output or plain bake-lane container data — mark the variable
|
|
1301
1331
|
`{ cache: false }` so a stray shell read refuses the capture, or key per
|
|
1302
|
-
variant at the CDN tier.
|
|
1332
|
+
variant at the CDN tier. A variable a middleware sets before `throw rewrite()`
|
|
1333
|
+
never reaches the target's shared shell: a rewritten request captures none
|
|
1334
|
+
unless its rewrite opts in with `.shell("target")` (see "A request middleware
|
|
1335
|
+
rewrote"); with `.shell("visitor")` it lands only in that URL's own shell.
|
|
1303
1336
|
|
|
1304
1337
|
## What always renders without a shell
|
|
1305
1338
|
|
|
@@ -1307,7 +1340,9 @@ Non-GET, non-partial RSC/action/loader fetches, partial requests without an
|
|
|
1307
1340
|
eligible captured segment snapshot, per-request CSP nonce, `streamMode:
|
|
1308
1341
|
"allReady"`, redirects, 404s, error renders, routes without `ppr`, a document
|
|
1309
1342
|
request the route's own `cache()` scope refuses (`cache(false)`, or a
|
|
1310
|
-
`condition()` returning false for it),
|
|
1343
|
+
`condition()` returning false for it), a request middleware rewrote whose
|
|
1344
|
+
rewrite chose no shell (`rewrite(...).shell()`), and any store without the
|
|
1345
|
+
shell family.
|
|
1311
1346
|
A stored shell is invalidated when
|
|
1312
1347
|
`React.version` changes (postponed state is build-coupled) or when the router's
|
|
1313
1348
|
document version changes, so a deploy that changes the router self-heals via
|
|
@@ -1020,8 +1020,7 @@ in dev. A removed page is the exception: its marker answers 404 in dev too.
|
|
|
1020
1020
|
that only a refresh produced.
|
|
1021
1021
|
- **Connected clients can lag.** A refresh has no response to reach open tabs:
|
|
1022
1022
|
a client that prefetched the page keeps its copy for `prefetchCacheTTL`
|
|
1023
|
-
(default 300s)
|
|
1024
|
-
them if the window matters.
|
|
1023
|
+
(default 300s, in memory only). Lower it if the window matters.
|
|
1025
1024
|
- **Entries are per deploy.** Keys carry the router's data version, so a deploy
|
|
1026
1025
|
that changes server code starts from the build entries again; a client-only
|
|
1027
1026
|
deploy keeps refreshed pages.
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -111,6 +111,7 @@ stated, greppable contract.
|
|
|
111
111
|
| debug a slow request | `debugPerformance` / telemetry | /observability |
|
|
112
112
|
| share config across routes | factory returning a helper array | /composability |
|
|
113
113
|
| compose a sub-app / module | `include()` | /route |
|
|
114
|
+
| lay out a whole app | section modules, guards, groups | /app-structure |
|
|
114
115
|
| modal / soft navigation | `intercept()` | /intercept |
|
|
115
116
|
| route group of client components | `clientUrls()` in `"use client"` | /client-urls |
|
|
116
117
|
| set meta/breadcrumbs from loader data | `ctx.use(Handle)` in the loader | /loader |
|
|
@@ -306,8 +307,10 @@ Grouped by concern — read when you need to…
|
|
|
306
307
|
| Skill | Description |
|
|
307
308
|
| ------------------------- | -------------------------------------------------------------------------- |
|
|
308
309
|
| `/router-setup` | Create and configure the RSC router |
|
|
310
|
+
| `/app-structure` | Whole-app recipes: large app with admin, multi-app, content site, shop |
|
|
309
311
|
| `/route` | Define routes with `urls()`, `path()`, and `include()` |
|
|
310
312
|
| `/client-urls` | Client-component route groups with `clientUrls()` — no handlers |
|
|
313
|
+
| `/ux-architecture` | Recipes for fast, stable navigation with `urls()` and `clientUrls()` |
|
|
311
314
|
| `/layout` | Layouts that wrap child routes |
|
|
312
315
|
| `/parallel` | Multi-column layouts and sidebars |
|
|
313
316
|
| `/intercept` | Modal/slide-over patterns for soft navigation |
|
|
@@ -617,7 +617,10 @@ path.json(
|
|
|
617
617
|
|
|
618
618
|
A dot-local name (`ctx.reverse(".comments")`) is a type error here. The
|
|
619
619
|
runtime reverse has no scope to resolve it against, so untyped (JS) code that
|
|
620
|
-
calls it throws `Unknown route` and the route answers 500.
|
|
620
|
+
calls it throws `Unknown route` and the route answers 500. Leaving out a
|
|
621
|
+
required param (`ctx.reverse("blog.api.comments")`) throws
|
|
622
|
+
`Missing param "slug" for route "blog.api.comments"`, with or without a params
|
|
623
|
+
object; it never returns the `/:slug` pattern as a URL.
|
|
621
624
|
|
|
622
625
|
## Content Negotiation
|
|
623
626
|
|
package/skills/scripts/SKILL.md
CHANGED
|
@@ -133,6 +133,17 @@ re-pushed inline script. The GTM demo does exactly this: a root-layout `Script`
|
|
|
133
133
|
bootstrap fires the first page_view on document load, and a `"use client"`
|
|
134
134
|
`<GtmPageViews>` component fires a page_view on every subsequent soft navigation.
|
|
135
135
|
|
|
136
|
+
**Key that component on a handle value that carries the page's path, not on the
|
|
137
|
+
URL.** Handle values arrive on their own channel, by design: on a page React
|
|
138
|
+
holds in a transition (`transition()`, a same-route search change, a prefetched
|
|
139
|
+
click) the URL changes before the destination's `content_group` and title land,
|
|
140
|
+
so an effect keyed on `usePathname()` sends the previous page's tagging. Push
|
|
141
|
+
the navigation's `pathname + search` into your `Gtm` handle (collected as `page.path`) from a root
|
|
142
|
+
`parallel()` slot with `revalidate(true)`, read the tagging with `useHandle(Gtm)`,
|
|
143
|
+
and send the page_view when `page.path` differs from the last path sent (seed it
|
|
144
|
+
with the landing path, which the bootstrap already sent). The pattern, its
|
|
145
|
+
trade-offs and the code are in [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design).
|
|
146
|
+
|
|
136
147
|
## The inline-self-inject rule (GTM/GA4/Segment)
|
|
137
148
|
|
|
138
149
|
If an inline bootstrap must run **before** an external loader, do NOT push the
|
|
@@ -202,6 +213,6 @@ warns once per key. See `/ppr` → "What always renders without a shell".
|
|
|
202
213
|
- **Partytown / web-worker offloading** — push the worker config with
|
|
203
214
|
`type: "text/partytown"` and wire Partytown's own nonce config manually.
|
|
204
215
|
|
|
205
|
-
A full GTM + GA4-style integration (page_view on first render + soft nav, nonce,
|
|
216
|
+
A full GTM + GA4-style integration (page_view on first render + soft nav keyed on a path-carrying handle value, nonce,
|
|
206
217
|
ecommerce events) lives in the router repository's `tests/vite-rsc-demo` app
|
|
207
218
|
(not shipped in this package).
|
|
@@ -446,8 +446,8 @@ import * as CartActions from "./actions/cart";
|
|
|
446
446
|
|
|
447
447
|
urls(({ path, loader, revalidate }) => [
|
|
448
448
|
path("/", HomePage, { name: "home" }, () => [
|
|
449
|
-
// Loader data re-runs by default after any action. Opt out with revalidate(
|
|
450
|
-
loader(StaticHomepageLoader, () => [revalidate(
|
|
449
|
+
// Loader data re-runs by default after any action. Opt out with revalidate(false).
|
|
450
|
+
loader(StaticHomepageLoader, () => [revalidate(false)]),
|
|
451
451
|
]),
|
|
452
452
|
|
|
453
453
|
// Re-render the cart page handler AND re-resolve its loader only after cart
|
|
@@ -603,6 +603,12 @@ Redirects from actions render the **target** route tree's matched segments
|
|
|
603
603
|
source page's — the target is what the user sees next. See `/hooks
|
|
604
604
|
useLocationState` for reading flash state on the target page.
|
|
605
605
|
|
|
606
|
+
Build the target by name rather than a literal path:
|
|
607
|
+
`throw redirect(getRequestContext().reverse("account.settings"))`. It fills
|
|
608
|
+
mount params (`:accountId`, `:locale`) from the page URL the action was called
|
|
609
|
+
on, so the module that owns the action does not need to know its mount
|
|
610
|
+
(`/links`).
|
|
611
|
+
|
|
606
612
|
### Same-origin by default (open-redirect protection)
|
|
607
613
|
|
|
608
614
|
`redirect()` is same-origin by default on every path — JS, no-JS PE, and
|
|
@@ -624,6 +630,28 @@ model). Omit it and off-host targets stay blocked. `external` only waives the
|
|
|
624
630
|
`javascript:` or `data:` URL is still neutralized, so a forged or mistaken
|
|
625
631
|
`external` target can never become a scriptable navigation.
|
|
626
632
|
|
|
633
|
+
## Actions on a Rewritten Page
|
|
634
|
+
|
|
635
|
+
An action cannot `throw rewrite()`; use `redirect()`. But a page that
|
|
636
|
+
middleware reached through `throw rewrite()` can call actions. The action
|
|
637
|
+
(JS or a no-JS form post) is posted to the visitor's URL, so the same
|
|
638
|
+
middleware sees it:
|
|
639
|
+
|
|
640
|
+
- A rewrite in **route middleware** runs after the action, around the
|
|
641
|
+
re-render, which route middleware sees as the page's GET, so a rewrite
|
|
642
|
+
written for GETs only fires too. The action runs once, then the re-render
|
|
643
|
+
runs as the target: its global and route middleware, its handlers and
|
|
644
|
+
loaders. The page on screen is revalidated in place, as at the target's
|
|
645
|
+
own URL.
|
|
646
|
+
- A rewrite in **global middleware** runs before the action and moves the
|
|
647
|
+
mutation to the target first: the target's global middleware, then the
|
|
648
|
+
action, once, then the target's route middleware and render, as a direct
|
|
649
|
+
post to the target. A page a routing rewrite showed (a multi-tenant host,
|
|
650
|
+
an A/B split) keeps its forms working, in JS and no-JS. A lock refuses
|
|
651
|
+
mutations instead: `throw rewrite("maintenance").disallowMutations()` is an
|
|
652
|
+
error before the action runs, or return a `Response` for mutations. See
|
|
653
|
+
`/middleware` ("Mutations: where the middleware sits decides").
|
|
654
|
+
|
|
627
655
|
## Error Handling
|
|
628
656
|
|
|
629
657
|
### Validation errors — return them as state
|
|
@@ -688,24 +716,36 @@ For a recoverable "already gone" case, return a result the form can show
|
|
|
688
716
|
### A loader that fails on the revalidation render
|
|
689
717
|
|
|
690
718
|
A loader that throws while it reruns after the action does not undo the
|
|
691
|
-
action, which has already run and returned.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
React error boundary
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
error
|
|
701
|
-
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
719
|
+
action, which has already run and returned. It behaves as on a navigation (a
|
|
720
|
+
plain or fully prefetched click, Back/Forward), on every route, called
|
|
721
|
+
directly or from `<form action>`, `useActionState` or `useTransition`:
|
|
722
|
+
|
|
723
|
+
- The call resolves with the action's result.
|
|
724
|
+
- The loader throws in place, at its `useLoader` read, and the nearest
|
|
725
|
+
boundary to the read catches it: your own React error boundary around the
|
|
726
|
+
read, else the route's `errorBoundary()`, else the router's root one.
|
|
727
|
+
Everything outside that boundary keeps its React state; loaders that reran
|
|
728
|
+
without error commit their new data.
|
|
729
|
+
- The error goes to `onError` on the server (phase `"loader"`) and to
|
|
730
|
+
`useAction(action).revalidationError` in the browser (`error` stays `null`,
|
|
731
|
+
`result` is set).
|
|
732
|
+
- A later rerun that succeeds clears an `errorBoundary()` fallback. Your own
|
|
733
|
+
boundary resets by its own rule: key it on data that reruns with the loader
|
|
734
|
+
and is read outside the boundary.
|
|
735
|
+
|
|
736
|
+
`notFound()` and `redirect()` from the loader keep their path: a `redirect()`
|
|
737
|
+
replaces the route's content and navigates, and the layouts keep their
|
|
738
|
+
instance. Every refetch the router runs for an action follows the same rule
|
|
739
|
+
(after several actions at once, after a navigation during it), and the action
|
|
740
|
+
that ran it reports the loader's error as its `revalidationError`. Not
|
|
741
|
+
covered yet: an `intercept()`'s loaders, and a parallel slot's loaders under
|
|
742
|
+
its own `loading()`. For a failure the user should see in place of the
|
|
743
|
+
data without an error boundary, return it from the loader as data
|
|
744
|
+
(`{ ok: false, reason }`) and render it where the data goes. A failure
|
|
745
|
+
returned as data is a value like any other, so a `cache()` on the loader (or
|
|
746
|
+
a `ppr` bake, or a prerender) stores and serves it; a thrown error is never
|
|
747
|
+
stored. See `/client-urls` → "A loader that fails on its rerun" for the
|
|
748
|
+
details and examples.
|
|
709
749
|
|
|
710
750
|
### Authorization in actions
|
|
711
751
|
|
package/skills/testing/SKILL.md
CHANGED
|
@@ -155,6 +155,7 @@ beside their function):
|
|
|
155
155
|
| | `setBuildVersions` | [`serve-shell-request.md`](./serve-shell-request.md) |
|
|
156
156
|
| | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
157
157
|
| | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
|
|
158
|
+
| | `visitorShellCacheKey` | [`serve-shell-request.md`](./serve-shell-request.md) |
|
|
158
159
|
| | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
|
|
159
160
|
| `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
|
|
160
161
|
| `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
| `useReverse` | Reverse a name->pattern map to a URL; merges `useParams()` and the `mount`/`basename` prefix. |
|
|
39
39
|
| `useHref` | Resolve an href against the mount/basename. |
|
|
40
40
|
| `useMount` | The `include()` mount prefix (`options.mount`), else `"/"`. |
|
|
41
|
-
| `useNavigation` | Navigation controller state
|
|
41
|
+
| `useNavigation` | Navigation controller state: `loading` from a `navigate()` call to its commit, else `idle`. |
|
|
42
42
|
| `useRouter` | The router handle, including `.basename`. |
|
|
43
43
|
| `usePathname` | Current committed pathname. |
|
|
44
44
|
| `useSearchParams` | Search params from the `request` URL. |
|
|
@@ -376,14 +376,17 @@ it("dims the held price while the next product streams", async () => {
|
|
|
376
376
|
});
|
|
377
377
|
```
|
|
378
378
|
|
|
379
|
-
| Pending state | Under `renderRoute`
|
|
380
|
-
| ---------------------------------------------------------------------------------------------- |
|
|
381
|
-
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit.
|
|
382
|
-
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit.
|
|
383
|
-
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag.
|
|
384
|
-
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate.
|
|
385
|
-
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e.
|
|
386
|
-
| `useNavigation().state`, `useLinkStatus().pending
|
|
379
|
+
| Pending state | Under `renderRoute` |
|
|
380
|
+
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
381
|
+
| `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
|
|
382
|
+
| `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
|
|
383
|
+
| navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
|
|
384
|
+
| `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
|
|
385
|
+
| Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
|
|
386
|
+
| `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until the commit is handed to React. |
|
|
387
|
+
| `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
|
|
388
|
+
| `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
|
|
389
|
+
| A back/forward restored from the history cache (its view transition, `pop`) | NOT modeled: the tree has one location and navigate() caches no entries, so a `popstate` restores nothing. Assert it at e2e. |
|
|
387
390
|
|
|
388
391
|
- Per-navigation seeds merge over the render-time `loaders`/`loaderData` for THAT navigation only; a later `navigate()` without `loaders` falls back to the render-time data.
|
|
389
392
|
- Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
|
|
@@ -392,7 +395,7 @@ it("dims the held price while the next product streams", async () => {
|
|
|
392
395
|
## Caveats
|
|
393
396
|
|
|
394
397
|
- Client tree ONLY. Does NOT catch server/client boundary reference-identity remount bugs, real Flight serialization errors, loader execution, middleware, or handler ordering — those are `renderServerTree` / `renderHandler` / e2e territory. Loader data is SEEDED, never run. `hydrate: true` adds an HTML pass of this same client tree ([Hydration](#hydration)), not a render of your routes on a server.
|
|
395
|
-
- `router.navigate()`
|
|
398
|
+
- `router.navigate()` drives the navigation lifecycle: `useNavigation()` reads `loading` from the call and `idle` in the commit, held with it for a `transition` spec ([Held navigation](#held-navigation)). `useAction()` non-idle states (action loading/streaming, result/error) are NOT reachable — no action runs; test those at e2e.
|
|
396
399
|
- `outletPending` seeds only the production-shaped outlet context. It is useful
|
|
397
400
|
for the two settled render states of a layout that reads `useOutlet()`, but it
|
|
398
401
|
does not prove the hydrated `clientUrls()` transition that toggles the value;
|
package/skills/testing/loader.md
CHANGED
|
@@ -8,29 +8,30 @@
|
|
|
8
8
|
|
|
9
9
|
### Options — `RunLoaderOptions<TEnv>`
|
|
10
10
|
|
|
11
|
-
| Field | Type | Meaning
|
|
12
|
-
| --------------- | --------------------------------------------------------------- |
|
|
13
|
-
| `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`.
|
|
14
|
-
| `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them.
|
|
15
|
-
| `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`).
|
|
16
|
-
| `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing.
|
|
17
|
-
| `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert.
|
|
18
|
-
| `env` | `TEnv` | Environment bindings surfaced as `ctx.env`.
|
|
19
|
-
| `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET.
|
|
20
|
-
| `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle).
|
|
21
|
-
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`.
|
|
22
|
-
| `routeName` | `string` | Matched route name for scoped `.name` reverse resolution.
|
|
23
|
-
| `
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
11
|
+
| Field | Type | Meaning |
|
|
12
|
+
| --------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`. |
|
|
14
|
+
| `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them. |
|
|
15
|
+
| `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`). |
|
|
16
|
+
| `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing. |
|
|
17
|
+
| `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert. |
|
|
18
|
+
| `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
|
|
19
|
+
| `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET. |
|
|
20
|
+
| `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle). |
|
|
21
|
+
| `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
|
|
22
|
+
| `routeName` | `string` | Matched route name for scoped `.name` reverse resolution. |
|
|
23
|
+
| `declaredIn` | `string` | Include name prefix of the module whose `layout()` / `parallel()` / `cache()` declares the loader (`"account"`, `""` for the root module). `.name` then resolves in that module, as such a loader does in production, instead of from `routeName`. |
|
|
24
|
+
| `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
|
|
25
|
+
| `body` | `unknown` | Request body surfaced as `ctx.body`. |
|
|
26
|
+
| `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
|
|
27
|
+
| `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>` | Seed `ctx.use(OtherLoader)` by REFERENCE as `[[OtherLoader, data]]` tuples (same shape as `renderHandler`/`renderRoute`). Checked before `use`. |
|
|
28
|
+
| `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
|
|
29
|
+
| `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
|
|
30
|
+
| `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
|
|
31
|
+
| `cache` | `PartialCacheOptions` | Run the loader through the `cache()` a route binds it with (`loader(Loader, () => [cache({...})])` takes the same options), against `cache.store`, else `cacheStore`. See the caveat below. |
|
|
32
|
+
| `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
|
|
33
|
+
| `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
|
|
34
|
+
| `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.get(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. (Loader handle WRITES need no seed — see `runLoaderResult(...).handlePushes`.) |
|
|
34
35
|
|
|
35
36
|
### Context — `TestLoaderContext<TEnv>` (what your loader receives)
|
|
36
37
|
|
|
@@ -116,7 +117,7 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
|
|
|
116
117
|
|
|
117
118
|
## Caveats
|
|
118
119
|
|
|
119
|
-
- `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution). It does NOT fall back to the global route map.
|
|
120
|
+
- `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution, or `declaredIn` for a layout's or parallel's loader). It does NOT fall back to the global route map.
|
|
120
121
|
- `ctx.rendered()` throws by default (the render barrier only exists in a full match); pass `{ rendered: true }` to mock it for post-barrier logic, and `{ handles: [[SomeHandle, data]] }` to seed the `ctx.get(SomeHandle)` read.
|
|
121
122
|
- A loader has no `isAction()`: whether it re-runs after an action is decided by `revalidate()` on its registration, not inside the loader body. Assert post-action re-runs at e2e.
|
|
122
123
|
- Seeded `loaders` (by-reference tuples) are NOT executed — `ctx.use(OtherLoader)` returns the seeded value. The dynamic `use` resolver, by contrast, IS executed (it is a function called to compute the value). Either way the REAL loader body is not run; real loader execution and side-effects are e2e-only. `loaders` is checked before the `use` resolver.
|
|
@@ -44,17 +44,17 @@ The `ctx` your middleware reads. Notable fields:
|
|
|
44
44
|
|
|
45
45
|
### Returns — `RunMiddlewareResult<TEnv>`
|
|
46
46
|
|
|
47
|
-
| Field | Type | Meaning
|
|
48
|
-
| ----------------- | ------------------------- |
|
|
49
|
-
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit.
|
|
50
|
-
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface.
|
|
51
|
-
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through.
|
|
52
|
-
| `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName? }`, resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
|
|
53
|
-
| `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out).
|
|
54
|
-
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`.
|
|
55
|
-
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`).
|
|
56
|
-
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none).
|
|
57
|
-
| `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`.
|
|
47
|
+
| Field | Type | Meaning |
|
|
48
|
+
| ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
|
|
50
|
+
| `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
|
|
51
|
+
| `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
|
|
52
|
+
| `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName?, mutationsDisallowed? }` (`mutationsDisallowed: true` only for `.disallowMutations()`, absent otherwise), resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
|
|
53
|
+
| `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). |
|
|
54
|
+
| `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
|
|
55
|
+
| `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
|
|
56
|
+
| `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
|
|
57
|
+
| `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
|
|
58
58
|
|
|
59
59
|
## Recipe
|
|
60
60
|
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`dispatch` runs the router's REAL matching (reusing `previewMatch`) and the real global + route-level middleware chain, with no RSC render — so redirects, 404s, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
|
|
6
6
|
|
|
7
|
+
Matching goes through the route trie, as every request in `vite dev` and production does.
|
|
8
|
+
|
|
7
9
|
## API
|
|
8
10
|
|
|
9
11
|
### Options — `DispatchOptions<TEnv>`
|
|
@@ -78,7 +80,7 @@ describe("api routes via dispatch", () => {
|
|
|
78
80
|
|
|
79
81
|
`dispatch` also covers, with production code paths:
|
|
80
82
|
|
|
81
|
-
- trailing-slash
|
|
83
|
+
- trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
|
|
82
84
|
- the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
|
|
83
85
|
- content-negotiated routes (`Vary: Accept` appended);
|
|
84
86
|
- cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
|
|
@@ -90,6 +92,7 @@ describe("api routes via dispatch", () => {
|
|
|
90
92
|
- A COMPONENT route's guard stack cannot run here. Assert it at e2e, or extract the middleware fn and unit-test it with `runMiddleware` (see `./middleware.md`).
|
|
91
93
|
- JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning or throwing a `Response` (e.g. `Response.json(x)`) uses the same control-flow path. Any other thrown error takes the error's status (`RouterError.status`, else 500, or a non-200 status already set upstream in the request pipeline). On a `path.json` route the body is an RFC 9457 problem+json object `{ title, status, detail, code }` (`application/problem+json;charset=utf-8`), where `code` is the `RouterError.code`, else `"INTERNAL"`, and the `type` member is omitted. On text/html/xml/md routes the body is `text/plain`: the `RouterError` message, the error message in dev, or `"Internal Server Error"` in production. Assert the shape matching what your handler returns.
|
|
92
94
|
- Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
|
|
95
|
+
- Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
|
|
93
96
|
- A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
|
|
94
97
|
- A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
|
|
95
98
|
- A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
|