@rangojs/router 0.24.3 → 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-root.d.ts +4 -1
- package/dist/types/client-urls/client-urls.d.ts +28 -2
- package/dist/types/client-urls/navigation.d.ts +6 -8
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +20 -5
- package/dist/types/client-urls/types.d.ts +35 -17
- 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 +19 -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/urls/path-helper.d.ts +5 -0
- 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 +195 -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 +119 -98
- 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-reverse.ts +4 -17
- 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-root.tsx +10 -5
- package/src/client-urls/client-urls.ts +246 -166
- package/src/client-urls/navigation.ts +45 -38
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +286 -75
- package/src/client-urls/types.ts +40 -16
- 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 +37 -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/urls/path-helper.ts +16 -3
- 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
|
@@ -59,7 +59,9 @@ only between a route handler and its child/orphan layouts and parallels (so
|
|
|
59
59
|
their latency overlaps rendering rather than blocking it. See `/loader` →
|
|
60
60
|
"Parallel and streaming".
|
|
61
61
|
|
|
62
|
-
The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
|
|
62
|
+
The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation. When the action throws, the `errorBoundary()` render is wrapped too, in both transports: the boundary reads the middleware's `ctx.set()` values and the response carries its headers.
|
|
63
|
+
|
|
64
|
+
Around the render after an action, route middleware's `ctx.request` is the page's GET: `method` is `"GET"`, with no body, and the headers are the POST's without `content-type` and `content-length`. It decides as it would for a GET of the page. Global middleware, which wraps the action, sees the POST, and so does `getRequestContext().request`. So a method check in route middleware never sees the mutation: it cannot block or answer one, since the action already ran. A header its GET branch sets also lands on the action's response. If a header must stay off mutation responses (a public `Cache-Control`), set it from global middleware.
|
|
63
65
|
|
|
64
66
|
Revalidation is still partial. Route middleware wraps the render pass that
|
|
65
67
|
does happen, but it does not force unrelated outer segments to recompute.
|
|
@@ -256,7 +258,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
|
|
|
256
258
|
ctx.header("X-Frame-Options", "DENY"); // set one response header
|
|
257
259
|
ctx.headers; // response Headers (stub before next(), the real response after)
|
|
258
260
|
ctx.setLocationState(FlashMessage({ text: "Saved" })); // history state for the client
|
|
259
|
-
ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill)
|
|
261
|
+
ctx.reverse("home"); // URL for a global route name (no ".name", no param auto-fill; a missing required param throws)
|
|
260
262
|
ctx.rewrittenFrom; // the visitor's URL when this request was rewritten, else undefined
|
|
261
263
|
ctx.isRewrite(error); // true for a thrown rewrite() (rethrow it from a try/catch)
|
|
262
264
|
|
|
@@ -442,8 +444,10 @@ import type { Middleware } from "@rangojs/router";
|
|
|
442
444
|
|
|
443
445
|
export const maintenance: Middleware = async (ctx, next) => {
|
|
444
446
|
if (!locked || ctx.pathname === "/maintenance") return next();
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
+
// A rewrite moves a server action or form post to its target, where the
|
|
448
|
+
// action runs. A lock answers mutations itself, so none runs under
|
|
449
|
+
// maintenance. HEAD rewrites like GET, as under .disallowMutations().
|
|
450
|
+
if (ctx.request.method !== "GET" && ctx.request.method !== "HEAD") {
|
|
447
451
|
return new Response("Maintenance", {
|
|
448
452
|
status: 503,
|
|
449
453
|
headers: { "Retry-After": "600" },
|
|
@@ -463,6 +467,16 @@ path("/maintenance", MaintenancePage, { name: "maintenance" }, () => [
|
|
|
463
467
|
]);
|
|
464
468
|
```
|
|
465
469
|
|
|
470
|
+
Without the `Response` branch, `.disallowMutations()` keeps a lock from
|
|
471
|
+
running mutations: a server action or form post under it is an error before
|
|
472
|
+
the action runs (a 500 the client shows as an error, reported through
|
|
473
|
+
`onError` with phase `"routing"`), and GET and HEAD rewrite as usual. The
|
|
474
|
+
`Response` above gives the caller a proper 503 instead.
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
throw rewrite("maintenance").disallowMutations();
|
|
478
|
+
```
|
|
479
|
+
|
|
466
480
|
Targets, like `ctx.reverse` with a path form:
|
|
467
481
|
|
|
468
482
|
```typescript
|
|
@@ -519,7 +533,9 @@ carries `x-rango-rewrite: <target pathname>`.
|
|
|
519
533
|
inside a `cache()` boundary or a `"use cache"` function throws (pass what
|
|
520
534
|
you need in from middleware), and a render other visitors may be served (a
|
|
521
535
|
PPR shell capture, a page the document cache stores) reads `undefined`, as a
|
|
522
|
-
direct visit does.
|
|
536
|
+
direct visit does. A shell keyed by the visitor's URL
|
|
537
|
+
(`rewrite(...).shell("visitor")`, see Caches) reads it, since its key covers
|
|
538
|
+
that URL.
|
|
523
539
|
|
|
524
540
|
**Middleware only, by design.** `rewrite()` is a middleware tool. A route
|
|
525
541
|
handler or a loader (any loader, `ssr: false` included) cannot rewrite: its
|
|
@@ -540,12 +556,59 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
540
556
|
]);
|
|
541
557
|
```
|
|
542
558
|
|
|
559
|
+
**Mutations: where the middleware sits decides.** A server action or a no-JS
|
|
560
|
+
form post from a rewritten page is posted to the visitor's URL, so the same
|
|
561
|
+
middleware sees it:
|
|
562
|
+
|
|
563
|
+
- **Global middleware** on the visitor's URL runs before the action. A
|
|
564
|
+
rewrite there moves the mutation to the target first, with its body: the
|
|
565
|
+
target's global middleware, then the action, once, then the target's
|
|
566
|
+
route middleware and render, as a direct post to the target. So a
|
|
567
|
+
routing rewrite (a multi-tenant host, an A/B split, a visitor URL with no
|
|
568
|
+
route) keeps its page's forms working, in JS and no-JS. Rewrite every
|
|
569
|
+
method: a rewrite written for GETs only leaves the mutation on the
|
|
570
|
+
visitor's URL, so the action runs on the visitor's route and its render
|
|
571
|
+
replaces the page. A lock must not run the mutation: return a `Response`
|
|
572
|
+
for mutations, or throw `rewrite(...).disallowMutations()`, which refuses
|
|
573
|
+
it with an error before the action runs. It chains with `.shell()` in
|
|
574
|
+
either order.
|
|
575
|
+
- **Route middleware** runs after the action, around the re-render, and
|
|
576
|
+
sees it as the page's GET. A routing rewrite there (A/B, URL override,
|
|
577
|
+
the `discontinued` example above) fires as on a GET, including one
|
|
578
|
+
written for GETs only (`if (ctx.request.method !== "GET") return next()`).
|
|
579
|
+
The action runs once, then the re-render runs as the target (its global
|
|
580
|
+
and route middleware, its render, or its error boundary when the action
|
|
581
|
+
threw), and the page keeps its layout and state. Route middleware cannot
|
|
582
|
+
refuse the action, so `.disallowMutations()` has no effect there: the
|
|
583
|
+
render moves to the target, dev logs a warning on each throw, and
|
|
584
|
+
production logs one `console.error`. Put a lock in global middleware.
|
|
585
|
+
- Global middleware on the target's pass sees the POST, with its headers.
|
|
586
|
+
A no-JS form post keeps its body, so a form-field check (a CSRF token
|
|
587
|
+
read from `ctx.request.clone().formData()`) passes on every pass, and so
|
|
588
|
+
does a mutation a global rewrite moved before its action, whose action
|
|
589
|
+
runs on the target's pass: do not skip a body check when
|
|
590
|
+
`ctx.rewrittenFrom` is set. Always read a clone; a rewrite after middleware
|
|
591
|
+
read the body itself is an error, since the action would get no
|
|
592
|
+
arguments. After a route-middleware rewrite, a JS action's body is gone
|
|
593
|
+
(the action read it), and that pass drops the `content-type` too, so a
|
|
594
|
+
body check gated on the content-type passes over it; that pass never runs
|
|
595
|
+
the action. Global middleware runs once per pass, as for any rewrite, so a
|
|
596
|
+
one-time token belongs in the action. An action's target must be a page
|
|
597
|
+
route: a rewrite that sends a server action or form post that already ran,
|
|
598
|
+
or a JS action before it runs, to a response route (`path.json()`) or a
|
|
599
|
+
path that redirects is an error. A plain POST a global rewrite moves
|
|
600
|
+
reaches a response route with its body, as a direct post would, and so
|
|
601
|
+
does a no-JS form post, whose action the router finds only on a page
|
|
602
|
+
route.
|
|
603
|
+
|
|
543
604
|
**Throw it, don't return it.** A returned rewrite is an error. Where a
|
|
544
605
|
rewrite cannot be honored it is an error that names the rule: a route
|
|
545
606
|
handler or a loader (see above), a server action (use `redirect()`),
|
|
546
|
-
|
|
547
|
-
rejects with the rule) or its middleware
|
|
548
|
-
|
|
607
|
+
a loader fetch (`useFetchLoader`, `load()`: it
|
|
608
|
+
rejects with the rule) or its middleware. A `.disallowMutations()` rewrite
|
|
609
|
+
from global middleware handling a non-GET, non-HEAD request (a JS action or
|
|
610
|
+
a no-JS form post) is an error too, by your choice: it reads
|
|
611
|
+
`rewrite("…") refused a mutation: .disallowMutations() was set`.
|
|
549
612
|
|
|
550
613
|
**try/catch around next().** A thrown rewrite passes through your
|
|
551
614
|
middleware's `catch`. Rethrow it:
|
|
@@ -570,12 +633,26 @@ the trail, `/a -> /b -> /a`) through `onError` with phase `"routing"`.
|
|
|
570
633
|
- The document cache keys a rewritten request by the target's URL, as a
|
|
571
634
|
direct visit. Register the rewriting middleware **before**
|
|
572
635
|
`createDocumentCacheMiddleware()`: a cache hit for the visitor's URL would
|
|
573
|
-
be served before your middleware runs. A page the document cache stores
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
`
|
|
577
|
-
|
|
578
|
-
|
|
636
|
+
be served before your middleware runs. A page the document cache stores
|
|
637
|
+
is rendered as a direct visit to the target: it carries the target's
|
|
638
|
+
pathname and `ctx.rewrittenFrom` is `undefined` there, so a component that
|
|
639
|
+
renders `usePathname()` can mismatch on hydration at the visitor's URL
|
|
640
|
+
when such a page is served to a rewritten request.
|
|
641
|
+
- A PPR shell (`ppr` target) is read and written only by requests on its
|
|
642
|
+
URL. A rewritten request renders live at the visitor's URL, and a direct
|
|
643
|
+
visit captures the target's shell, so values you set before the throw
|
|
644
|
+
never reach it. Pick a shell for the rewritten request with `.shell()` on
|
|
645
|
+
the thrown value: `"visitor"` keys a shell by the visitor's URL, captured
|
|
646
|
+
with the visitor's location and `ctx.rewrittenFrom` (one shell per public
|
|
647
|
+
URL, the right choice for a routing catch-all); `"target"` reads and
|
|
648
|
+
captures the target's shared shell, which shows the target's URL to the
|
|
649
|
+
visitor on a HIT and bakes in what the rewriting middleware set, so keep it
|
|
650
|
+
for a lock or promo page that renders neither:
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
throw rewrite("item", { id }).shell("visitor");
|
|
654
|
+
```
|
|
655
|
+
|
|
579
656
|
- The client's prefetch and history caches keep a rewritten page under the
|
|
580
657
|
visitor's URL, and nothing tells them about the rewrite: a page prefetched
|
|
581
658
|
during a lock shows the lock page for up to `prefetchCacheTTL` after the
|
|
@@ -600,10 +677,29 @@ the trail, `/a -> /b -> /a`) through `onError` with phase `"routing"`.
|
|
|
600
677
|
};
|
|
601
678
|
```
|
|
602
679
|
|
|
680
|
+
**Navigating from a rewritten page.** A navigation, prefetch or action from
|
|
681
|
+
the page diffs against the route that rendered it, as one from a direct
|
|
682
|
+
visit does: the payload carries the target (`metadata.rewriteTarget`), and
|
|
683
|
+
the browser sends it back with the next request from that page. A
|
|
684
|
+
`revalidate()` sees the target's params and URL (`currentParams.id` against
|
|
685
|
+
`nextParams.id` holds a loader on a sort change), and a navigation to the
|
|
686
|
+
page already shown re-renders nothing. A move to another visitor URL with
|
|
687
|
+
the same target (`/promo/a` to `/promo/b`, both rewritten to
|
|
688
|
+
`/promo/landing`) re-runs the route and its loaders, which may read
|
|
689
|
+
`ctx.rewrittenFrom`. The browser only names the page being left: what
|
|
690
|
+
renders still comes from the request's URL and your middleware.
|
|
691
|
+
|
|
603
692
|
**Testing.** `runMiddleware` reports a thrown rewrite as `result.rewrite`
|
|
604
|
-
(`{ pathname, search, routeName? }
|
|
693
|
+
(`{ pathname, search, routeName?, mutationsDisallowed? }`, the last `true`
|
|
694
|
+
only for `.disallowMutations()`). `serveShellRequest` takes a rewritten
|
|
605
695
|
page request through the full production pipeline (document or `partial`
|
|
606
696
|
navigation): the target's middleware, render, status and `x-rango-rewrite`.
|
|
697
|
+
Pass the page's `metadata.rewriteTarget` as `partial.rewriteTarget` (or
|
|
698
|
+
`action.rewriteTarget`) to send a request from it as the browser does.
|
|
699
|
+
With `action` or `form` (from `createActionForm`) it posts a mutation to the
|
|
700
|
+
visitor's URL, so a global rewrite's move, a `.disallowMutations()` refusal
|
|
701
|
+
(it rejects with the refusal) and a route-middleware rewrite's re-render are
|
|
702
|
+
testable too.
|
|
607
703
|
`dispatch` re-dispatches too, for response routes only: it matches the
|
|
608
704
|
visitor's URL first and throws on a component (RSC) route.
|
|
609
705
|
`renderRoute(..., { request: "/shop", rewrite: "/maintenance" })` renders
|
|
@@ -302,6 +302,17 @@ Then inspect:
|
|
|
302
302
|
first paint.
|
|
303
303
|
- the `Server-Timing` header to compare local logs with browser-network timing.
|
|
304
304
|
|
|
305
|
+
## Telling a page is a Rango app
|
|
306
|
+
|
|
307
|
+
Every page sets `window.__rango = { version }` (frozen, one key) from the
|
|
308
|
+
bundled client entry before hydration. `version` is the exact npm semver of
|
|
309
|
+
`@rangojs/router`, nothing else: no build hash, router id or config. It is for
|
|
310
|
+
devtools and support; the router never reads it and no option turns it off.
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
window.__rango; // { version: "0.24.2" }
|
|
314
|
+
```
|
|
315
|
+
|
|
305
316
|
## Zero-overhead defaults
|
|
306
317
|
|
|
307
318
|
`debugPerformance` is off by default, `telemetry` emits nothing unless a sink
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -206,9 +206,9 @@ parallel(
|
|
|
206
206
|
() => [
|
|
207
207
|
// The loader keeps its own revalidation: by default it re-runs after
|
|
208
208
|
// every action. Scope it here to stop that.
|
|
209
|
-
loader(CategoriesLoader, () => [revalidate(
|
|
209
|
+
loader(CategoriesLoader, () => [revalidate(false)]),
|
|
210
210
|
loading(<SidebarSkeleton />),
|
|
211
|
-
revalidate(
|
|
211
|
+
revalidate(false), // the slot component renders once, then is kept
|
|
212
212
|
]
|
|
213
213
|
)
|
|
214
214
|
```
|
|
@@ -430,7 +430,7 @@ boundary above it: the route's `loading()`, replacing the entire route content
|
|
|
430
430
|
with the route skeleton while one slot refetches. Either scope the slot's
|
|
431
431
|
`revalidate()` (share the route's named contract, as above) or give the slot
|
|
432
432
|
its own `loading()` so the fallback stays local. Don't reach for a bare
|
|
433
|
-
`revalidate(
|
|
433
|
+
`revalidate(false)`: the slot then never refreshes on param changes
|
|
434
434
|
either, and keeps the previous param's content on e.g. a product-to-product
|
|
435
435
|
navigation.
|
|
436
436
|
|
|
@@ -438,7 +438,7 @@ navigation.
|
|
|
438
438
|
it the first time. A slot the browser has not rendered yet has nothing to keep
|
|
439
439
|
showing, so on that first render the decision is clamped to `true` and your
|
|
440
440
|
callback's `false` is ignored; from then on it is honored and the browser keeps
|
|
441
|
-
the copy it already has. So `revalidate(
|
|
441
|
+
the copy it already has. So `revalidate(false)` means "render once, then
|
|
442
442
|
never re-render", never "don't appear".
|
|
443
443
|
|
|
444
444
|
Revalidate callbacks must be synchronous. A callback that returns a Promise is
|
|
@@ -565,7 +565,7 @@ export const shopPatterns = urls(({
|
|
|
565
565
|
{ "@sidebar": () => <CategorySidebar /> },
|
|
566
566
|
() => [
|
|
567
567
|
loader(CategoriesLoader),
|
|
568
|
-
revalidate(
|
|
568
|
+
revalidate(false),
|
|
569
569
|
]
|
|
570
570
|
),
|
|
571
571
|
|
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;
|