@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/links/SKILL.md
CHANGED
|
@@ -24,14 +24,62 @@ links to JSON/text endpoints see `/response-routes`.
|
|
|
24
24
|
|
|
25
25
|
Available on the handler context (route handlers, layouts, parallels), on
|
|
26
26
|
middleware and loader contexts, and on `getRequestContext()` inside server
|
|
27
|
-
actions.
|
|
27
|
+
actions. In loaders and in actions (after the route match) params the matched
|
|
28
|
+
route carries are optional, as in handlers: `ctx.reverse("account.settings")`
|
|
29
|
+
fills `accountId` from the matched params. In global middleware before `next()`
|
|
30
|
+
nothing is matched, so a route with a required param throws `Missing param`.
|
|
31
|
+
Resolves named routes using the full route map. This is the default
|
|
28
32
|
way to generate URLs on the server. Patterns in the map already include the
|
|
29
33
|
router `basename`, so results are basename-prefixed.
|
|
30
34
|
|
|
31
35
|
Middleware and response routes (`path.json()`, `path.text()`, …) get a
|
|
32
36
|
global-only `ctx.reverse` (`GlobalReverseFunction`): no `include()` scope and no
|
|
33
37
|
param auto-fill, so pass the fully qualified name and every param. A dot-local
|
|
34
|
-
`.name` is a type error there.
|
|
38
|
+
`.name` is a type error there. A missing required param throws
|
|
39
|
+
`Missing param`, with or without a params object. Route
|
|
40
|
+
middleware can use `getRequestContext().reverse` instead: it is rebound after
|
|
41
|
+
the match, takes `.name` and global names, and fills mount params. In global
|
|
42
|
+
middleware (`router.use`) it fills only after `next()`. See `/middleware` and
|
|
43
|
+
`/response-routes`.
|
|
44
|
+
|
|
45
|
+
A server action's `getRequestContext().reverse("global.name")` fills mount
|
|
46
|
+
params from the page URL the action was called on, so
|
|
47
|
+
`throw redirect(getRequestContext().reverse("account.settings"))` needs no
|
|
48
|
+
hardcoded path. A `.name` there resolves in that page's module; an action
|
|
49
|
+
shared by pages in different modules should use global names.
|
|
50
|
+
|
|
51
|
+
### The module never knows its mount
|
|
52
|
+
|
|
53
|
+
Inside a module, link by local name: `ctx.reverse(".x")` on the server,
|
|
54
|
+
`useReverse(routes)` with the module's own `.gen.ts` on the client. Both
|
|
55
|
+
resolve against wherever the module is mounted and fill the mount's params
|
|
56
|
+
from the current URL, so the same module can be mounted twice:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
include("/:locale", shopPatterns, { name: "shop" });
|
|
60
|
+
include("/outlet", shopPatterns, { name: "outlet" });
|
|
61
|
+
|
|
62
|
+
// in shopPatterns, on /en/... and on /outlet/...
|
|
63
|
+
ctx.reverse(".product", { slug: "b" }); // "/en/product/b" and "/outlet/product/b"
|
|
64
|
+
ctx.reverse(".product", { slug: "b", locale: "de" }); // "/de/product/b" (switch)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`/:locale?` and `/:locale` both auto-fill. `{ locale: undefined }` drops an
|
|
68
|
+
optional locale (`/product/b`) and throws `Missing param "locale"` for a
|
|
69
|
+
required one. Don't rebuild this by hand: `useParams().accountId` in a template
|
|
70
|
+
string, `ctx.params` threaded through props, or a "current account" context
|
|
71
|
+
all duplicate what the mount already knows.
|
|
72
|
+
|
|
73
|
+
- A server layout stays held across navigations, its own mount param
|
|
74
|
+
(`:projectId`) changing included: partial rendering by design (`/layout`,
|
|
75
|
+
"Layout Revalidation"). Links it rendered keep the old params. Render
|
|
76
|
+
param-dependent nav links in a client component with `useReverse` (it
|
|
77
|
+
re-renders from `useParams()`), or give the layout a `revalidate()` that
|
|
78
|
+
compares the param.
|
|
79
|
+
- A layout, a parallel slot and the loaders declared on them resolve `.name`
|
|
80
|
+
in the module that declares them, whichever route below them matched: a
|
|
81
|
+
parent module's layout links within its own module on every page (see
|
|
82
|
+
"Local names" below).
|
|
35
83
|
|
|
36
84
|
```typescript
|
|
37
85
|
import { urls } from "@rangojs/router";
|
|
@@ -47,7 +95,7 @@ export const shopPatterns = urls(({ path, layout }) => [
|
|
|
47
95
|
|
|
48
96
|
### Resolution rules
|
|
49
97
|
|
|
50
|
-
- **`.name`** — local route, resolved within the
|
|
98
|
+
- **`.name`** — local route, resolved within the `include()` scope of the module that defines the calling route, layout, parallel slot or loader
|
|
51
99
|
- **`name`** — global route, from the named-routes definition
|
|
52
100
|
|
|
53
101
|
The include site decides which global names exist:
|
|
@@ -87,6 +135,24 @@ Prefix a name with `.` to resolve it within the current `include()` scope. The r
|
|
|
87
135
|
};
|
|
88
136
|
```
|
|
89
137
|
|
|
138
|
+
The scope is the module that **defines** the code calling `ctx.reverse`. A route handler resolves from its own module. A `layout()`, a `parallel()` slot and the loaders declared on them resolve from the module where they are declared, whichever route below them matched. So a parent module's layout gives the same link on every page:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// mounted at include("/:accountId", accountPatterns, { name: "account" })
|
|
142
|
+
export const accountPatterns = urls(({ path, layout, include }) => [
|
|
143
|
+
layout(
|
|
144
|
+
(ctx) => <AccountNav settings={ctx.reverse(".settings")} />, // "/acme/settings" on every page below
|
|
145
|
+
() => [
|
|
146
|
+
path("/settings", AccountSettings, { name: "settings" }),
|
|
147
|
+
// projectPatterns also has a "settings" route
|
|
148
|
+
include("/:projectId", projectPatterns, { name: "project" }),
|
|
149
|
+
],
|
|
150
|
+
),
|
|
151
|
+
]);
|
|
152
|
+
// On /acme/web/issues, the account layout still links to "/acme/settings".
|
|
153
|
+
// The project's issues handler's ctx.reverse(".settings") gives "/acme/web/settings".
|
|
154
|
+
```
|
|
155
|
+
|
|
90
156
|
### Auto-fill of mount params
|
|
91
157
|
|
|
92
158
|
When routes are mounted via a parameterized `include()`, `ctx.reverse()` automatically fills mount params from `ctx.params`. Inner handlers don't need to pass params that are already known from the current URL match. Explicitly passed params override auto-filled values.
|
|
@@ -109,7 +175,7 @@ export const tenantPatterns = urls(({ path }) => [
|
|
|
109
175
|
]);
|
|
110
176
|
```
|
|
111
177
|
|
|
112
|
-
Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored. Because every current param is a default, drop an optional one explicitly with `undefined` or `""` — under `include("/:locale?", ...)` on `/en/...`, a bare `ctx.reverse("menu.index")` keeps `/en`, while `ctx.reverse("menu.index", { locale: undefined })` returns `/` (see `/i18n`). A missing required param throws `Missing param "slug" for route "…"
|
|
178
|
+
Auto-fill uses `{ ...ctx.params, ...hrefParams }` — current request params are defaults, explicit params win. Params not needed by the target route are silently ignored. Because every current param is a default, drop an optional one explicitly with `undefined` or `""` — under `include("/:locale?", ...)` on `/en/...`, a bare `ctx.reverse("menu.index")` keeps `/en`, while `ctx.reverse("menu.index", { locale: undefined })` returns `/` (see `/i18n`). A missing required param throws `Missing param "slug" for route "…"` (in middleware and response routes too, with or without a params object); an unknown name throws `Unknown route: "…"`.
|
|
113
179
|
|
|
114
180
|
### Global names (unprefixed)
|
|
115
181
|
|
|
@@ -213,6 +279,48 @@ function Nav() {
|
|
|
213
279
|
|
|
214
280
|
`useLoader()` requires the loader to be attached to an active route. If you need on-demand fetching instead, use `useFetchLoader()`.
|
|
215
281
|
|
|
282
|
+
A loader on a layout is held on navigation, like the layout. When its links follow a param of the layout's own mount (`:projectId`), give it a `revalidate()` that compares that param; links that never change can come from the layout's RSC as props:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
// urls/project.tsx (server): build the links where the names resolve
|
|
286
|
+
export const ProjectNavLoader = createLoader((ctx) => ({
|
|
287
|
+
issues: ctx.reverse(".issues"), // /acme/web/issues
|
|
288
|
+
settings: ctx.reverse(".settings"), // /acme/web/settings
|
|
289
|
+
account: ctx.reverse("account.settings"), // /acme/settings, another module
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
export const projectUrls = urls(({ layout, path, loader, revalidate }) => [
|
|
293
|
+
layout(
|
|
294
|
+
// From the RSC: fine for links that do not follow :projectId.
|
|
295
|
+
(ctx) => <ProjectShell accountHref={ctx.reverse("account.settings")} />,
|
|
296
|
+
() => [
|
|
297
|
+
// From a loader: held like the layout, re-run when the project changes.
|
|
298
|
+
loader(ProjectNavLoader, () => [
|
|
299
|
+
revalidate(({ currentParams, nextParams }) =>
|
|
300
|
+
currentParams.projectId !== nextParams.projectId ? true : undefined,
|
|
301
|
+
),
|
|
302
|
+
]),
|
|
303
|
+
path("/issues", Issues, { name: "issues" }),
|
|
304
|
+
path("/settings", Settings, { name: "settings" }),
|
|
305
|
+
],
|
|
306
|
+
),
|
|
307
|
+
]);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
"use client";
|
|
312
|
+
export function ProjectNav() {
|
|
313
|
+
const { data: links } = useLoader(ProjectNavLoader);
|
|
314
|
+
return (
|
|
315
|
+
<nav>
|
|
316
|
+
<Link to={links.issues}>Issues</Link>
|
|
317
|
+
<Link to={links.settings}>Settings</Link>
|
|
318
|
+
<Link to={links.account}>Account</Link>
|
|
319
|
+
</nav>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
216
324
|
3. Return from a server action:
|
|
217
325
|
|
|
218
326
|
```tsx
|
|
@@ -286,9 +394,9 @@ function ShopNav() {
|
|
|
286
394
|
}
|
|
287
395
|
```
|
|
288
396
|
|
|
289
|
-
Use `useHref()` for local navigation within a mounted module. Use the bare `href()` function for absolute paths outside the current mount.
|
|
397
|
+
Use `useHref()` for local navigation within a mounted module (`include("/shop", ...)` or `include("/tenant/:tenantId", ...)`). Use the bare `href()` function for absolute paths outside the current mount.
|
|
290
398
|
|
|
291
|
-
`useHref()`
|
|
399
|
+
Under a parameterized mount (`include("/tenant/:tenantId", ...)`, `/:locale`, `/:locale?`), `useHref()` fills the mount's params from `useParams()`, the same way `useReverse` does: at `/tenant/acme/board`, `href("/cart")` returns `/tenant/acme/cart`. A mount param missing from the current match makes the returned function throw `Missing param "tenantId" for route "/tenant/:tenantId"`; it never returns a URL with a literal `:tenantId`. Relative `useRouter().push("cart")` / `replace` / `prefetch` fill the mount from the committed route params at call time; a missing one rejects the `push`/`replace` promise and skips the `prefetch`. Only the mount is filled: the path argument is appended verbatim and is not checked against your routes, so for a link to a named route with its own params prefer `useReverse(routes)`.
|
|
292
400
|
|
|
293
401
|
## Client: useMount()
|
|
294
402
|
|
|
@@ -306,7 +414,7 @@ function MountInfo() {
|
|
|
306
414
|
}
|
|
307
415
|
```
|
|
308
416
|
|
|
309
|
-
`useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree. Nested includes return the nearest mount's full URL prefix (e.g. `/shop/admin`), and a parameterized include returns its pattern (`/tenant/:tenantId`), not the matched URL. `href(path, mount)` is the non-hook equivalent of `useHref()
|
|
417
|
+
`useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree. Nested includes return the nearest mount's full URL prefix (e.g. `/shop/admin`), and a parameterized include returns its pattern (`/tenant/:tenantId`), not the matched URL. `href(path, mount)` concatenates the mount it is given as-is; it is the non-hook equivalent of `useHref()` only under a static mount (`useHref()` also fills mount params).
|
|
310
418
|
|
|
311
419
|
## Client: useReverse(routes)
|
|
312
420
|
|
|
@@ -331,6 +439,27 @@ export function BlogNav() {
|
|
|
331
439
|
}
|
|
332
440
|
```
|
|
333
441
|
|
|
442
|
+
### One hook per module, in a userland file
|
|
443
|
+
|
|
444
|
+
Wrap the map once instead of importing it in every component:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
// src/lib/links.ts
|
|
448
|
+
"use client";
|
|
449
|
+
import { useReverse } from "@rangojs/router/client";
|
|
450
|
+
import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
451
|
+
import { routes as accountRoutes } from "../urls/account.gen.js";
|
|
452
|
+
|
|
453
|
+
export const useBlogReverse = () => useReverse(blogRoutes);
|
|
454
|
+
export const useAccountReverse = () => useReverse(accountRoutes);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Components call `useAccountReverse()`; a renamed or moved module changes one
|
|
458
|
+
file. The wrapper resolves like `useReverse`: against the nearest `include()`
|
|
459
|
+
mount, so call a module's hook from components under that module's own mount.
|
|
460
|
+
Inside a nested module it builds a wrong URL with no error (dc-t57); pass
|
|
461
|
+
cross-module links from the server there.
|
|
462
|
+
|
|
334
463
|
### How it resolves
|
|
335
464
|
|
|
336
465
|
1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
|
|
@@ -413,24 +542,29 @@ Both happen synchronously during `reverse()` — wrap calls in try/catch (or an
|
|
|
413
542
|
|
|
414
543
|
### The leading dot is optional
|
|
415
544
|
|
|
416
|
-
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server.
|
|
545
|
+
`reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. A module's `routes` map is right only inside that module's own mount: it resolves against the current mount, so another module's map gives a wrong URL with no error, and `useReverse(NamedRoutes)` is correct only at the root mount (under a mount it double-prefixes). There is no client API for links that cross modules under a nested mount (dc-t57). Build them on the server, where global names resolve fully, and pass them down:
|
|
417
546
|
|
|
418
547
|
```tsx
|
|
419
|
-
|
|
420
|
-
|
|
548
|
+
// server layout or handler
|
|
549
|
+
const links = {
|
|
550
|
+
blog: ctx.reverse("blog.index"),
|
|
551
|
+
cart: ctx.reverse("shop.cart"),
|
|
552
|
+
};
|
|
553
|
+
return <CrossNav links={links} />;
|
|
421
554
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const shop = useReverse(shopRoutes);
|
|
555
|
+
// client component
|
|
556
|
+
function CrossNav({ links }: { links: { blog: string; cart: string } }) {
|
|
425
557
|
return (
|
|
426
558
|
<nav>
|
|
427
|
-
<Link to={blog
|
|
428
|
-
<Link to={
|
|
559
|
+
<Link to={links.blog}>Blog</Link>
|
|
560
|
+
<Link to={links.cart}>Cart</Link>
|
|
429
561
|
</nav>
|
|
430
562
|
);
|
|
431
563
|
}
|
|
432
564
|
```
|
|
433
565
|
|
|
566
|
+
Loader data is the better carrier for chrome that follows a param: a layout handler is kept across param changes, while a layout loader re-runs when its `revalidate()` compares that param (a layout loader is held on navigation by default).
|
|
567
|
+
|
|
434
568
|
### Codegen
|
|
435
569
|
|
|
436
570
|
Each `urls()` module gets a sibling `.gen.ts` with the local route names and patterns, produced by `rango generate`:
|
|
@@ -512,10 +646,10 @@ a link or a whole section from prefetching at all, use the `prefetch` prop or
|
|
|
512
646
|
| Client component | `useReverse(routes)` | Local names from an imported `routes` map | Typed in-module URL generation without round-tripping the server |
|
|
513
647
|
| Client component | (URL passed as prop / loader data / action return) | Named routes | Cross-module URLs or one-off names you don't want to import |
|
|
514
648
|
| Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
|
|
515
|
-
| Client component | `useHref()` | Mount-prefixed paths | Local
|
|
649
|
+
| Client component | `useHref()` | Mount-prefixed paths | Local paths inside `include()`; fills `:param` mount segments |
|
|
516
650
|
| Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
|
|
517
651
|
|
|
518
|
-
> `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names.
|
|
652
|
+
> `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names, inside that module's own mount.
|
|
519
653
|
|
|
520
654
|
## Complete example: mounted module
|
|
521
655
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -241,7 +241,7 @@ so response-shaping belongs in middleware or the handler).
|
|
|
241
241
|
| `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
|
|
242
242
|
| `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
|
|
243
243
|
| `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
|
|
244
|
-
| `reverse` | `
|
|
244
|
+
| `reverse` | `AutofillAwareReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers; params the matched route carries are optional). |
|
|
245
245
|
|
|
246
246
|
### Example
|
|
247
247
|
|
|
@@ -266,9 +266,10 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
266
266
|
// Variables set by middleware (from Rango.Vars augmentation)
|
|
267
267
|
const user = ctx.get("user");
|
|
268
268
|
|
|
269
|
-
// Type-checked URLs for payloads. `.name` resolves within the
|
|
270
|
-
//
|
|
271
|
-
//
|
|
269
|
+
// Type-checked URLs for payloads. `.name` resolves within the include()
|
|
270
|
+
// scope of the module that declares this loader (a layout's loader in a
|
|
271
|
+
// parent module links within that module); a bare `name` resolves
|
|
272
|
+
// globally. See /links and /typesafety for scope rules and autocomplete.
|
|
272
273
|
const detailUrl = ctx.reverse(".detail", { slug });
|
|
273
274
|
|
|
274
275
|
return {
|
|
@@ -321,7 +322,7 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
321
322
|
|
|
322
323
|
// Loader with revalidation control
|
|
323
324
|
loader(RelatedProductsLoader, () => [
|
|
324
|
-
revalidate(
|
|
325
|
+
revalidate(false), // Never revalidate
|
|
325
326
|
]),
|
|
326
327
|
|
|
327
328
|
// Loader that re-runs only after cart actions. Loaders re-run after every
|
|
@@ -355,6 +356,7 @@ chain continues:
|
|
|
355
356
|
```typescript
|
|
356
357
|
// 1) Hard decision — short-circuits the chain, used as the final answer.
|
|
357
358
|
revalidate(() => true);
|
|
359
|
+
revalidate(true); // shorthand for () => true; revalidate(false) for () => false
|
|
358
360
|
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
359
361
|
|
|
360
362
|
// 2) Soft decision — updates the running suggestion for downstream
|
|
@@ -377,6 +379,14 @@ revalidate(() => null); // explicit defer
|
|
|
377
379
|
If every revalidator on a segment defers, the segment default is used
|
|
378
380
|
(`defaultShouldRevalidate`, see the table below).
|
|
379
381
|
|
|
382
|
+
`revalidate(true)` and `revalidate(false)` are shorthand for the constant hard
|
|
383
|
+
decisions `revalidate(() => true)` and `revalidate(() => false)`: `true`
|
|
384
|
+
re-runs the segment on every navigation and action, `false` keeps the client's
|
|
385
|
+
copy once it holds one. A defer has no shorthand: write the function. The
|
|
386
|
+
shorthand works everywhere `revalidate()` does, `clientUrls()` loaders
|
|
387
|
+
included. There the build checks the argument: a literal at its call site,
|
|
388
|
+
any other expression when the server mounts the group.
|
|
389
|
+
|
|
380
390
|
The callback must be **synchronous**. A returned Promise is ignored (dev
|
|
381
391
|
warning) and the running suggestion is kept, so move async work into a loader.
|
|
382
392
|
A callback that throws is logged and treated as a defer; a thrown `Response`
|
|
@@ -944,6 +954,8 @@ function ProductPage() {
|
|
|
944
954
|
|
|
945
955
|
A skeleton that has appeared stays on screen for at least about 300 ms before React replaces it with what it was waiting for: React does not replace a fallback sooner than 300 ms after the last fallback committed ([#1078](https://github.com/rangojs/rango/issues/1078)). On a cold click that mounts the `loading()` boundary, a loader that finishes sooner than that still shows its skeleton for about 300 ms, while a slower loader is not delayed. The router leaves this to React: an early skeleton is preferred to holding the click, and a fully prefetched click commits complete and shows none. A nested `loading()` whose data lands after the outer one has been revealed shows its own fallback, and the 300 ms starts again. The loaders feature page on the docs site has the measured figures. Under `transition()` a related cost applies; see [view-transitions](../view-transitions/SKILL.md).
|
|
946
956
|
|
|
957
|
+
A loader that throws (anything but `notFound()` or `redirect()`) throws at its `useLoader` read on every client lane: a plain or fully prefetched click, Back/Forward restored from the history cache, and the rerun after a server action. A prefetch in which a loader threw is never used, in memory or from the browser's HTTP cache: the click asks the server again, so a loader that has recovered shows its data. The nearest boundary to the read catches it (your React error boundary around the read, else the route's `errorBoundary()`, else the router's root one), and the layouts above keep their instance. The background refresh after Back/Forward onto a stale page is the exception: a loader that fails there leaves the page on screen and logs `[Browser] Background revalidation failed`. Not covered: an `intercept()`'s loaders, and a parallel slot's loaders under its own `loading()`, which are decoded above the slot's content.
|
|
958
|
+
|
|
947
959
|
## Loader Authority: notFound() and redirect()
|
|
948
960
|
|
|
949
961
|
A loader may **throw** `notFound()` and `redirect()` — data-dependent
|
|
@@ -955,7 +967,7 @@ import { createLoader, notFound, redirect } from "@rangojs/router";
|
|
|
955
967
|
|
|
956
968
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
957
969
|
const moved = LEGACY_SLUGS[ctx.params.slug];
|
|
958
|
-
if (moved) throw redirect(
|
|
970
|
+
if (moved) throw redirect(ctx.reverse("shop.product", { slug: moved }));
|
|
959
971
|
|
|
960
972
|
// Existence check BEFORE the expensive fetch: a near-instant rejection
|
|
961
973
|
// usually wins the race to first flush (see the semantics below).
|
|
@@ -1012,6 +1024,7 @@ Delivery is async **by the race model**: pushes that settle before the handler
|
|
|
1012
1024
|
barrier ride the SSR handle snapshot (in the SSR'd document — `<Html.Meta />`,
|
|
1013
1025
|
`useHandle` reads); later pushes stream to the client and apply post-hydration
|
|
1014
1026
|
on document loads (`metadata.handlesLate`) or progressively on navigations.
|
|
1027
|
+
Handle values land on their own channel and never hold the page, so an effect that needs a late push keys on the handle value, not the URL (see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design)).
|
|
1015
1028
|
A push before your slow fetch usually beats the barrier; a push derived from
|
|
1016
1029
|
the fetched data usually does not. When it MUST be in the document, use
|
|
1017
1030
|
`ssr: false` below. A loader with its own `cache()` keeps its pushes on a hit —
|
|
@@ -1021,7 +1034,9 @@ there every push a loader makes on that request applies post-hydration (see
|
|
|
1021
1034
|
`/ppr`, "Handles on a shell HIT"). A `useHandle` reader inside this loader's
|
|
1022
1035
|
`loading()` or `<Suspense>` hydrates with the data its HTML was rendered
|
|
1023
1036
|
from and shows a later push right after, so a push after an `await` causes
|
|
1024
|
-
no hydration mismatch there.
|
|
1037
|
+
no hydration mismatch there. A loader body may push after an `await`; a server
|
|
1038
|
+
component that does throws `LateHandlePushError`, so it resolves a slot its
|
|
1039
|
+
handler reserved with `.defer()` instead (`/breadcrumbs`, "Deferred content").
|
|
1025
1040
|
|
|
1026
1041
|
Under `cache()`, loader pushes are not stored with the cached segments: the
|
|
1027
1042
|
loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
|
|
@@ -1279,9 +1294,16 @@ The limits:
|
|
|
1279
1294
|
data with the URL, and one fill runs the flagged loaders (see
|
|
1280
1295
|
`/client-urls` → "`prefetch: false`"). A flagged `loading()` defers nothing
|
|
1281
1296
|
on a group route, from the group or from a server layout above it.
|
|
1282
|
-
- **
|
|
1283
|
-
|
|
1284
|
-
|
|
1297
|
+
- **Inside `intercept()`.** In `urls()` and in `clientUrls()`, a prefetch of
|
|
1298
|
+
the intercept's target from its source page defers the flagged loader: the
|
|
1299
|
+
click opens the modal at once, with its `loading()`, and one fill, sent
|
|
1300
|
+
from the source, runs the loader. The page under the modal stays. With no
|
|
1301
|
+
`loading()` the slot is empty until the fill answers, as on a plain click
|
|
1302
|
+
until its response. The modal's loaders resolve together: it shows its
|
|
1303
|
+
`loading()` until every one has arrived. A flagged `loading()` inside
|
|
1304
|
+
`intercept()` defers nothing. Each intercept loader also runs its own
|
|
1305
|
+
`revalidate()`, so an action from the modal re-runs only those that say so
|
|
1306
|
+
(see `/intercept`).
|
|
1285
1307
|
- **`ppr` and `ssr: false`.** On a `ppr` route an `ssr: false` loader is the
|
|
1286
1308
|
bake lane and is served from the shell, so `prefetch: false` is ignored for
|
|
1287
1309
|
it and development logs a warning. Under `cache()` and `Prerender` the flag
|
|
@@ -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
|
|