@rangojs/router 0.16.0 → 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 +7 -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/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/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 +1381 -1346
- package/package.json +2 -2
- 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 +23 -3
- 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 +35 -25
- 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 +7 -4
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +117 -30
- 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 +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/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/ThemeScript.tsx +3 -3
- 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 +12 -3
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -1,12 +1,39 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: router-setup
|
|
3
|
-
description: Create and configure
|
|
3
|
+
description: Create and configure a Rango app with createRouter() and the rango() Vite plugin. Use when bootstrapping a new app, wiring vite.config, writing the Document or worker entry, or setting top-level router options (basename, cache store, prefetch, timeouts, origin check, telemetry, SSR streaming).
|
|
4
4
|
argument-hint: [option]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Router Setup with createRouter
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This skill covers the app skeleton: the Vite plugin, the router module, the
|
|
10
|
+
Document, the request entry, and every top-level `createRouter()` and `rango()`
|
|
11
|
+
option. Route-level features (loaders, caching, middleware, intercepts) have
|
|
12
|
+
their own skills; start at `/rango` for the map.
|
|
13
|
+
|
|
14
|
+
## Minimal app
|
|
15
|
+
|
|
16
|
+
On the default Node preset an app needs a Vite config, a router module, and a
|
|
17
|
+
`urls()` module (plus your page components). `rango()` finds the router by
|
|
18
|
+
scanning for the single `createRouter()` call and generates the client, SSR,
|
|
19
|
+
and RSC entries, so there is no `index.html` and no entry file to write. The
|
|
20
|
+
`document` option is optional (see [Document component](#document-component)).
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// vite.config.ts
|
|
24
|
+
import { defineConfig } from "vite";
|
|
25
|
+
import { rango } from "@rangojs/router/vite";
|
|
26
|
+
|
|
27
|
+
export default defineConfig(({ command }) => ({
|
|
28
|
+
plugins: [rango()],
|
|
29
|
+
// Node preset only: fold NODE_ENV so SSR/RSC ship only React's production
|
|
30
|
+
// build. The cloudflare and vercel presets do this for you (see /bundle-analysis).
|
|
31
|
+
define:
|
|
32
|
+
command === "build"
|
|
33
|
+
? { "process.env.NODE_ENV": JSON.stringify("production") }
|
|
34
|
+
: undefined,
|
|
35
|
+
}));
|
|
36
|
+
```
|
|
10
37
|
|
|
11
38
|
```typescript
|
|
12
39
|
// src/router.tsx
|
|
@@ -14,15 +41,32 @@ import { createRouter } from "@rangojs/router";
|
|
|
14
41
|
import { Document } from "./document";
|
|
15
42
|
import { urlpatterns } from "./urls";
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
// Node and Vercel presets import the NAMED `router` export from this module.
|
|
45
|
+
// A default-only export is not picked up.
|
|
46
|
+
export const router = createRouter({
|
|
18
47
|
document: Document,
|
|
19
48
|
urls: urlpatterns,
|
|
20
49
|
});
|
|
50
|
+
```
|
|
21
51
|
|
|
22
|
-
|
|
52
|
+
```typescript
|
|
53
|
+
// src/urls.tsx
|
|
54
|
+
import { urls } from "@rangojs/router";
|
|
55
|
+
import { HomePage } from "./pages/home";
|
|
56
|
+
import { AboutPage } from "./pages/about";
|
|
57
|
+
|
|
58
|
+
export const urlpatterns = urls(({ path }) => [
|
|
59
|
+
path("/", HomePage, { name: "home" }),
|
|
60
|
+
path("/about", AboutPage, { name: "about" }),
|
|
61
|
+
]);
|
|
23
62
|
```
|
|
24
63
|
|
|
25
|
-
|
|
64
|
+
`@vitejs/plugin-react` is optional: `rango()` works on its own. Add
|
|
65
|
+
`react()` before `rango()` when you want its features, such as the React
|
|
66
|
+
Compiler (`/react-compiler`). For Workers use `/cloudflare`; for Vercel
|
|
67
|
+
Functions use `/vercel`.
|
|
68
|
+
|
|
69
|
+
## URL patterns (Django-style)
|
|
26
70
|
|
|
27
71
|
```typescript
|
|
28
72
|
// src/urls.tsx
|
|
@@ -30,7 +74,9 @@ import { urls } from "@rangojs/router";
|
|
|
30
74
|
import { HomePage } from "./pages/home";
|
|
31
75
|
import { AboutPage } from "./pages/about";
|
|
32
76
|
import { ProductPage } from "./pages/product";
|
|
77
|
+
import { ProductSkeleton } from "./pages/product-skeleton";
|
|
33
78
|
import { RootLayout } from "./layouts/RootLayout";
|
|
79
|
+
import { ProductLoader } from "./loaders/product";
|
|
34
80
|
|
|
35
81
|
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
36
82
|
path("/", HomePage, { name: "home" }),
|
|
@@ -47,105 +93,239 @@ export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
|
47
93
|
|
|
48
94
|
## The urls() DSL
|
|
49
95
|
|
|
50
|
-
|
|
96
|
+
`urls()` passes every DSL helper to its callback. Destructure the ones you use:
|
|
51
97
|
|
|
52
98
|
```typescript
|
|
53
99
|
urls(
|
|
54
100
|
({
|
|
55
|
-
path, // Define a route
|
|
56
|
-
layout, // Wrap routes in a layout
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
path, // Define a route; also path.json/.text/.html/.xml/.md/.image/.stream/.any (/response-routes)
|
|
102
|
+
layout, // Wrap routes in a layout (/layout)
|
|
103
|
+
include, // Mount another urls() module under a prefix (/composability)
|
|
104
|
+
parallel, // Named slots rendered alongside the outlet (/parallel)
|
|
105
|
+
intercept, // Soft-navigation overlays such as modals (/intercept)
|
|
106
|
+
loader, // Attach a data loader (/loader)
|
|
107
|
+
loading, // Loading fallback for a segment
|
|
108
|
+
cache, // Segment caching (/caching)
|
|
109
|
+
middleware, // Route middleware (/middleware)
|
|
110
|
+
revalidate, // Choose which segments re-render after navigation/actions
|
|
111
|
+
errorBoundary, // Error fallback for a segment
|
|
112
|
+
notFoundBoundary, // Fallback for notFound() thrown below it
|
|
113
|
+
transition, // startTransition driving + view transitions (/view-transitions)
|
|
67
114
|
}) => [
|
|
68
115
|
// Route definitions here
|
|
69
116
|
],
|
|
70
117
|
);
|
|
71
118
|
```
|
|
72
119
|
|
|
73
|
-
## Router
|
|
120
|
+
## Router options
|
|
121
|
+
|
|
122
|
+
Every `createRouter()` option, grouped by concern. All are optional.
|
|
74
123
|
|
|
75
124
|
```typescript
|
|
76
125
|
interface RangoOptions<TEnv> {
|
|
77
|
-
//
|
|
78
|
-
urls
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
// --- Structure ---
|
|
127
|
+
urls?: UrlPatterns | UrlBuilder; // urls() result, or the builder fn directly
|
|
128
|
+
document?: ComponentType<{ children: ReactNode }>; // "use client" Document; a default is used when omitted
|
|
129
|
+
basename?: string; // Sub-path prefix, e.g. "/admin" (see Basename)
|
|
130
|
+
id?: string; // Router id; namespaces build output and route maps. Auto-generated.
|
|
131
|
+
|
|
132
|
+
// --- Fallbacks and errors ---
|
|
133
|
+
defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler; // When no errorBoundary() matches
|
|
134
|
+
defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler; // When no notFoundBoundary() matches
|
|
135
|
+
notFound?: ReactNode | ((props: { pathname: string }) => ReactNode); // 404 page
|
|
136
|
+
onError?: OnErrorCallback<TEnv>; // Logging/reporting hook; cannot change the response
|
|
137
|
+
|
|
138
|
+
// --- Caching ---
|
|
139
|
+
cache?:
|
|
140
|
+
| {
|
|
141
|
+
store: SegmentCacheStore;
|
|
142
|
+
enabled?: boolean;
|
|
143
|
+
searchParams?: CacheSearchParams;
|
|
144
|
+
}
|
|
145
|
+
| ((
|
|
146
|
+
env: TEnv,
|
|
147
|
+
ctx?: ExecutionContext,
|
|
148
|
+
) => {
|
|
149
|
+
store: SegmentCacheStore;
|
|
150
|
+
enabled?: boolean;
|
|
151
|
+
searchParams?: CacheSearchParams;
|
|
152
|
+
});
|
|
153
|
+
cacheProfiles?: Record<
|
|
154
|
+
string,
|
|
155
|
+
{ ttl: number; swr?: number; tags?: string[] }
|
|
156
|
+
>; // "use cache: <name>" (/use-cache)
|
|
157
|
+
|
|
158
|
+
// --- Client navigation and prefetch ---
|
|
159
|
+
defaultPrefetch?: "hover" | "viewport" | "render" | "adaptive" | "none"; // dev: "none", prod: "viewport"
|
|
160
|
+
prefetchCacheTTL?: number | false; // Seconds (default 300). false disables all prefetching.
|
|
161
|
+
prefetchCacheSize?: number; // Max cached prefetch payloads, FIFO (default 100)
|
|
162
|
+
prefetchConcurrency?: number; // Max concurrent viewport/render prefetches (default 2)
|
|
163
|
+
viewTransition?: "auto" | false; // Router-placed <ViewTransition> default (/view-transitions)
|
|
164
|
+
warmup?: boolean; // Connection warmup after idle (default true)
|
|
165
|
+
strictMode?: boolean; // Hydrate inside <React.StrictMode> (default true)
|
|
166
|
+
stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` (default "rango-state")
|
|
167
|
+
|
|
168
|
+
// --- Request handling ---
|
|
169
|
+
nonce?: (
|
|
170
|
+
request: Request,
|
|
171
|
+
env: TEnv,
|
|
172
|
+
) => string | boolean | Promise<string | boolean>;
|
|
173
|
+
version?: string; // Client/server version string; defaults to the build VERSION
|
|
174
|
+
originCheck?:
|
|
175
|
+
| boolean
|
|
176
|
+
| ((
|
|
177
|
+
ctx: OriginCheckContext<TEnv>,
|
|
178
|
+
) => boolean | Response | Promise<boolean | Response>); // default true
|
|
179
|
+
timeout?: number; // ms; shorthand for timeouts.actionMs + timeouts.renderStartMs
|
|
180
|
+
timeouts?: {
|
|
181
|
+
actionMs?: number;
|
|
182
|
+
renderStartMs?: number;
|
|
183
|
+
streamIdleMs?: number;
|
|
184
|
+
};
|
|
185
|
+
onTimeout?: (ctx: TimeoutContext<TEnv>) => Response | Promise<Response>;
|
|
186
|
+
ssr?: {
|
|
187
|
+
resolveStreaming?: (
|
|
188
|
+
ctx: ResolveStreamingContext<TEnv>,
|
|
189
|
+
) => SSRStreamMode | Promise<SSRStreamMode>;
|
|
190
|
+
};
|
|
191
|
+
theme?: ThemeConfig | true; // Light/dark theme support (/theme)
|
|
192
|
+
|
|
193
|
+
// --- Observability and debugging ---
|
|
194
|
+
debugPerformance?: boolean; // Console waterfall + Server-Timing (/observability)
|
|
195
|
+
telemetry?: TelemetrySink; // Structured lifecycle events (/observability)
|
|
196
|
+
tracing?: RouterTracingConfig; // Phase spans: createOTelTracing / createCloudflareTracing / createVercelTracing
|
|
197
|
+
debugCacheSignal?: boolean; // Dev/test only: X-Rango-Cache header for cache assertions (/testing)
|
|
198
|
+
debugShellCapture?: boolean | ((event: ShellCaptureDebugEvent) => void); // PPR capture diagnostics (/ppr)
|
|
199
|
+
}
|
|
200
|
+
```
|
|
93
201
|
|
|
94
|
-
|
|
95
|
-
defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler;
|
|
202
|
+
## Vite plugin options
|
|
96
203
|
|
|
97
|
-
|
|
98
|
-
|
|
204
|
+
`rango()` takes one options object. `preset` picks the deployment target; the
|
|
205
|
+
options below apply to every preset. Values shown are the defaults unless
|
|
206
|
+
noted.
|
|
99
207
|
|
|
100
|
-
|
|
101
|
-
|
|
208
|
+
```typescript
|
|
209
|
+
import { rango } from "@rangojs/router/vite";
|
|
210
|
+
|
|
211
|
+
rango({
|
|
212
|
+
preset: "node", // "node" | "cloudflare" | "vercel"
|
|
213
|
+
banner: true, // Print the startup banner
|
|
214
|
+
clientChunks: true, // Per-route client chunk splitting; false, or a function
|
|
215
|
+
headScripts: "preinit", // "preinit" (executing head module scripts) | "preload" (modulepreload hints only)
|
|
216
|
+
prerender: { onError: "fail" }, // "fail" | "warn" when a Prerender/Static render throws (/prerender)
|
|
217
|
+
buildEnv: false, // Build-time ctx.env for Prerender/Static handlers (/prerender)
|
|
218
|
+
});
|
|
219
|
+
```
|
|
102
220
|
|
|
103
|
-
|
|
104
|
-
|
|
221
|
+
Preset-specific options:
|
|
222
|
+
|
|
223
|
+
- `buildEnv`: an object, or a factory `({ root, mode, command, preset }) =>
|
|
224
|
+
({ env, dispose? })`, on every preset; `"auto"` (Wrangler platform proxy)
|
|
225
|
+
on `cloudflare` only.
|
|
226
|
+
- `hostRouter` (`node`, `vercel`): path to a module that exports a
|
|
227
|
+
`createHostRouter()` instance, for multi-app hosts (`/host-router`). Without
|
|
228
|
+
it, rango auto-detects a single `createHostRouter()` file when it finds
|
|
229
|
+
several `createRouter()` files.
|
|
230
|
+
- `vercel` (`vercel`): `{ runtime, maxDuration, memory, regions, functionName }`
|
|
231
|
+
for the generated function (`/vercel`).
|
|
232
|
+
|
|
233
|
+
Notes:
|
|
234
|
+
|
|
235
|
+
- `clientChunks` default groups app client components by the directory after a
|
|
236
|
+
route root (`routes/`, `app/`, `pages/`, `features/`, `handlers/`, ...), so
|
|
237
|
+
`routes/dashboard/**` becomes chunk `app-dashboard`. Flat folders such as
|
|
238
|
+
`src/components/` stay in the shared chunk. `false` restores
|
|
239
|
+
`@vitejs/plugin-rsc`'s grouping (one client chunk per router). A function
|
|
240
|
+
`(meta) => string | undefined` names the group per module (`undefined` keeps
|
|
241
|
+
the default); `directoryClientChunks(meta)`, the default strategy, is
|
|
242
|
+
exported from `@rangojs/router/vite` to fall back to.
|
|
243
|
+
- `discovery: { include?, exclude? }` filters which files the route-discovery
|
|
244
|
+
scan reads, as root-relative globs (e.g. `include: ["src/routes/**"]`).
|
|
245
|
+
`exclude` replaces the default excludes (`__tests__`, `__mocks__`, `dist`,
|
|
246
|
+
`coverage`, `*.test.*`, `*.spec.*`) rather than adding to them.
|
|
247
|
+
- `progressiveChunkSize` sets React's Fizz `progressiveChunkSize` for document
|
|
248
|
+
renders and PPR shell capture. Unset, documents whose route has a
|
|
249
|
+
`loader(Def, { ssr: false })` raise it automatically so that content stays
|
|
250
|
+
in place; see `/loader`.
|
|
251
|
+
- `@rangojs/router/vite` also exports `poke()`, a dev-server plugin: type `e`
|
|
252
|
+
then Enter (or Ctrl+R where the terminal passes it through) to full-reload
|
|
253
|
+
the browser.
|
|
254
|
+
|
|
255
|
+
## Document component
|
|
256
|
+
|
|
257
|
+
The Document renders `<html>`, `<head>`, and `<body>` around every page and
|
|
258
|
+
every error state. It must be a client component. Include `<Html.Meta />` (it
|
|
259
|
+
emits the default `charset`/`viewport` tags, `Meta` handle output, and the
|
|
260
|
+
theme script), `<Html.Scripts />` (the `Script` handle, see `/scripts`) and
|
|
261
|
+
`<Html.ScrollRestoration />` (back/forward scroll restoration):
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// src/document.tsx
|
|
265
|
+
"use client";
|
|
105
266
|
|
|
106
|
-
|
|
107
|
-
|
|
267
|
+
import type { ReactNode } from "react";
|
|
268
|
+
import { Html } from "@rangojs/router/client";
|
|
108
269
|
|
|
109
|
-
|
|
110
|
-
|
|
270
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
271
|
+
return (
|
|
272
|
+
<html lang="en" suppressHydrationWarning>
|
|
273
|
+
<head>
|
|
274
|
+
<Html.Meta />
|
|
275
|
+
<Html.Scripts />
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<Html.Scripts position="body" />
|
|
279
|
+
{children}
|
|
280
|
+
<Html.ScrollRestoration />
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
111
286
|
|
|
112
|
-
|
|
113
|
-
|
|
287
|
+
When `document` is omitted, the router uses exactly this default. To pass
|
|
288
|
+
`getKey` to `Html.ScrollRestoration`, or to leave back/forward scrolling to the
|
|
289
|
+
browser, provide your own document. Page titles
|
|
290
|
+
and other per-route tags come from the `Meta` handle, not from the Document.
|
|
291
|
+
Stylesheets and fonts: `/css`, `/tailwind`, `/fonts`.
|
|
114
292
|
|
|
115
|
-
|
|
116
|
-
warmup?: boolean;
|
|
293
|
+
## Serving requests
|
|
117
294
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
295
|
+
`router.fetch(request, input?)` is the request handler. `input` is
|
|
296
|
+
`{ env?, ctx?, vars? }`: bindings, the platform `ExecutionContext` (used for
|
|
297
|
+
`waitUntil`, `CFCacheStore`, and Cloudflare tracing), and optional initial
|
|
298
|
+
context variables.
|
|
122
299
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// opt-out for both Links and plain anchors, including explicit opt-ins.
|
|
128
|
-
// (dev: "none", production: "viewport").
|
|
129
|
-
// Per-Link props win over the router default, not a disabled container scope.
|
|
130
|
-
defaultPrefetch?: "hover" | "viewport" | "render" | "adaptive" | "none";
|
|
300
|
+
- **Node and Vercel presets:** rango owns the server entry. It imports the
|
|
301
|
+
named `router` export and calls it for you. Write no entry file.
|
|
302
|
+
- **Cloudflare preset:** you own the Worker entry. Pass `env` and `ctx`
|
|
303
|
+
explicitly:
|
|
131
304
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
) => string | true | Promise<string | true>;
|
|
305
|
+
```typescript
|
|
306
|
+
// src/worker.rsc.tsx
|
|
307
|
+
import { router } from "./router";
|
|
308
|
+
import type { AppBindings } from "./env";
|
|
137
309
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
310
|
+
export default {
|
|
311
|
+
fetch(request, env, ctx) {
|
|
312
|
+
return router.fetch(request, { env, ctx });
|
|
313
|
+
},
|
|
314
|
+
} satisfies ExportedHandler<AppBindings>;
|
|
141
315
|
```
|
|
142
316
|
|
|
143
|
-
|
|
317
|
+
Do not write `export default { fetch: router.fetch }` on Workers. The runtime
|
|
318
|
+
calls `fetch(request, env, ctx)`, so the bindings object would land in the
|
|
319
|
+
`input` slot: handlers see an empty `ctx.env`, and the `ExecutionContext` is
|
|
320
|
+
lost. See `/cloudflare` for
|
|
321
|
+
the full Workers setup.
|
|
144
322
|
|
|
145
|
-
|
|
323
|
+
## Basename (sub-path deployment)
|
|
324
|
+
|
|
325
|
+
When the app is served under a sub-path (e.g. `/admin` or `/v2`), set `basename`:
|
|
146
326
|
|
|
147
327
|
```typescript
|
|
148
|
-
const router = createRouter({
|
|
328
|
+
export const router = createRouter({
|
|
149
329
|
basename: "/admin",
|
|
150
330
|
document: Document,
|
|
151
331
|
}).routes(({ path, include }) => [
|
|
@@ -167,79 +347,14 @@ Router-owned APIs are basename-aware:
|
|
|
167
347
|
- `useRouter().push("/users")` navigates to `/admin/users`
|
|
168
348
|
- Route names stay unprefixed (`"home"`, not `"admin.home"`)
|
|
169
349
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Using the Request Handler
|
|
174
|
-
|
|
175
|
-
The router provides a `fetch` method to handle RSC requests:
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
// src/router.tsx
|
|
179
|
-
import { createRouter } from "@rangojs/router";
|
|
180
|
-
import { Document } from "./document";
|
|
181
|
-
import { urlpatterns } from "./urls";
|
|
182
|
-
|
|
183
|
-
export const router = createRouter({
|
|
184
|
-
document: Document,
|
|
185
|
-
urls: urlpatterns,
|
|
186
|
-
nonce: () => true, // Auto-generate nonce for CSP
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// src/worker.tsx (Cloudflare Workers)
|
|
190
|
-
import { router } from "./router";
|
|
191
|
-
|
|
192
|
-
export default { fetch: router.fetch };
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Document Component
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// src/document.tsx
|
|
199
|
-
import type { ReactNode } from "react";
|
|
200
|
-
|
|
201
|
-
export function Document({ children }: { children: ReactNode }) {
|
|
202
|
-
return (
|
|
203
|
-
<html lang="en">
|
|
204
|
-
<head>
|
|
205
|
-
<meta charSet="utf-8" />
|
|
206
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
207
|
-
<title>My App</title>
|
|
208
|
-
</head>
|
|
209
|
-
<body>
|
|
210
|
-
<div id="root">{children}</div>
|
|
211
|
-
</body>
|
|
212
|
-
</html>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
```
|
|
350
|
+
`href()` is a raw path helper and does **not** add the basename. Use
|
|
351
|
+
`reverse()` or `<Link>` for basename-aware URLs.
|
|
216
352
|
|
|
217
|
-
##
|
|
353
|
+
## Cache store configuration
|
|
218
354
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
import { Document } from "./document";
|
|
223
|
-
import { urlpatterns } from "./urls";
|
|
224
|
-
|
|
225
|
-
export const router = createRouter<AppBindings>({
|
|
226
|
-
document: Document,
|
|
227
|
-
urls: urlpatterns,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// src/worker.tsx
|
|
231
|
-
import { router } from "./router";
|
|
232
|
-
|
|
233
|
-
export default {
|
|
234
|
-
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
235
|
-
return router.fetch(request, { env, ctx });
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### With Dynamic Cache Configuration
|
|
241
|
-
|
|
242
|
-
For per-request cache configuration (e.g., Cloudflare Workers with ExecutionContext):
|
|
355
|
+
Pass a static config, or a factory when the store needs per-request bindings.
|
|
356
|
+
The factory receives `(env, ctx)`; `ctx` is typed optional, so assert it where
|
|
357
|
+
the platform always provides it:
|
|
243
358
|
|
|
244
359
|
```typescript
|
|
245
360
|
// src/router.tsx
|
|
@@ -249,37 +364,31 @@ import { CFCacheStore } from "@rangojs/router/cache";
|
|
|
249
364
|
export const router = createRouter<AppBindings>({
|
|
250
365
|
document: Document,
|
|
251
366
|
urls: urlpatterns,
|
|
252
|
-
// Cache config receives (env, ctx) separately
|
|
253
367
|
cache: (_env, ctx) => ({
|
|
254
368
|
store: new CFCacheStore({ ctx: ctx!, defaults: { ttl: 60 } }),
|
|
255
369
|
}),
|
|
256
370
|
});
|
|
257
|
-
|
|
258
|
-
// src/worker.tsx
|
|
259
|
-
import { router } from "./router";
|
|
260
|
-
|
|
261
|
-
export default {
|
|
262
|
-
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
263
|
-
return router.fetch(request, { env, ctx });
|
|
264
|
-
},
|
|
265
|
-
};
|
|
266
371
|
```
|
|
267
372
|
|
|
268
|
-
|
|
373
|
+
`searchParams` controls which query params key the cache (`"all"` by default,
|
|
374
|
+
`"none"`, `{ include: [...] }`, or `{ exclude: [...] }`).
|
|
375
|
+
`TRACKING_SEARCH_PARAMS` from `@rangojs/router` is a ready-made exclude list
|
|
376
|
+
for utm/click-id params. See `/caching` and `/cache-guide`.
|
|
377
|
+
|
|
378
|
+
## Complete example
|
|
269
379
|
|
|
270
380
|
```typescript
|
|
271
381
|
// src/urls.tsx
|
|
272
382
|
import { urls } from "@rangojs/router";
|
|
273
|
-
import { Outlet } from "@rangojs/router/client";
|
|
274
383
|
|
|
275
384
|
// Pages
|
|
276
385
|
import { HomePage } from "./pages/home";
|
|
277
386
|
import { AboutPage } from "./pages/about";
|
|
278
|
-
import { BlogIndexPage, BlogPostPage } from "./pages/blog";
|
|
387
|
+
import { BlogIndexPage, BlogPostPage, BlogPostSkeleton } from "./pages/blog";
|
|
279
388
|
|
|
280
|
-
// Layouts
|
|
281
|
-
import { RootLayout } from "./layouts/RootLayout";
|
|
389
|
+
// Layouts and slots
|
|
282
390
|
import { BlogLayout } from "./layouts/BlogLayout";
|
|
391
|
+
import { BlogSidebar } from "./components/BlogSidebar";
|
|
283
392
|
|
|
284
393
|
// Loaders
|
|
285
394
|
import { BlogPostLoader, BlogSidebarLoader } from "./loaders/blog";
|
|
@@ -291,7 +400,7 @@ export const urlpatterns = urls(({ path, layout, parallel, loader, loading, cach
|
|
|
291
400
|
|
|
292
401
|
// Blog with layout and loaders
|
|
293
402
|
layout(<BlogLayout />, () => [
|
|
294
|
-
// Sidebar as parallel
|
|
403
|
+
// Sidebar as a parallel slot (the layout renders <ParallelOutlet name="@sidebar" />)
|
|
295
404
|
parallel({ "@sidebar": () => <BlogSidebar /> }, () => [
|
|
296
405
|
loader(BlogSidebarLoader),
|
|
297
406
|
]),
|
|
@@ -314,14 +423,17 @@ import { createRouter } from "@rangojs/router";
|
|
|
314
423
|
import { Document } from "./document";
|
|
315
424
|
import { urlpatterns } from "./urls";
|
|
316
425
|
|
|
317
|
-
const router = createRouter({
|
|
426
|
+
export const router = createRouter({
|
|
318
427
|
document: Document,
|
|
319
428
|
urls: urlpatterns,
|
|
320
429
|
|
|
321
|
-
|
|
430
|
+
// Server-rendered fallback: receives { error } only. There is no reset()
|
|
431
|
+
// for server-side errors, so offer a link or a reload instead.
|
|
432
|
+
defaultErrorBoundary: ({ error }) => (
|
|
322
433
|
<div>
|
|
323
434
|
<h1>Something went wrong</h1>
|
|
324
|
-
<
|
|
435
|
+
<p>{error.message}</p>
|
|
436
|
+
<a href="/">Go home</a>
|
|
325
437
|
</div>
|
|
326
438
|
),
|
|
327
439
|
|
|
@@ -332,20 +444,20 @@ const router = createRouter({
|
|
|
332
444
|
</div>
|
|
333
445
|
),
|
|
334
446
|
});
|
|
335
|
-
|
|
336
|
-
export default router;
|
|
337
447
|
```
|
|
338
448
|
|
|
339
|
-
## Not
|
|
449
|
+
## Not found handling
|
|
340
450
|
|
|
341
451
|
Two distinct 404 scenarios:
|
|
342
452
|
|
|
343
|
-
**1. No route matches the URL
|
|
453
|
+
**1. No route matches the URL.** The router renders the `notFound` option from
|
|
454
|
+
`createRouter()` (or `<h1>Not Found</h1>` when unset) with status 404.
|
|
344
455
|
|
|
345
|
-
**2. A handler
|
|
456
|
+
**2. A handler or loader calls `notFound()`.** The route matched but the data
|
|
457
|
+
does not exist (e.g. an unknown product id).
|
|
346
458
|
|
|
347
459
|
```typescript
|
|
348
|
-
import { notFound } from "@rangojs/router";
|
|
460
|
+
import { createLoader, notFound } from "@rangojs/router";
|
|
349
461
|
|
|
350
462
|
// In a handler
|
|
351
463
|
path("/product/:slug", async (ctx) => {
|
|
@@ -354,7 +466,7 @@ path("/product/:slug", async (ctx) => {
|
|
|
354
466
|
return <ProductPage product={product} />;
|
|
355
467
|
});
|
|
356
468
|
|
|
357
|
-
// In a loader
|
|
469
|
+
// In a loader: data-dependent authority lives with the data
|
|
358
470
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
359
471
|
if (!(await exists(ctx.params.slug))) notFound("Product not found");
|
|
360
472
|
return getProduct(ctx.params.slug);
|
|
@@ -363,20 +475,19 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
363
475
|
|
|
364
476
|
### Fallback chain for `notFound()`
|
|
365
477
|
|
|
366
|
-
When `notFound()` is thrown, the router
|
|
478
|
+
When `notFound()` is thrown, the router uses the first of:
|
|
367
479
|
|
|
368
|
-
1. **`notFoundBoundary()
|
|
369
|
-
2. **`defaultNotFoundBoundary
|
|
370
|
-
3. **`notFound
|
|
371
|
-
4.
|
|
480
|
+
1. **`notFoundBoundary()`**: nearest boundary in the route tree
|
|
481
|
+
2. **`defaultNotFoundBoundary`**: from `createRouter()`
|
|
482
|
+
3. **`notFound`**: from `createRouter()` (the same page used for no-route-match)
|
|
483
|
+
4. **`<h1>Not Found</h1>`**: built-in fallback
|
|
372
484
|
|
|
373
|
-
Handler and no-match
|
|
374
|
-
on a document
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
stays 200 — the client owns presentation there). See `/loader` → "Loader
|
|
485
|
+
Handler-thrown and no-match 404s always set HTTP status 404. A loader-thrown
|
|
486
|
+
`notFound()` on a document request always renders the not-found UI, but the
|
|
487
|
+
404 status is set only if the loader rejects before the document Response is
|
|
488
|
+
constructed (loaders stream). Register the loader as `loader(Def, { ssr: false })` to
|
|
489
|
+
make the 404 status deterministic. On client navigations the 404 UI swaps in
|
|
490
|
+
with the URL preserved and the payload stays 200. See `/loader` → "Loader
|
|
380
491
|
Authority".
|
|
381
492
|
|
|
382
493
|
### notFoundBoundary
|
|
@@ -384,7 +495,7 @@ Authority".
|
|
|
384
495
|
Wrap routes with `notFoundBoundary()` for route-specific not-found UI:
|
|
385
496
|
|
|
386
497
|
```typescript
|
|
387
|
-
urls(({ path, layout }) => [
|
|
498
|
+
urls(({ path, layout, notFoundBoundary }) => [
|
|
388
499
|
layout(ShopLayout, () => [
|
|
389
500
|
notFoundBoundary(({ notFound: info }) => (
|
|
390
501
|
<div>
|
|
@@ -392,20 +503,21 @@ urls(({ path, layout }) => [
|
|
|
392
503
|
<p>{info.message}</p>
|
|
393
504
|
</div>
|
|
394
505
|
)),
|
|
395
|
-
path("/product/:slug", ProductPage),
|
|
506
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
396
507
|
]),
|
|
397
508
|
]);
|
|
398
509
|
```
|
|
399
510
|
|
|
400
|
-
|
|
511
|
+
The handler receives `{ notFound: NotFoundInfo }`: `message`, `segmentId`,
|
|
512
|
+
`segmentType`, and `pathname` (optional).
|
|
401
513
|
|
|
402
|
-
## Including
|
|
514
|
+
## Including sub-patterns
|
|
403
515
|
|
|
404
516
|
```typescript
|
|
405
517
|
// src/urls/shop.tsx
|
|
406
518
|
import { urls } from "@rangojs/router";
|
|
407
519
|
|
|
408
|
-
export const shopPatterns = urls(({ path
|
|
520
|
+
export const shopPatterns = urls(({ path }) => [
|
|
409
521
|
path("/", ShopIndex, { name: "index" }),
|
|
410
522
|
path("/product/:slug", ProductPage, { name: "product" }),
|
|
411
523
|
]);
|
|
@@ -416,23 +528,24 @@ import { shopPatterns } from "./urls/shop";
|
|
|
416
528
|
|
|
417
529
|
export const urlpatterns = urls(({ path, include }) => [
|
|
418
530
|
path("/", HomePage, { name: "home" }),
|
|
419
|
-
include("/shop", shopPatterns, { name: "shop" }),
|
|
531
|
+
include("/shop", shopPatterns, { name: "shop" }), // "shop.index", "shop.product"
|
|
420
532
|
]);
|
|
421
533
|
```
|
|
422
534
|
|
|
423
|
-
`include()` also accepts an async provider
|
|
424
|
-
chunk, imported on the first request
|
|
535
|
+
`include()` also accepts an async provider that code-splits the group into its
|
|
536
|
+
own chunk, imported on the first request that reaches the prefix instead of at
|
|
537
|
+
startup:
|
|
425
538
|
|
|
426
539
|
```typescript
|
|
427
540
|
// urls/shop.tsx: `export default shopPatterns`
|
|
428
541
|
include("/shop", () => import("./urls/shop"), { name: "shop" }),
|
|
429
542
|
```
|
|
430
543
|
|
|
431
|
-
Build-time discovery still
|
|
544
|
+
Build-time discovery still awaits the provider, so route types, `href()`, and
|
|
432
545
|
prerender see every route in the split group. Reach for it when a group is a
|
|
433
|
-
large, independently
|
|
546
|
+
large, independently loadable unit. See `/composability`.
|
|
434
547
|
|
|
435
|
-
## Environment
|
|
548
|
+
## Environment types
|
|
436
549
|
|
|
437
550
|
```typescript
|
|
438
551
|
// Bindings passed as TEnv to createRouter<TEnv>()
|
|
@@ -441,17 +554,17 @@ interface AppBindings {
|
|
|
441
554
|
KV: KVNamespace;
|
|
442
555
|
}
|
|
443
556
|
|
|
444
|
-
//
|
|
557
|
+
// Context variables set by middleware via ctx.set()
|
|
445
558
|
interface AppVariables {
|
|
446
559
|
user?: { id: string; name: string };
|
|
447
560
|
}
|
|
448
561
|
|
|
449
|
-
const router = createRouter<AppBindings>({
|
|
562
|
+
export const router = createRouter<AppBindings>({
|
|
450
563
|
document: Document,
|
|
451
564
|
urls: urlpatterns,
|
|
452
565
|
});
|
|
453
566
|
|
|
454
|
-
// Register
|
|
567
|
+
// Register once so ctx.env and ctx.get() are typed everywhere
|
|
455
568
|
declare global {
|
|
456
569
|
namespace Rango {
|
|
457
570
|
interface Env extends AppBindings {}
|
|
@@ -460,44 +573,37 @@ declare global {
|
|
|
460
573
|
}
|
|
461
574
|
```
|
|
462
575
|
|
|
463
|
-
|
|
576
|
+
See `/typesafety` for route-name and search-param typing.
|
|
464
577
|
|
|
465
|
-
|
|
466
|
-
don't pay handshake costs.
|
|
578
|
+
## Connection warmup
|
|
467
579
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
the TLS connection before the next navigation. The server responds with 204 No Content
|
|
471
|
-
before any middleware or routing runs.
|
|
580
|
+
Enabled by default. It keeps the TCP+TLS connection warm so the first
|
|
581
|
+
navigation after an idle period does not pay the handshake.
|
|
472
582
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
});
|
|
583
|
+
After 60s without user interaction the connection is marked cold. When the user
|
|
584
|
+
returns (tab becomes visible, or first mouse move/touch), the client sends
|
|
585
|
+
`HEAD /?_rsc_warmup` to the page's origin. The router answers `204 No Content`
|
|
586
|
+
before middleware, nonce resolution, or routing runs. The request only needs to
|
|
587
|
+
reach the origin to warm the connection, so it works with `basename` too.
|
|
479
588
|
|
|
589
|
+
```typescript
|
|
480
590
|
// Disable warmup
|
|
481
|
-
const router = createRouter({
|
|
591
|
+
export const router = createRouter({
|
|
482
592
|
document: Document,
|
|
483
593
|
urls: urlpatterns,
|
|
484
594
|
warmup: false,
|
|
485
595
|
});
|
|
486
596
|
```
|
|
487
597
|
|
|
488
|
-
|
|
489
|
-
with subpath deployments (reverse proxy, base path).
|
|
598
|
+
## Telemetry and tracing
|
|
490
599
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
The router emits structured lifecycle events through a pluggable telemetry sink.
|
|
494
|
-
Zero overhead when not configured.
|
|
600
|
+
Two independent slots. `telemetry` receives discrete lifecycle events;
|
|
601
|
+
`tracing` wraps router phases in spans. Both cost nothing when unset.
|
|
495
602
|
|
|
496
603
|
```typescript
|
|
497
|
-
// Console sink for development
|
|
498
604
|
import { createRouter, createConsoleSink } from "@rangojs/router";
|
|
499
605
|
|
|
500
|
-
const router = createRouter({
|
|
606
|
+
export const router = createRouter({
|
|
501
607
|
document: Document,
|
|
502
608
|
urls: urlpatterns,
|
|
503
609
|
telemetry: createConsoleSink(),
|
|
@@ -505,8 +611,7 @@ const router = createRouter({
|
|
|
505
611
|
```
|
|
506
612
|
|
|
507
613
|
```typescript
|
|
508
|
-
// OpenTelemetry
|
|
509
|
-
// discrete-fact spans via the telemetry sink.
|
|
614
|
+
// OpenTelemetry: phase spans via tracing, discrete-fact spans via telemetry.
|
|
510
615
|
import {
|
|
511
616
|
createRouter,
|
|
512
617
|
createOTelTracing,
|
|
@@ -516,7 +621,7 @@ import { trace } from "@opentelemetry/api";
|
|
|
516
621
|
|
|
517
622
|
const tracer = trace.getTracer("my-app");
|
|
518
623
|
|
|
519
|
-
const router = createRouter({
|
|
624
|
+
export const router = createRouter({
|
|
520
625
|
document: Document,
|
|
521
626
|
urls: urlpatterns,
|
|
522
627
|
tracing: createOTelTracing(tracer),
|
|
@@ -525,45 +630,46 @@ const router = createRouter({
|
|
|
525
630
|
```
|
|
526
631
|
|
|
527
632
|
```typescript
|
|
528
|
-
//
|
|
529
|
-
// (no @opentelemetry/api dependency); the telemetry slot is unchanged.
|
|
633
|
+
// Cloudflare Workers: native custom spans, no @opentelemetry/api dependency.
|
|
530
634
|
// On Vercel (Node runtime) use createVercelTracing() from @rangojs/router/vercel.
|
|
531
635
|
import { createCloudflareTracing } from "@rangojs/router/cloudflare";
|
|
532
636
|
|
|
533
|
-
const router = createRouter({
|
|
637
|
+
export const router = createRouter({
|
|
534
638
|
document: Document,
|
|
535
639
|
urls: urlpatterns,
|
|
536
|
-
tracing: createCloudflareTracing(), // { spans: { ssr: false } } to
|
|
640
|
+
tracing: createCloudflareTracing(), // { spans: { ssr: false } } to turn phases off
|
|
537
641
|
});
|
|
538
642
|
```
|
|
539
643
|
|
|
540
644
|
```typescript
|
|
541
645
|
// Custom sink
|
|
542
|
-
const router = createRouter({
|
|
646
|
+
export const router = createRouter({
|
|
543
647
|
telemetry: {
|
|
544
648
|
emit(event) {
|
|
545
|
-
|
|
546
|
-
myTracer.record(event);
|
|
649
|
+
myMetrics.record(event);
|
|
547
650
|
},
|
|
548
651
|
},
|
|
549
652
|
});
|
|
550
653
|
```
|
|
551
654
|
|
|
552
|
-
Events
|
|
553
|
-
`
|
|
655
|
+
Events: `request.start/end/error`, `loader.start/end/error`, `handler.error`,
|
|
656
|
+
`cache.decision`, `revalidation.decision`, `request.timeout`, and
|
|
657
|
+
`request.origin-rejected`. Span names, attributes, and debugging recipes are in
|
|
658
|
+
`/observability`.
|
|
554
659
|
|
|
555
|
-
## SSR
|
|
660
|
+
## SSR streaming policy
|
|
556
661
|
|
|
557
|
-
|
|
662
|
+
HTML responses stream by default. `ssr.resolveStreaming` picks, per request,
|
|
663
|
+
whether to stream or wait for the whole page:
|
|
558
664
|
|
|
559
665
|
```typescript
|
|
560
666
|
import { createRouter, type SSRStreamMode } from "@rangojs/router";
|
|
561
667
|
|
|
562
|
-
const router = createRouter({
|
|
668
|
+
export const router = createRouter({
|
|
563
669
|
ssr: {
|
|
564
|
-
resolveStreaming: ({ request }) => {
|
|
670
|
+
resolveStreaming: ({ request }): SSRStreamMode => {
|
|
565
671
|
const ua = request.headers.get("user-agent") ?? "";
|
|
566
|
-
//
|
|
672
|
+
// Crawlers that cannot process streamed HTML get a fully resolved page
|
|
567
673
|
if (/Googlebot|bingbot/i.test(ua)) return "allReady";
|
|
568
674
|
return "stream";
|
|
569
675
|
},
|
|
@@ -573,9 +679,81 @@ const router = createRouter({
|
|
|
573
679
|
|
|
574
680
|
`SSRStreamMode` is `"stream" | "allReady"`:
|
|
575
681
|
|
|
576
|
-
- `"stream"` (default)
|
|
577
|
-
|
|
682
|
+
- `"stream"` (default): flush HTML as React renders. Suspense fallbacks appear
|
|
683
|
+
first, then resolved content streams in. Fastest TTFB for real users.
|
|
684
|
+
- `"allReady"`: wait for every Suspense boundary (`stream.allReady`) before
|
|
685
|
+
sending bytes. The full page arrives at once.
|
|
578
686
|
|
|
579
|
-
The resolver receives `{ request, env, url }` and may be
|
|
687
|
+
The resolver receives `{ request, env, url }` and may be async. It runs only
|
|
688
|
+
for HTML document responses; RSC payloads (navigations, prefetches, `__rsc`
|
|
689
|
+
requests) and response routes are unaffected.
|
|
690
|
+
|
|
691
|
+
## Timeouts
|
|
692
|
+
|
|
693
|
+
Off by default. A timed-out action or render start returns a `504` (with an
|
|
694
|
+
`X-Rango-Timeout-Phase` header) unless `onTimeout` returns its own response.
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
export const router = createRouter({
|
|
698
|
+
timeout: 10_000, // actionMs + renderStartMs
|
|
699
|
+
timeouts: {
|
|
700
|
+
renderStartMs: 8_000, // overrides the shorthand
|
|
701
|
+
streamIdleMs: 30_000, // opt-in; not covered by `timeout`
|
|
702
|
+
},
|
|
703
|
+
onTimeout: ({ phase }) =>
|
|
704
|
+
Response.json({ error: "timeout", phase }, { status: 504 }),
|
|
705
|
+
});
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
- `actionMs`: server action execution.
|
|
709
|
+
- `renderStartMs`: time until the response is produced.
|
|
710
|
+
- `streamIdleMs`: after handoff, no chunk reached the client for this long.
|
|
711
|
+
The stream is errored and the render canceled; `onTimeout` does not run (the
|
|
712
|
+
response has already started). It is reported via `onError` and the
|
|
713
|
+
`request.timeout` event. A slow client counts as idle too, so use generous
|
|
714
|
+
budgets.
|
|
715
|
+
|
|
716
|
+
`0` disables a phase. `RouterTimeoutError` is exported from `@rangojs/router`.
|
|
717
|
+
|
|
718
|
+
## Origin check
|
|
719
|
+
|
|
720
|
+
On by default. Before running a server action, a fetchable loader, or a
|
|
721
|
+
progressive-enhancement form post, the router compares the `Origin` header (or
|
|
722
|
+
`Referer`) with `Host` and the request protocol. Requests with neither header
|
|
723
|
+
are allowed. `X-Forwarded-*` headers are not trusted.
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
export const router = createRouter<AppBindings>({
|
|
727
|
+
// Behind a proxy that rewrites Host, supply your own rule:
|
|
728
|
+
originCheck: ({ request, url, env, defaultCheck }) => {
|
|
729
|
+
if (!env.TRUST_PROXY) return defaultCheck();
|
|
730
|
+
const origin = request.headers.get("origin");
|
|
731
|
+
if (!origin) return true;
|
|
732
|
+
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
733
|
+
return origin === `${url.protocol}//${host}`;
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
```
|
|
580
737
|
|
|
581
|
-
|
|
738
|
+
Return `true` to allow, `false` for the default 403, or a `Response` to reject
|
|
739
|
+
with your own. `phase` is `"action" | "loader" | "pe-form"`. `false` disables
|
|
740
|
+
the check.
|
|
741
|
+
|
|
742
|
+
## Other options
|
|
743
|
+
|
|
744
|
+
- `defaultPrefetch`: strategy for Links without a `prefetch` prop (`/links`).
|
|
745
|
+
Production default `"viewport"` renders every visible Link's route on the
|
|
746
|
+
server; choose `"hover"`, `"adaptive"`, or `"none"` when that cost matters.
|
|
747
|
+
- `prefetchCacheTTL`, `prefetchCacheSize`, `prefetchConcurrency`: lifetime,
|
|
748
|
+
entry count, and parallelism of the client prefetch cache.
|
|
749
|
+
`prefetchCacheTTL: false` turns prefetching off entirely, including per-Link
|
|
750
|
+
opt-ins and `useRouter().prefetch()`.
|
|
751
|
+
- `strictMode: false`: hydrate without `<React.StrictMode>`, e.g. to get exact
|
|
752
|
+
render counts in development. Production behavior is unchanged.
|
|
753
|
+
- `stateCookiePrefix`: rename the state cookie that keys the client caches. The
|
|
754
|
+
`_{routerId}` suffix is always kept so sibling apps on one origin do not
|
|
755
|
+
collide.
|
|
756
|
+
- `nonce: () => true`: generate a CSP nonce per request and apply it to the
|
|
757
|
+
router's inline scripts. Return a string to supply your own, or
|
|
758
|
+
`false`/`""` to skip it for one request. Middleware reads it with
|
|
759
|
+
`ctx.get(nonce)` (`nonce` token from `@rangojs/router`).
|