@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/skills/rango/SKILL.md
CHANGED
|
@@ -26,14 +26,16 @@ with the shape, then pick a primitive.
|
|
|
26
26
|
inside a cached render. They run **in parallel** right after middleware and
|
|
27
27
|
**stream**, so data latency overlaps first paint instead of blocking it (a
|
|
28
28
|
cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
|
|
29
|
-
caching explicitly. See `/loader` → "Parallel and streaming".
|
|
29
|
+
caching explicitly. See `/loader` → "Parallel and streaming". The one
|
|
30
|
+
exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
|
|
31
|
+
and runs on a shell HIT only when its return carries promises (`/ppr`).
|
|
30
32
|
- **One identity, one store** — loaders, handles, cached fns, and actions are all
|
|
31
33
|
`path#export`; all caches share one store. Entries expire by TTL/SWR, and are
|
|
32
34
|
tagged via `cache({ tags })` or runtime `cacheTag(...tags)`; built-in stores
|
|
33
35
|
index by tag and invalidate via `updateTag(...tags)` (awaitable, read-your-own-writes)
|
|
34
36
|
or `revalidateTag(...tags)` (background, non-blocking).
|
|
35
37
|
- **Type-safe end to end** — route names, params, search schemas, loader return
|
|
36
|
-
types, context vars, and `href` / `reverse` are checked at compile time
|
|
38
|
+
types, context vars, and `href()` / `ctx.reverse()` are checked at compile time
|
|
37
39
|
(`/typesafety`).
|
|
38
40
|
- **See where time goes** — turn on `debugPerformance` early (router option, or
|
|
39
41
|
`ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
|
|
@@ -42,7 +44,9 @@ with the shape, then pick a primitive.
|
|
|
42
44
|
console, OpenTelemetry, or custom sink. See `/observability`.
|
|
43
45
|
|
|
44
46
|
Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
|
|
45
|
-
`include()`, and `reverse()
|
|
47
|
+
`include()`, and URL generation (`ctx.reverse()` on the server, `href()` /
|
|
48
|
+
`useReverse()` on the client; there is no standalone `reverse()` export — see
|
|
49
|
+
`/links`). Caching, parallel routes, intercepts, prerender,
|
|
46
50
|
i18n, themes, and the rest are opt-in — reach for them when a requirement
|
|
47
51
|
appears, not up front.
|
|
48
52
|
|
|
@@ -66,8 +70,9 @@ Reach for the next rung only when the one above doesn't fit — the higher rungs
|
|
|
66
70
|
are immune to partial-revalidation staleness by construction.
|
|
67
71
|
|
|
68
72
|
1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
|
|
69
|
-
pass — full renders, action revalidations, cache hits
|
|
70
|
-
|
|
73
|
+
pass — full renders, action revalidations, cache hits, PPR shell HITs (on a
|
|
74
|
+
`ppr` route, keep it off `ssr: false`, which bakes it into the shell).
|
|
75
|
+
Nothing to keep in sync. If the data can be a loader, make it a loader.
|
|
71
76
|
2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
|
|
72
77
|
including post-action revalidation and PE re-renders, so its variables are
|
|
73
78
|
never stale. Right for request-shaped context: auth, session, locale.
|
|
@@ -112,7 +117,7 @@ stated, greppable contract.
|
|
|
112
117
|
| guarantee loader output in the SSR HTML | `loader(L, { ssr: false })` | /loader |
|
|
113
118
|
| pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
|
|
114
119
|
| feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
|
|
115
|
-
| cache
|
|
120
|
+
| cache HTML shell, live loaders as holes | `ppr` path option | /ppr |
|
|
116
121
|
| choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
|
|
117
122
|
| stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
|
|
118
123
|
|
|
@@ -121,19 +126,30 @@ stated, greppable contract.
|
|
|
121
126
|
- `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
|
|
122
127
|
- **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
|
|
123
128
|
- Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
|
|
129
|
+
The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
|
|
130
|
+
and runs on a shell HIT only when its return carries promises.
|
|
124
131
|
- **The consumption-lane rule.** For every shared artifact (`cache()`,
|
|
125
132
|
`"use cache"`, the PPR shell): server-side handler consumption
|
|
126
|
-
(`await ctx.use(loader)`) yields a BAKED copy —
|
|
127
|
-
(`cookies()`/`headers()
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
(`await ctx.use(loader)`) yields a BAKED copy — the capture-time value
|
|
134
|
+
freezes into the shared artifact. Identity reads (`cookies()`/`headers()`,
|
|
135
|
+
a `{ cache: false }` variable) inside that loader are permitted under
|
|
136
|
+
`cache()`/`"use cache"` (a documented footgun; see `/caching` → "Cache
|
|
137
|
+
purity & tainted objects") but REFUSE a PPR capture: the route serves
|
|
138
|
+
uncached until the read moves to a live loader. Client-side consumption
|
|
139
|
+
(`useLoader` in a `"use client"` component) is the LIVE lane. DSL
|
|
140
|
+
`loader()` segments follow their PPR lane (only `ssr: false` bakes; see
|
|
141
|
+
`/ppr` → The loader lane rule).
|
|
142
|
+
- **A PPR shell HIT never runs a handler.** Everything a handler produces
|
|
143
|
+
(promises it passes under `<Suspense>`, async server components, handle
|
|
144
|
+
pushes, loaders it awaits) is shell material, as under `cache()`; a HIT
|
|
145
|
+
runs middleware and live loaders only (an `ssr: false` loader runs there
|
|
146
|
+
only when its return carries promises). Live data belongs in a loader without
|
|
147
|
+
`ssr: false`, read with `useLoader` under `loading()` or an inline
|
|
148
|
+
`<Suspense>`.
|
|
133
149
|
- Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
|
|
134
150
|
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
|
135
|
-
is captured on miss and replayed on hit.
|
|
136
|
-
|
|
151
|
+
is captured on miss and replayed on hit. A non-cacheable variable read
|
|
152
|
+
(`createVar({ cache: false })`) throws too — see the correctness bullet below.
|
|
137
153
|
- One identity `path#export` (`functionId`/`$$id`/`actionId`); one store. Freshness
|
|
138
154
|
is TTL/SWR expiry plus tag-based invalidation: tag via `cache({ tags })` /
|
|
139
155
|
`cacheTag(...tags)`, then `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)`
|
|
@@ -142,7 +158,7 @@ stated, greppable contract.
|
|
|
142
158
|
- Caches are correctness-first: persistent store keys are version-segmented (no
|
|
143
159
|
cross-deploy drift), the forward/back cache is mutation-aware, and
|
|
144
160
|
`createVar({ cache: false })` throws on a **direct** read inside a `cache()`
|
|
145
|
-
boundary (a deliberately non-propagating guard). See `/cache-guide` →
|
|
161
|
+
boundary or a `"use cache"` function (a deliberately non-propagating guard). See `/cache-guide` →
|
|
146
162
|
"Correctness & invalidation".
|
|
147
163
|
- Nested caches: the outer cache window bounds the inner — an inner shorter TTL
|
|
148
164
|
only applies when the enclosing cache recomputes; put a value in a loader if it
|
|
@@ -160,17 +176,22 @@ stated, greppable contract.
|
|
|
160
176
|
Same words, different jobs — this is the most common source of the
|
|
161
177
|
`revalidate()`-is-caching misread.
|
|
162
178
|
|
|
163
|
-
| You may know | Maps to Rango
|
|
164
|
-
| --------------------------------------- |
|
|
165
|
-
| Next.js `export const revalidate = N` | **
|
|
166
|
-
| Next.js `revalidateTag` / `updateTag` | **
|
|
167
|
-
| React Router / Remix `shouldRevalidate` | **
|
|
168
|
-
| HTTP `Cache-Control` / ISR | Deployment layer
|
|
169
|
-
| Next.js PPR (partial prerendering) | HTML shell layer
|
|
170
|
-
| Remix/RR `loader` | live data
|
|
179
|
+
| You may know | Maps to Rango | Watch out |
|
|
180
|
+
| --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
181
|
+
| Next.js `export const revalidate = N` | **Value freshness** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **partial-render selection**. Use `cache({ ttl })` for the Next behavior. |
|
|
182
|
+
| Next.js `revalidateTag` / `updateTag` | **Value freshness** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
|
|
183
|
+
| React Router / Remix `shouldRevalidate` | **Partial-render selection** | This is the correct mental model for Rango's `revalidate()`. |
|
|
184
|
+
| HTTP `Cache-Control` / ISR | Deployment layer | Complete-response deployment layer. A CDN hit bypasses Rango entirely; the store-backed middleware does not. See `/deployment-caching` and `/document-cache`. |
|
|
185
|
+
| Next.js PPR (partial prerendering) | HTML shell layer | Same React primitive, different transport: Rango serves shells in-function after middleware. Ordinary `ppr` captures at runtime; `Prerender + ppr` captures at build. See `/ppr`, `/prerender`, and `/deployment-caching`. |
|
|
186
|
+
| Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
|
|
171
187
|
|
|
172
|
-
|
|
173
|
-
|
|
188
|
+
Next.js PPR and Rango's `ppr` also differ in the hole model: in Rango only
|
|
189
|
+
loaders make holes. A handler's own promise under `<Suspense>` bakes into the
|
|
190
|
+
shell, as under `cache()`.
|
|
191
|
+
|
|
192
|
+
See `/cache-guide` for the cache decision guide, `/loader` and `/route` for
|
|
193
|
+
`revalidate()` (partial-render selection), and `/document-cache` for the edge
|
|
194
|
+
layer.
|
|
174
195
|
|
|
175
196
|
## Canonical shape
|
|
176
197
|
|
|
@@ -178,9 +199,10 @@ See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
|
|
|
178
199
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
179
200
|
layout(<ShopLayout />, () => [ // structure: wraps children
|
|
180
201
|
loader(CartLoader, () => [ // config: live data
|
|
181
|
-
// partial-render axis: re-run
|
|
202
|
+
// partial-render axis: after an action, re-run only for cart actions;
|
|
203
|
+
// on navigation, undefined keeps the default.
|
|
182
204
|
// ctx.isAction() matches by reference (rename-safe), not by string.
|
|
183
|
-
revalidate((ctx) => ctx.isAction(CartActions)
|
|
205
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
|
|
184
206
|
]),
|
|
185
207
|
path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
|
|
186
208
|
loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
|
|
@@ -201,17 +223,29 @@ The predicate arg carries the action's full context, not just its identity. Matc
|
|
|
201
223
|
_which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
|
|
202
224
|
returned_ with `ctx.actionResult` — the value your `"use server"` function
|
|
203
225
|
returned, for outcome-conditional revalidation. The arg also exposes `actionId`
|
|
204
|
-
(raw `path#export`), `actionUrl`, `formData
|
|
205
|
-
`_rsc_stale` signal
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
226
|
+
(raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
|
|
227
|
+
and `stale` (the `_rsc_stale` signal that an action ran in this or another tab).
|
|
228
|
+
`actionId`, `actionUrl`, `actionResult`, and `formData` are `undefined` on plain
|
|
229
|
+
navigation; `method` is `"GET"` there and `"POST"` for an action. Call
|
|
230
|
+
`ctx.isAction()` with no arguments to ask "was this any action?".
|
|
231
|
+
|
|
232
|
+
`ctx.isAction()` returns a raw boolean. Pick the idiom by what the segment's
|
|
233
|
+
default already does: after an action, loaders, the route, and segments inside
|
|
234
|
+
the `path()` re-run; layouts and parallels above the route are skipped.
|
|
235
|
+
|
|
236
|
+
- **Add a signal** where the default skips: `ctx.isAction(CartActions) || undefined`
|
|
237
|
+
on a parent layout re-renders it after cart actions and defers otherwise. On a
|
|
238
|
+
loader or route segment it changes nothing.
|
|
239
|
+
- **Narrow after actions** where the default re-runs:
|
|
240
|
+
`ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
|
|
241
|
+
actions and keeps the navigation default (params or search changed).
|
|
242
|
+
- **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
|
|
243
|
+
too, so a loader stops refetching when params change, and it ends the chain for
|
|
244
|
+
any later revalidator on the segment.
|
|
212
245
|
|
|
213
246
|
```ts
|
|
214
|
-
//
|
|
247
|
+
// on a parent layout (skipped after actions by default): also re-render when
|
|
248
|
+
// checkout succeeded; defer to the default otherwise
|
|
215
249
|
revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
|
|
216
250
|
```
|
|
217
251
|
|
|
@@ -262,18 +296,18 @@ Grouped by concern — read when you need to…
|
|
|
262
296
|
|
|
263
297
|
**Data & caching** — fetch, mutate, and cache:
|
|
264
298
|
|
|
265
|
-
| Skill | Description
|
|
266
|
-
| --------------------- |
|
|
267
|
-
| `/loader` | Data loaders with `createLoader()` and `revalidate()`
|
|
268
|
-
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation
|
|
269
|
-
| `/caching` | Segment caching with memory or KV stores
|
|
270
|
-
| `/use-cache` | Function-level caching with `"use cache"` directive
|
|
271
|
-
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide
|
|
272
|
-
| `/document-cache` | Store-backed complete-response middleware using Cache-Control policy
|
|
273
|
-
| `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache
|
|
274
|
-
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()`
|
|
275
|
-
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback)
|
|
276
|
-
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes)
|
|
299
|
+
| Skill | Description |
|
|
300
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
+
| `/loader` | Data loaders with `createLoader()` and `revalidate()` |
|
|
302
|
+
| `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
|
|
303
|
+
| `/caching` | Segment caching with memory or KV stores |
|
|
304
|
+
| `/use-cache` | Function-level caching with `"use cache"` directive |
|
|
305
|
+
| `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
|
|
306
|
+
| `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
|
|
307
|
+
| `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
|
|
308
|
+
| `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a live loader read under `loading()` or an inline `<Suspense>`; handler output bakes |
|
|
309
|
+
| `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
|
|
310
|
+
| `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
|
|
277
311
|
|
|
278
312
|
**Client & presentation** — build the client-side UX:
|
|
279
313
|
|
|
@@ -326,6 +360,8 @@ Grouped by concern — read when you need to…
|
|
|
326
360
|
```typescript
|
|
327
361
|
// urls.tsx
|
|
328
362
|
import { urls } from "@rangojs/router";
|
|
363
|
+
import { RootLayout } from "./layouts/root"; // renders <Outlet />
|
|
364
|
+
import { HomePage, AboutPage } from "./pages";
|
|
329
365
|
|
|
330
366
|
export const urlpatterns = urls(({ path, layout }) => [
|
|
331
367
|
layout(RootLayout, () => [
|
|
@@ -336,12 +372,15 @@ export const urlpatterns = urls(({ path, layout }) => [
|
|
|
336
372
|
|
|
337
373
|
// router.tsx
|
|
338
374
|
import { createRouter } from "@rangojs/router";
|
|
375
|
+
import { Document } from "./document"; // <html>/<head>/<body> shell
|
|
339
376
|
import { urlpatterns } from "./urls";
|
|
340
377
|
|
|
341
|
-
export
|
|
378
|
+
export const router = createRouter({ document: Document, urls: urlpatterns });
|
|
342
379
|
```
|
|
343
380
|
|
|
344
|
-
|
|
381
|
+
`createRouter({ ... }).routes(urlpatterns)` is equivalent to passing `urls`.
|
|
382
|
+
See `/router-setup` for the Document component and router options, and
|
|
383
|
+
`/typesafety` for type-safe href and environment setup.
|
|
345
384
|
|
|
346
385
|
## CLI: `npx rango generate`
|
|
347
386
|
|
|
@@ -362,27 +401,37 @@ npx rango generate src/
|
|
|
362
401
|
npx rango generate src/urls.tsx src/api/
|
|
363
402
|
```
|
|
364
403
|
|
|
404
|
+
Three modes:
|
|
405
|
+
|
|
406
|
+
| Flag | Behavior |
|
|
407
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
408
|
+
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic). |
|
|
409
|
+
| `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings. |
|
|
410
|
+
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite` and `@vitejs/plugin-rsc`). `--config <path>` picks the config. |
|
|
411
|
+
|
|
365
412
|
### Auto-detection
|
|
366
413
|
|
|
367
414
|
Each file is classified by its contents:
|
|
368
415
|
|
|
369
|
-
| Contains
|
|
370
|
-
|
|
|
371
|
-
| `urls(`
|
|
372
|
-
| `createRouter`
|
|
373
|
-
| Both
|
|
416
|
+
| Contains | Generated output |
|
|
417
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
418
|
+
| `urls(` or `clientUrls(` | Per-module `*.gen.ts` with route names, patterns, params, search |
|
|
419
|
+
| `createRouter` | Per-router `*.named-routes.gen.ts` with global route map |
|
|
420
|
+
| Both | Both files |
|
|
374
421
|
|
|
375
|
-
Directories are scanned recursively for `.ts`/`.tsx` files, skipping
|
|
376
|
-
|
|
422
|
+
Directories are scanned recursively for `.ts`/`.tsx`/`.js`/`.jsx` files, skipping
|
|
423
|
+
`node_modules`, `dist`, `build`, `coverage`, dot-directories, and existing
|
|
424
|
+
`.gen.` files.
|
|
377
425
|
|
|
378
426
|
> The two generated files are **not interchangeable surfaces**.
|
|
379
427
|
> `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
|
|
380
428
|
> named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
|
|
381
429
|
> Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
|
|
382
430
|
> and explicit local handler typing (`Handler<".name", routes>`). Neither
|
|
383
|
-
> carries response payloads
|
|
384
|
-
> `
|
|
385
|
-
> See `/typesafety` for the full
|
|
431
|
+
> carries response payloads: `RouteResponse<typeof patterns, "name">` reads
|
|
432
|
+
> them from the `urls()` value directly, and `Rango.PathResponse` reads them from
|
|
433
|
+
> `RegisteredRoutes` (`typeof router.routeMap`). See `/typesafety` for the full
|
|
434
|
+
> surface breakdown.
|
|
386
435
|
|
|
387
436
|
### Recursive includes
|
|
388
437
|
|
|
@@ -413,7 +462,8 @@ extract routes defined dynamically:
|
|
|
413
462
|
- Routes computed from external data (databases, config files)
|
|
414
463
|
- Template literal patterns with interpolated variables
|
|
415
464
|
|
|
416
|
-
These routes are only discovered by the Vite plugin
|
|
417
|
-
`pnpm dev`
|
|
418
|
-
than the runtime-generated version.
|
|
419
|
-
prevents the static parser from
|
|
465
|
+
These routes are only discovered at runtime: by the Vite plugin during
|
|
466
|
+
`pnpm dev` / `pnpm build`, or by `rango generate --runtime`. A statically
|
|
467
|
+
generated `.gen.ts` may have fewer routes than the runtime-generated version.
|
|
468
|
+
During dev, the `preserveIfLarger` guard prevents the static parser from
|
|
469
|
+
overwriting a larger runtime-discovered file.
|
|
@@ -74,7 +74,7 @@ export default defineConfig({
|
|
|
74
74
|
react({ compiler: true }),
|
|
75
75
|
rango({ preset: "cloudflare" }),
|
|
76
76
|
cloudflare({
|
|
77
|
-
|
|
77
|
+
viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
|
|
78
78
|
}),
|
|
79
79
|
],
|
|
80
80
|
});
|
|
@@ -200,7 +200,8 @@ import babel from "@rolldown/plugin-babel";
|
|
|
200
200
|
## Reference
|
|
201
201
|
|
|
202
202
|
A worked, tested wiring (dev + production e2e markers, incl. the client-only
|
|
203
|
-
contract and the Fast Refresh check) lives in the `@rangojs/router` repository
|
|
204
|
-
not shipped in this package: `docs/react-compiler.md
|
|
205
|
-
`react-compiler.test.ts`
|
|
206
|
-
|
|
203
|
+
contract and the Fast Refresh check) lives in the `@rangojs/router` repository,
|
|
204
|
+
not shipped in this package: `packages/rangojs-router/docs/react-compiler.md`,
|
|
205
|
+
`packages/rangojs-router/e2e/react-compiler.test.ts` (covers the
|
|
206
|
+
`e2e/e2e-basic` app), `tests/cloudflare-basic/e2e/react-compiler.test.ts`, and
|
|
207
|
+
`tests/vite-rsc-demo/e2e/react-compiler.test.ts`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: response-routes
|
|
3
|
-
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages,
|
|
3
|
+
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, asking how to return raw JSON instead of RSC from a route, or protecting a state-changing endpoint from CSRF.
|
|
4
4
|
argument-hint: [json|text|html|xml|md|image|stream]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,13 +8,15 @@ argument-hint: [json|text|html|xml|md|image|stream]
|
|
|
8
8
|
|
|
9
9
|
Response routes skip the RSC pipeline entirely. Use them for JSON APIs, plain text endpoints,
|
|
10
10
|
XML feeds, image proxies, and any route that returns a `Response` instead of React components.
|
|
11
|
+
Serving several representations from one URL is `/mime-routes`; a typed client for
|
|
12
|
+
calling your JSON routes is `/api-client`.
|
|
11
13
|
|
|
12
14
|
## Route-Level Tags: path.json(), path.text(), etc.
|
|
13
15
|
|
|
14
|
-
Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes
|
|
16
|
+
Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes. Each takes `(pattern, handler, options?, use?)` like `path()`:
|
|
15
17
|
|
|
16
18
|
```typescript
|
|
17
|
-
import { urls
|
|
19
|
+
import { urls } from "@rangojs/router";
|
|
18
20
|
|
|
19
21
|
export const urlpatterns = urls(({ path, layout, include }) => [
|
|
20
22
|
// RSC routes (normal)
|
|
@@ -81,18 +83,27 @@ export const urlpatterns = urls(({ path, layout, include }) => [
|
|
|
81
83
|
|
|
82
84
|
## ResponseHandlerContext
|
|
83
85
|
|
|
84
|
-
Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.res`):
|
|
86
|
+
Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.set()`, no `ctx.res`):
|
|
85
87
|
|
|
86
88
|
```typescript
|
|
87
89
|
interface ResponseHandlerContext<TParams, TEnv> {
|
|
88
|
-
request
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
url: URL;
|
|
90
|
+
// Shared request scope (same as every Rango context)
|
|
91
|
+
request: Request; // raw request: headers, method, body
|
|
92
|
+
url: URL; // internal _rsc* params stripped
|
|
93
|
+
originalUrl: URL; // transport URL, unmodified
|
|
93
94
|
pathname: string;
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
searchParams: URLSearchParams;
|
|
96
|
+
env: TEnv; // Plain bindings (DB, KV, etc.)
|
|
97
|
+
waitUntil(fn: () => Promise<void>): void; // work after the response
|
|
98
|
+
executionContext?: ExecutionContext; // Cloudflare only
|
|
99
|
+
// Response-route specific
|
|
100
|
+
params: TParams; // Typed from URL pattern
|
|
101
|
+
reverse: (
|
|
102
|
+
name: string,
|
|
103
|
+
params?: Record<string, string>,
|
|
104
|
+
search?: Record<string, unknown>,
|
|
105
|
+
) => string; // GlobalReverseFunction: global names only, ".name" is a type error
|
|
106
|
+
get: GetVariableFn; // Read middleware variables (string key or createVar token)
|
|
96
107
|
header: (name: string, value: string) => void;
|
|
97
108
|
// Use cookies().set(name, value, opts) for cookie mutations (standalone API)
|
|
98
109
|
}
|
|
@@ -104,6 +115,8 @@ String-returning handlers (json, text, html, xml, md) can set custom headers and
|
|
|
104
115
|
without constructing a full Response:
|
|
105
116
|
|
|
106
117
|
```typescript
|
|
118
|
+
import { cookies } from "@rangojs/router";
|
|
119
|
+
|
|
107
120
|
path.md(
|
|
108
121
|
"/docs/:slug.md",
|
|
109
122
|
(ctx) => {
|
|
@@ -127,10 +140,12 @@ merge, and router `onError` is not invoked. Throw ordinary errors (including
|
|
|
127
140
|
|
|
128
141
|
### Environment Access
|
|
129
142
|
|
|
130
|
-
`ctx.env` is
|
|
143
|
+
`ctx.env` is the plain bindings object the router received for the request. Its
|
|
144
|
+
type comes from the global `Rango.Env` registration (`/typesafety`), not from
|
|
145
|
+
the `createRouter<TEnv>()` generic, which `urls()` modules cannot see:
|
|
131
146
|
|
|
132
147
|
```typescript
|
|
133
|
-
//
|
|
148
|
+
// Rango.Env registered as { DB: D1Database; KV: KVNamespace }
|
|
134
149
|
|
|
135
150
|
// In a response handler:
|
|
136
151
|
path.json(
|
|
@@ -192,6 +207,13 @@ path.json(
|
|
|
192
207
|
);
|
|
193
208
|
```
|
|
194
209
|
|
|
210
|
+
`RouterError` defaults to status 500 when `status` is omitted. Any other thrown
|
|
211
|
+
error becomes a 500 with `code: "INTERNAL"`, and its message is hidden in
|
|
212
|
+
production (`RouterError` messages are always sent). Only `path.json()` produces
|
|
213
|
+
`problem+json`: the other response types answer errors with `text/plain` — the
|
|
214
|
+
`RouterError` message, or for other errors the message in development and
|
|
215
|
+
`Internal Server Error` in production.
|
|
216
|
+
|
|
195
217
|
### Returning Response Directly
|
|
196
218
|
|
|
197
219
|
JSON handlers can return `Response` to bypass auto-wrap (custom status, headers, streaming):
|
|
@@ -223,6 +245,8 @@ For an OAuth, SSO, or payment callback, validate the registered target and
|
|
|
223
245
|
return an explicitly external redirect:
|
|
224
246
|
|
|
225
247
|
```typescript
|
|
248
|
+
import { redirect } from "@rangojs/router";
|
|
249
|
+
|
|
226
250
|
path.any("/oauth/decision", async (ctx) => {
|
|
227
251
|
const redirectUri = await readAndValidateRedirectUri(ctx.request);
|
|
228
252
|
return redirect(redirectUri, { status: 303, external: true });
|
|
@@ -341,16 +365,19 @@ For local/scoped response typing without global augmentation, prefer
|
|
|
341
365
|
the response payload straight from the `urls()` patterns and needs no
|
|
342
366
|
`RegisteredRoutes` wiring.
|
|
343
367
|
|
|
344
|
-
###
|
|
368
|
+
### RouteParams with Response Routes
|
|
345
369
|
|
|
346
370
|
```typescript
|
|
347
|
-
import type {
|
|
371
|
+
import type { RouteParams } from "@rangojs/router";
|
|
348
372
|
|
|
349
|
-
// Works for both RSC and response routes
|
|
350
|
-
type ProductParams =
|
|
373
|
+
// Works for both RSC and response routes (reads the global route map)
|
|
374
|
+
type ProductParams = RouteParams<"api.productDetail">;
|
|
351
375
|
// = { id: string }
|
|
352
376
|
```
|
|
353
377
|
|
|
378
|
+
`ParamsFor<TRoutes, "name">` is the lower-level form that takes an explicit
|
|
379
|
+
route map first (e.g. `ParamsFor<typeof router.routeMap, "api.productDetail">`).
|
|
380
|
+
|
|
354
381
|
## Links to Response Routes
|
|
355
382
|
|
|
356
383
|
### Client: href.json(), href.text(), etc.
|
|
@@ -401,6 +428,80 @@ Response-route `cache()` stores whole `Response`s. The serve leaf
|
|
|
401
428
|
| **Per-client signals** | A response with `Set-Cookie` or `x-rango-keep-cache` is returned live but **not** stored (MISS + SWR revalidation also skip put). |
|
|
402
429
|
| **Default key** | `response:{type}:{cacheKeyBase(host, path, searchParams)}` — sorted search, reserved `_rsc*` / allowlisted `__*` params excluded. Custom `key()` / store `keyGenerator` still override. |
|
|
403
430
|
|
|
431
|
+
## CSRF
|
|
432
|
+
|
|
433
|
+
The router's built-in `originCheck` guards server actions, fetchable loaders,
|
|
434
|
+
and progressive-enhancement form posts (`/server-actions` → "CSRF
|
|
435
|
+
Protection"). Response routes are **outside** that gate: the check is never
|
|
436
|
+
consulted for them, not even a custom `originCheck` function.
|
|
437
|
+
|
|
438
|
+
That matters for a response route that changes state **and** authenticates
|
|
439
|
+
with cookies. Another site can make the victim's browser send a POST to it —
|
|
440
|
+
an HTML form (`urlencoded`, `multipart`, or `text/plain`) or a `no-cors`
|
|
441
|
+
`fetch()` — and the browser attaches the session cookie. Being a "JSON API" is
|
|
442
|
+
no protection: `path.json()` does not parse the request body, and
|
|
443
|
+
`await ctx.request.json()` parses a `text/plain` body just as happily. Routes
|
|
444
|
+
authenticated only by an `Authorization` header are not exposed, because a
|
|
445
|
+
cross-site request cannot set it.
|
|
446
|
+
|
|
447
|
+
Guard state-changing response routes with middleware that applies the same
|
|
448
|
+
rule as `originCheck`:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import type { Middleware } from "@rangojs/router";
|
|
452
|
+
|
|
453
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
454
|
+
|
|
455
|
+
// Mirrors the built-in originCheck: Origin (or Referer) must equal the request
|
|
456
|
+
// protocol + Host; no Origin/Referer means a non-browser client; "null" and a
|
|
457
|
+
// missing Host fail closed.
|
|
458
|
+
export const requireSameOrigin: Middleware = async (ctx, next) => {
|
|
459
|
+
if (SAFE_METHODS.has(ctx.request.method)) return next();
|
|
460
|
+
|
|
461
|
+
const headers = ctx.request.headers;
|
|
462
|
+
let origin = headers.get("origin");
|
|
463
|
+
if (!origin) {
|
|
464
|
+
const referer = headers.get("referer");
|
|
465
|
+
if (referer) {
|
|
466
|
+
try {
|
|
467
|
+
origin = new URL(referer).origin;
|
|
468
|
+
} catch {
|
|
469
|
+
// Malformed Referer: treat as absent
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!origin) return next();
|
|
474
|
+
|
|
475
|
+
const host = headers.get("host");
|
|
476
|
+
const expected = host ? `${ctx.url.protocol}//${host}` : null;
|
|
477
|
+
if (!expected || origin.toLowerCase() !== expected.toLowerCase()) {
|
|
478
|
+
throw new Response("Forbidden", { status: 403 });
|
|
479
|
+
}
|
|
480
|
+
return next();
|
|
481
|
+
};
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Attach it with a pattern-scoped `router.use()` to cover a whole API prefix, or
|
|
485
|
+
as a `middleware()` use item on a single route:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// router.tsx — every response route under the API prefix
|
|
489
|
+
const router = createRouter()
|
|
490
|
+
.use("/api/*", requireSameOrigin)
|
|
491
|
+
.routes(urlpatterns);
|
|
492
|
+
|
|
493
|
+
// urls.tsx — one route
|
|
494
|
+
path.json("/api/cart", updateCart, { name: "cart" }, () => [
|
|
495
|
+
middleware(requireSameOrigin),
|
|
496
|
+
]);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Server-to-server callers (signed webhooks, cron, other backends) send no
|
|
500
|
+
`Origin`, so they pass this check; verify their signature or credentials in the
|
|
501
|
+
handler. Behind a proxy that rewrites `Host`, derive `expected` from the header
|
|
502
|
+
your proxy sets, as with a custom `originCheck` (`/router-setup` → "Origin
|
|
503
|
+
check").
|
|
504
|
+
|
|
404
505
|
## Mountable Module Pattern
|
|
405
506
|
|
|
406
507
|
A self-contained module with RSC pages + JSON APIs, mountable via `include()`:
|
|
@@ -474,8 +575,7 @@ the `_responses` phantom are inferred from the resolved `urls()` value, so
|
|
|
474
575
|
### Type safety after mounting
|
|
475
576
|
|
|
476
577
|
```typescript
|
|
477
|
-
import type { RouteResponse } from "@rangojs/router";
|
|
478
|
-
import type { ParamsFor } from "@rangojs/router";
|
|
578
|
+
import type { RouteResponse, RouteParams } from "@rangojs/router";
|
|
479
579
|
|
|
480
580
|
// Scoped (before mount) -- use the module directly, no global wiring needed
|
|
481
581
|
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
@@ -488,21 +588,25 @@ type BlogStats = Rango.PathResponse<"/blog/api/stats">;
|
|
|
488
588
|
// = { views: number; visitors: number }
|
|
489
589
|
|
|
490
590
|
// Params work through nested includes
|
|
491
|
-
type LikesParams =
|
|
591
|
+
type LikesParams = RouteParams<"blog.api.likes">;
|
|
492
592
|
// = { slug: string }
|
|
493
593
|
```
|
|
494
594
|
|
|
495
595
|
### ctx.reverse inside mounted modules
|
|
496
596
|
|
|
497
|
-
|
|
597
|
+
A response handler's `ctx.reverse` resolves **global** names only: it has no
|
|
598
|
+
`include()` scope and does not auto-fill params from the current request. Use
|
|
599
|
+
the fully qualified name and pass every param:
|
|
498
600
|
|
|
499
601
|
```typescript
|
|
500
|
-
// Inside blogApiPatterns
|
|
501
|
-
|
|
602
|
+
// Inside blogApiPatterns (mounted via include("/api", ..., { name: "api" })
|
|
603
|
+
// inside blogPatterns, mounted via include("/blog", ..., { name: "blog" }))
|
|
604
|
+
path.json(
|
|
502
605
|
"/:slug/likes",
|
|
503
606
|
(ctx) => {
|
|
504
|
-
|
|
505
|
-
|
|
607
|
+
const commentsUrl = ctx.reverse("blog.api.comments", {
|
|
608
|
+
slug: ctx.params.slug,
|
|
609
|
+
});
|
|
506
610
|
// -> "/blog/api/my-post/comments"
|
|
507
611
|
|
|
508
612
|
return { slug: ctx.params.slug, count: 42, commentsUrl };
|
|
@@ -511,6 +615,10 @@ path(
|
|
|
511
615
|
);
|
|
512
616
|
```
|
|
513
617
|
|
|
618
|
+
A dot-local name (`ctx.reverse(".comments")`) is a type error here. The
|
|
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.
|
|
621
|
+
|
|
514
622
|
## Content Negotiation
|
|
515
623
|
|
|
516
624
|
Multiple response types can share the same URL pattern. See `/mime-routes` for the
|