@rangojs/router 0.15.1 → 0.17.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 +254 -89
- package/dist/testing/vitest.js +252 -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-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- 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 +52 -0
- 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 +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -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/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- 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 +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- 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/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- 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/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/thenable-status.d.ts +9 -0
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- 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/vite/index.js +884 -768
- package/package.json +4 -4
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -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 +39 -33
- 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 +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +43 -1
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -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 +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- 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 +8 -5
- 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/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 +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- 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 +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- 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 +75 -80
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -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 +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- 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 +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -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 +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/ssr-suspension-warning.ts +2 -13
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -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 +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
package/skills/rango/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ with the shape, then pick a primitive.
|
|
|
33
33
|
index by tag and invalidate via `updateTag(...tags)` (awaitable, read-your-own-writes)
|
|
34
34
|
or `revalidateTag(...tags)` (background, non-blocking).
|
|
35
35
|
- **Type-safe end to end** — route names, params, search schemas, loader return
|
|
36
|
-
types, context vars, and `href` / `reverse` are checked at compile time
|
|
36
|
+
types, context vars, and `href()` / `ctx.reverse()` are checked at compile time
|
|
37
37
|
(`/typesafety`).
|
|
38
38
|
- **See where time goes** — turn on `debugPerformance` early (router option, or
|
|
39
39
|
`ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
|
|
@@ -42,7 +42,9 @@ with the shape, then pick a primitive.
|
|
|
42
42
|
console, OpenTelemetry, or custom sink. See `/observability`.
|
|
43
43
|
|
|
44
44
|
Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
|
|
45
|
-
`include()`, and `reverse()
|
|
45
|
+
`include()`, and URL generation (`ctx.reverse()` on the server, `href()` /
|
|
46
|
+
`useReverse()` on the client; there is no standalone `reverse()` export — see
|
|
47
|
+
`/links`). Caching, parallel routes, intercepts, prerender,
|
|
46
48
|
i18n, themes, and the rest are opt-in — reach for them when a requirement
|
|
47
49
|
appears, not up front.
|
|
48
50
|
|
|
@@ -128,12 +130,12 @@ stated, greppable contract.
|
|
|
128
130
|
freezes into the shared artifact (a documented footgun; see `/caching` →
|
|
129
131
|
"Cache purity & tainted objects"). Client-side consumption (`useLoader` in
|
|
130
132
|
a `"use client"` component) is the LIVE lane. DSL `loader()` segments
|
|
131
|
-
follow their lane
|
|
132
|
-
|
|
133
|
+
follow their PPR lane (only `ssr: false` bakes; see `/ppr` → The loader
|
|
134
|
+
lane rule). Pinned by semantic-matrix row PPR3.
|
|
133
135
|
- Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
|
|
134
136
|
(`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
|
|
135
|
-
is captured on miss and replayed on hit.
|
|
136
|
-
|
|
137
|
+
is captured on miss and replayed on hit. A non-cacheable variable read
|
|
138
|
+
(`createVar({ cache: false })`) throws too — see the correctness bullet below.
|
|
137
139
|
- One identity `path#export` (`functionId`/`$$id`/`actionId`); one store. Freshness
|
|
138
140
|
is TTL/SWR expiry plus tag-based invalidation: tag via `cache({ tags })` /
|
|
139
141
|
`cacheTag(...tags)`, then `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)`
|
|
@@ -142,7 +144,7 @@ stated, greppable contract.
|
|
|
142
144
|
- Caches are correctness-first: persistent store keys are version-segmented (no
|
|
143
145
|
cross-deploy drift), the forward/back cache is mutation-aware, and
|
|
144
146
|
`createVar({ cache: false })` throws on a **direct** read inside a `cache()`
|
|
145
|
-
boundary (a deliberately non-propagating guard). See `/cache-guide` →
|
|
147
|
+
boundary or a `"use cache"` function (a deliberately non-propagating guard). See `/cache-guide` →
|
|
146
148
|
"Correctness & invalidation".
|
|
147
149
|
- Nested caches: the outer cache window bounds the inner — an inner shorter TTL
|
|
148
150
|
only applies when the enclosing cache recomputes; put a value in a loader if it
|
|
@@ -178,9 +180,10 @@ See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
|
|
|
178
180
|
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
179
181
|
layout(<ShopLayout />, () => [ // structure: wraps children
|
|
180
182
|
loader(CartLoader, () => [ // config: live data
|
|
181
|
-
// partial-render axis: re-run
|
|
183
|
+
// partial-render axis: after an action, re-run only for cart actions;
|
|
184
|
+
// on navigation, undefined keeps the default.
|
|
182
185
|
// ctx.isAction() matches by reference (rename-safe), not by string.
|
|
183
|
-
revalidate((ctx) => ctx.isAction(CartActions)
|
|
186
|
+
revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
|
|
184
187
|
]),
|
|
185
188
|
path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
|
|
186
189
|
loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
|
|
@@ -201,17 +204,29 @@ The predicate arg carries the action's full context, not just its identity. Matc
|
|
|
201
204
|
_which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
|
|
202
205
|
returned_ with `ctx.actionResult` — the value your `"use server"` function
|
|
203
206
|
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
|
-
|
|
207
|
+
(raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
|
|
208
|
+
and `stale` (the `_rsc_stale` signal that an action ran in this or another tab).
|
|
209
|
+
`actionId`, `actionUrl`, `actionResult`, and `formData` are `undefined` on plain
|
|
210
|
+
navigation; `method` is `"GET"` there and `"POST"` for an action. Call
|
|
211
|
+
`ctx.isAction()` with no arguments to ask "was this any action?".
|
|
212
|
+
|
|
213
|
+
`ctx.isAction()` returns a raw boolean. Pick the idiom by what the segment's
|
|
214
|
+
default already does: after an action, loaders, the route, and segments inside
|
|
215
|
+
the `path()` re-run; layouts and parallels above the route are skipped.
|
|
216
|
+
|
|
217
|
+
- **Add a signal** where the default skips: `ctx.isAction(CartActions) || undefined`
|
|
218
|
+
on a parent layout re-renders it after cart actions and defers otherwise. On a
|
|
219
|
+
loader or route segment it changes nothing.
|
|
220
|
+
- **Narrow after actions** where the default re-runs:
|
|
221
|
+
`ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
|
|
222
|
+
actions and keeps the navigation default (params or search changed).
|
|
223
|
+
- **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
|
|
224
|
+
too, so a loader stops refetching when params change, and it ends the chain for
|
|
225
|
+
any later revalidator on the segment.
|
|
212
226
|
|
|
213
227
|
```ts
|
|
214
|
-
//
|
|
228
|
+
// on a parent layout (skipped after actions by default): also re-render when
|
|
229
|
+
// checkout succeeded; defer to the default otherwise
|
|
215
230
|
revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
|
|
216
231
|
```
|
|
217
232
|
|
|
@@ -326,6 +341,8 @@ Grouped by concern — read when you need to…
|
|
|
326
341
|
```typescript
|
|
327
342
|
// urls.tsx
|
|
328
343
|
import { urls } from "@rangojs/router";
|
|
344
|
+
import { RootLayout } from "./layouts/root"; // renders <Outlet />
|
|
345
|
+
import { HomePage, AboutPage } from "./pages";
|
|
329
346
|
|
|
330
347
|
export const urlpatterns = urls(({ path, layout }) => [
|
|
331
348
|
layout(RootLayout, () => [
|
|
@@ -336,12 +353,15 @@ export const urlpatterns = urls(({ path, layout }) => [
|
|
|
336
353
|
|
|
337
354
|
// router.tsx
|
|
338
355
|
import { createRouter } from "@rangojs/router";
|
|
356
|
+
import { Document } from "./document"; // <html>/<head>/<body> shell
|
|
339
357
|
import { urlpatterns } from "./urls";
|
|
340
358
|
|
|
341
|
-
export
|
|
359
|
+
export const router = createRouter({ document: Document, urls: urlpatterns });
|
|
342
360
|
```
|
|
343
361
|
|
|
344
|
-
|
|
362
|
+
`createRouter({ ... }).routes(urlpatterns)` is equivalent to passing `urls`.
|
|
363
|
+
See `/router-setup` for the Document component and router options, and
|
|
364
|
+
`/typesafety` for type-safe href and environment setup.
|
|
345
365
|
|
|
346
366
|
## CLI: `npx rango generate`
|
|
347
367
|
|
|
@@ -362,27 +382,37 @@ npx rango generate src/
|
|
|
362
382
|
npx rango generate src/urls.tsx src/api/
|
|
363
383
|
```
|
|
364
384
|
|
|
385
|
+
Three modes:
|
|
386
|
+
|
|
387
|
+
| Flag | Behavior |
|
|
388
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
389
|
+
| (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic). |
|
|
390
|
+
| `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings. |
|
|
391
|
+
| `--runtime` | Vite-based runtime discovery (full coverage; needs `vite` and `@vitejs/plugin-rsc`). `--config <path>` picks the config. |
|
|
392
|
+
|
|
365
393
|
### Auto-detection
|
|
366
394
|
|
|
367
395
|
Each file is classified by its contents:
|
|
368
396
|
|
|
369
|
-
| Contains
|
|
370
|
-
|
|
|
371
|
-
| `urls(`
|
|
372
|
-
| `createRouter`
|
|
373
|
-
| Both
|
|
397
|
+
| Contains | Generated output |
|
|
398
|
+
| ------------------------ | ---------------------------------------------------------------- |
|
|
399
|
+
| `urls(` or `clientUrls(` | Per-module `*.gen.ts` with route names, patterns, params, search |
|
|
400
|
+
| `createRouter` | Per-router `*.named-routes.gen.ts` with global route map |
|
|
401
|
+
| Both | Both files |
|
|
374
402
|
|
|
375
|
-
Directories are scanned recursively for `.ts`/`.tsx` files, skipping
|
|
376
|
-
|
|
403
|
+
Directories are scanned recursively for `.ts`/`.tsx`/`.js`/`.jsx` files, skipping
|
|
404
|
+
`node_modules`, `dist`, `build`, `coverage`, dot-directories, and existing
|
|
405
|
+
`.gen.` files.
|
|
377
406
|
|
|
378
407
|
> The two generated files are **not interchangeable surfaces**.
|
|
379
408
|
> `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
|
|
380
409
|
> named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
|
|
381
410
|
> Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
|
|
382
411
|
> and explicit local handler typing (`Handler<".name", routes>`). Neither
|
|
383
|
-
> carries response payloads
|
|
384
|
-
> `
|
|
385
|
-
> See `/typesafety` for the full
|
|
412
|
+
> carries response payloads: `RouteResponse<typeof patterns, "name">` reads
|
|
413
|
+
> them from the `urls()` value directly, and `Rango.PathResponse` reads them from
|
|
414
|
+
> `RegisteredRoutes` (`typeof router.routeMap`). See `/typesafety` for the full
|
|
415
|
+
> surface breakdown.
|
|
386
416
|
|
|
387
417
|
### Recursive includes
|
|
388
418
|
|
|
@@ -413,7 +443,8 @@ extract routes defined dynamically:
|
|
|
413
443
|
- Routes computed from external data (databases, config files)
|
|
414
444
|
- Template literal patterns with interpolated variables
|
|
415
445
|
|
|
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
|
|
446
|
+
These routes are only discovered at runtime: by the Vite plugin during
|
|
447
|
+
`pnpm dev` / `pnpm build`, or by `rango generate --runtime`. A statically
|
|
448
|
+
generated `.gen.ts` may have fewer routes than the runtime-generated version.
|
|
449
|
+
During dev, the `preserveIfLarger` guard prevents the static parser from
|
|
450
|
+
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
|