@rangojs/router 0.0.0-experimental.10
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/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: response-routes
|
|
3
|
+
description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses
|
|
4
|
+
argument-hint: [json|text|html|xml|md|image|stream]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Response Routes
|
|
8
|
+
|
|
9
|
+
Response routes skip the RSC pipeline entirely. Use them for JSON APIs, plain text endpoints,
|
|
10
|
+
XML feeds, image proxies, and any route that returns a `Response` instead of React components.
|
|
11
|
+
|
|
12
|
+
## Route-Level Tags: path.json(), path.text(), etc.
|
|
13
|
+
|
|
14
|
+
Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { urls, RouterError } from "@rangojs/router/server";
|
|
18
|
+
|
|
19
|
+
export const urlpatterns = urls(({ path, layout, include }) => [
|
|
20
|
+
// RSC routes (normal)
|
|
21
|
+
path("/", HomePage, { name: "home" }),
|
|
22
|
+
path("/about", AboutPage, { name: "about" }),
|
|
23
|
+
|
|
24
|
+
// JSON API route (inline, alongside RSC routes)
|
|
25
|
+
path.json("/api/status", (ctx) => ({
|
|
26
|
+
status: "ok",
|
|
27
|
+
timestamp: Date.now(),
|
|
28
|
+
}), { name: "status" }),
|
|
29
|
+
|
|
30
|
+
// Text route
|
|
31
|
+
path.text("/robots.txt", (ctx) => {
|
|
32
|
+
return "User-agent: *\nAllow: /\nDisallow: /api/\n";
|
|
33
|
+
}, { name: "robots" }),
|
|
34
|
+
|
|
35
|
+
// Markdown route
|
|
36
|
+
path.md("/docs/:slug.md", (ctx) => {
|
|
37
|
+
return `# ${ctx.params.slug}\n\nDocumentation content here.`;
|
|
38
|
+
}, { name: "docs" }),
|
|
39
|
+
|
|
40
|
+
// Response route (full control, returns Response directly)
|
|
41
|
+
path.image("/og/:slug.png", async (ctx) => {
|
|
42
|
+
const image = await generateOgImage(ctx.params.slug);
|
|
43
|
+
return new Response(image, {
|
|
44
|
+
headers: { "Content-Type": "image/png", "Cache-Control": "public, max-age=86400" },
|
|
45
|
+
});
|
|
46
|
+
}, { name: "ogImage" }),
|
|
47
|
+
]);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Available Tags
|
|
51
|
+
|
|
52
|
+
| Tag | Usage | Handler returns | Auto-wrap |
|
|
53
|
+
|-----|-------|-----------------|-----------|
|
|
54
|
+
| `json` | `path.json()` | plain object/array | `{ data: T }` envelope |
|
|
55
|
+
| `text` | `path.text()` | string | text/plain Response |
|
|
56
|
+
| `html` | `path.html()` | string | text/html Response |
|
|
57
|
+
| `xml` | `path.xml()` | string | application/xml Response |
|
|
58
|
+
| `md` | `path.md()` | string | text/markdown Response |
|
|
59
|
+
| `image` | `path.image()` | Response | pass-through |
|
|
60
|
+
| `stream` | `path.stream()` | Response | pass-through |
|
|
61
|
+
| `any` | `path.any()` | Response | pass-through |
|
|
62
|
+
|
|
63
|
+
## ResponseHandlerContext
|
|
64
|
+
|
|
65
|
+
Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.res`):
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface ResponseHandlerContext<TParams, TEnv> {
|
|
69
|
+
request: Request;
|
|
70
|
+
params: TParams; // Typed from URL pattern
|
|
71
|
+
env: Bindings; // Extracted from RouterEnv (DB, KV, etc.)
|
|
72
|
+
searchParams: URLSearchParams;
|
|
73
|
+
url: URL;
|
|
74
|
+
pathname: string;
|
|
75
|
+
href: (name: string, params?: Record<string, string>) => string;
|
|
76
|
+
header: (name: string, value: string) => void;
|
|
77
|
+
setCookie: (name: string, value: string, options?: CookieOptions) => void;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Setting Headers and Cookies
|
|
82
|
+
|
|
83
|
+
String-returning handlers (json, text, html, xml, md) can set custom headers and cookies
|
|
84
|
+
without constructing a full Response:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
path.md("/docs/:slug.md", (ctx) => {
|
|
88
|
+
ctx.header("Cache-Control", "public, max-age=3600");
|
|
89
|
+
ctx.setCookie("last-doc", ctx.params.slug, { path: "/" });
|
|
90
|
+
return `# ${ctx.params.slug}\n\nContent here.`;
|
|
91
|
+
}, { name: "docs" });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Headers and cookies set via `ctx.header()` / `ctx.setCookie()` are merged into the
|
|
95
|
+
auto-wrapped Response. If the handler returns a `Response` directly, these are ignored
|
|
96
|
+
(use the Response headers instead).
|
|
97
|
+
|
|
98
|
+
### Environment Type Extraction
|
|
99
|
+
|
|
100
|
+
`env` extracts bindings from `RouterEnv`, not the full env:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
type AppEnv = RouterEnv<{ DB: D1Database; KV: KVNamespace }, { user: User }>;
|
|
104
|
+
|
|
105
|
+
// In a response handler:
|
|
106
|
+
path.json("/api/data", (ctx) => {
|
|
107
|
+
ctx.env.DB; // D1Database (bindings extracted)
|
|
108
|
+
ctx.env.KV; // KVNamespace
|
|
109
|
+
// ctx.env.user -- NOT available (variables are not on response ctx.env)
|
|
110
|
+
return { data: "ok" };
|
|
111
|
+
}, { name: "data" });
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## JSON Envelope
|
|
115
|
+
|
|
116
|
+
`path.json()` handlers return plain data. The framework auto-wraps it
|
|
117
|
+
in a `ResponseEnvelope<T>` discriminated union:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Success: HTTP 200
|
|
121
|
+
{ "data": { "status": "ok", "timestamp": 1700000000 } }
|
|
122
|
+
|
|
123
|
+
// Error: HTTP 404 (or whatever status RouterError specifies)
|
|
124
|
+
{ "error": { "message": "Product 999 not found", "code": "NOT_FOUND" } }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Error Handling with RouterError
|
|
128
|
+
|
|
129
|
+
Throw `RouterError` to return structured error envelopes:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { RouterError } from "@rangojs/router/server";
|
|
133
|
+
|
|
134
|
+
path.json("/api/users/:id", (ctx) => {
|
|
135
|
+
const user = users.get(ctx.params.id);
|
|
136
|
+
if (!user) {
|
|
137
|
+
throw new RouterError("NOT_FOUND", `User ${ctx.params.id} not found`, { status: 404 });
|
|
138
|
+
}
|
|
139
|
+
if (!hasPermission(ctx)) {
|
|
140
|
+
throw new RouterError("FORBIDDEN", "Access denied", { status: 403 });
|
|
141
|
+
}
|
|
142
|
+
return user;
|
|
143
|
+
}, { name: "user" });
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Returning Response Directly
|
|
147
|
+
|
|
148
|
+
JSON handlers can return `Response` to bypass auto-wrap (custom status, headers, streaming):
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
path.json("/api/export", (ctx) => {
|
|
152
|
+
const csv = generateCsv();
|
|
153
|
+
return new Response(csv, {
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "text/csv",
|
|
156
|
+
"Content-Disposition": "attachment; filename=export.csv",
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}, { name: "export" });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Client-Side Type Safety
|
|
163
|
+
|
|
164
|
+
### ResponseEnvelope and isResponseError
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
"use client";
|
|
168
|
+
import type { ResponseEnvelope, ResponseError } from "@rangojs/router/client";
|
|
169
|
+
import { isResponseError } from "@rangojs/router/client";
|
|
170
|
+
|
|
171
|
+
// Fetch a typed response
|
|
172
|
+
const res = await fetch("/api/products/1");
|
|
173
|
+
const result: ResponseEnvelope<Product> = await res.json();
|
|
174
|
+
|
|
175
|
+
if (isResponseError(result)) {
|
|
176
|
+
// result.error: ResponseError (message, code?, type?)
|
|
177
|
+
// result.data: undefined
|
|
178
|
+
console.error(result.error.message);
|
|
179
|
+
} else {
|
|
180
|
+
// result.data: Product
|
|
181
|
+
// result.error: undefined
|
|
182
|
+
console.log(result.data.name);
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### RouteResponse (scoped lookup by route name)
|
|
187
|
+
|
|
188
|
+
Look up response type from a `path.json()` or `path.text()` module by route name:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import type { RouteResponse } from "@rangojs/router/server";
|
|
192
|
+
|
|
193
|
+
// From the apiPatterns module (before include)
|
|
194
|
+
type HealthData = RouteResponse<typeof apiPatterns, "health">;
|
|
195
|
+
// = ResponseEnvelope<{ status: string; timestamp: number }>
|
|
196
|
+
|
|
197
|
+
type ProductsData = RouteResponse<typeof apiPatterns, "products">;
|
|
198
|
+
// = ResponseEnvelope<{ id: string; name: string; price: number }[]>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### PathResponse (global lookup by URL pattern)
|
|
202
|
+
|
|
203
|
+
Look up response type from the merged route map by URL pattern:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import type { PathResponse } from "@rangojs/router/client";
|
|
207
|
+
|
|
208
|
+
// After include("/api", apiPatterns) in main urls
|
|
209
|
+
type Health = PathResponse<"/api/health">;
|
|
210
|
+
// = ResponseEnvelope<{ status: string; timestamp: number }>
|
|
211
|
+
|
|
212
|
+
// RSC routes return ResponseEnvelope<never>
|
|
213
|
+
type Home = PathResponse<"/">;
|
|
214
|
+
// = ResponseEnvelope<never>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### ParamsFor with Response Routes
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import type { ParamsFor } from "@rangojs/router/client";
|
|
221
|
+
|
|
222
|
+
// Works for both RSC and response routes
|
|
223
|
+
type ProductParams = ParamsFor<"api.productDetail">;
|
|
224
|
+
// = { id: string }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Links to Response Routes
|
|
228
|
+
|
|
229
|
+
### Client: href.json(), href.text(), etc.
|
|
230
|
+
|
|
231
|
+
Response route links need `data-external` to trigger hard navigation (skip RSC fetch).
|
|
232
|
+
Use `href.json()` which returns props to spread on `<Link>`:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
"use client";
|
|
236
|
+
import { href, Link } from "@rangojs/router/client";
|
|
237
|
+
|
|
238
|
+
function Nav() {
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
{/* RSC link (client-side navigation) */}
|
|
242
|
+
<Link to={href("/about")}>About</Link>
|
|
243
|
+
|
|
244
|
+
{/* Response route link (hard navigation via data-external) */}
|
|
245
|
+
<Link {...href.json("/api/health")}>API Health</Link>
|
|
246
|
+
<Link {...href.text("/robots.txt")}>Robots</Link>
|
|
247
|
+
</>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// href.json("/api/health") returns:
|
|
252
|
+
// { to: "/api/health", "data-external": "" }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Use Items
|
|
256
|
+
|
|
257
|
+
Response routes support only `middleware()` and `cache()` as use items.
|
|
258
|
+
No `loader`, `loading`, `layout`, or `parallel`.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
path.json("/api/users", handler, { name: "users" }, () => [
|
|
262
|
+
cache({ ttl: 60, swr: 300 }),
|
|
263
|
+
]);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Mountable Module Pattern
|
|
267
|
+
|
|
268
|
+
A self-contained module with RSC pages + JSON APIs, mountable via `include()`:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// blog/api/urls.tsx
|
|
272
|
+
import { urls, RouterError } from "@rangojs/router/server";
|
|
273
|
+
|
|
274
|
+
export const blogApiPatterns = urls(({ path }) => [
|
|
275
|
+
path.json("/stats", (ctx) => ({
|
|
276
|
+
views: 1200, visitors: 450,
|
|
277
|
+
}), { name: "stats" }),
|
|
278
|
+
|
|
279
|
+
path.json("/:slug/likes", (ctx) => ({
|
|
280
|
+
slug: ctx.params.slug,
|
|
281
|
+
count: 42,
|
|
282
|
+
}), { name: "likes" }),
|
|
283
|
+
|
|
284
|
+
path.json("/:slug/comments", (ctx) => ([
|
|
285
|
+
{ id: "c1", body: "Great post", author: "alice" },
|
|
286
|
+
]), { name: "comments" }),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
// blog/urls.tsx
|
|
290
|
+
import { urls } from "@rangojs/router/server";
|
|
291
|
+
import { blogApiPatterns } from "./api/urls";
|
|
292
|
+
|
|
293
|
+
export const blogPatterns = urls(({ path, include }) => [
|
|
294
|
+
path("/", BlogIndex, { name: "index" }),
|
|
295
|
+
path("/:slug", BlogPost, { name: "post" }),
|
|
296
|
+
path("/category/:catId", BlogCategory, { name: "category" }),
|
|
297
|
+
|
|
298
|
+
include("/api", blogApiPatterns, { name: "api" }),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
// app/urls.tsx
|
|
302
|
+
import { urls } from "@rangojs/router/server";
|
|
303
|
+
import { blogPatterns } from "./blog/urls";
|
|
304
|
+
|
|
305
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
306
|
+
path("/", HomePage, { name: "home" }),
|
|
307
|
+
include("/blog", blogPatterns, { name: "blog" }),
|
|
308
|
+
]);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Type safety after mounting
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import type { RouteResponse } from "@rangojs/router/server";
|
|
315
|
+
import type { PathResponse, ParamsFor } from "@rangojs/router/client";
|
|
316
|
+
|
|
317
|
+
// Scoped (before mount) -- use the module directly
|
|
318
|
+
type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
|
|
319
|
+
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
320
|
+
|
|
321
|
+
// After mounting -- names get prefixed
|
|
322
|
+
type BlogStats = PathResponse<"/blog/api/stats">;
|
|
323
|
+
// = ResponseEnvelope<{ views: number; visitors: number }>
|
|
324
|
+
|
|
325
|
+
// Params work through nested includes
|
|
326
|
+
type LikesParams = ParamsFor<"blog.api.likes">;
|
|
327
|
+
// = { slug: string }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### ctx.reverse inside mounted modules
|
|
331
|
+
|
|
332
|
+
Response route handlers inside a mounted module can reference local names:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// Inside blogApiPatterns handler
|
|
336
|
+
path("/:slug/likes", (ctx) => {
|
|
337
|
+
// ctx.reverse resolves names relative to the mount point
|
|
338
|
+
const commentsUrl = ctx.reverse("comments", { slug: ctx.params.slug });
|
|
339
|
+
// -> "/blog/api/my-post/comments"
|
|
340
|
+
|
|
341
|
+
return { slug: ctx.params.slug, count: 42, commentsUrl };
|
|
342
|
+
}, { name: "likes" });
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Content Negotiation
|
|
346
|
+
|
|
347
|
+
Multiple response types can share the same URL pattern. See `/mime-routes` for the
|
|
348
|
+
full content negotiation API (Accept header matching, Vary: Accept, multi-variant routes).
|
|
349
|
+
|
|
350
|
+
## How It Works
|
|
351
|
+
|
|
352
|
+
1. `path.json()` tags the route at the trie level with a MIME type
|
|
353
|
+
2. `coreRequestHandler()` checks the tag before the RSC pipeline
|
|
354
|
+
3. Tagged routes short-circuit: handler runs, Response is returned directly
|
|
355
|
+
4. JSON routes auto-wrap return values in `{ data }` / `{ error }` envelope
|
|
356
|
+
5. Client-side navigation to response routes gets `X-RSC-Reload` header, triggering hard navigation
|
|
357
|
+
6. Response types flow through `_responses` phantom type on `UrlPatterns`, propagated by `include()`
|
|
358
|
+
7. When multiple routes share a URL pattern, the trie merges them for content negotiation (see `/mime-routes`)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: route
|
|
3
|
+
description: Define routes with path() in @rangojs/router
|
|
4
|
+
argument-hint: [pattern]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Defining Routes with path()
|
|
8
|
+
|
|
9
|
+
## Basic Route
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { urls } from "@rangojs/router";
|
|
13
|
+
|
|
14
|
+
export const urlpatterns = urls(({ path }) => [
|
|
15
|
+
path("/", HomePage, { name: "home" }),
|
|
16
|
+
path("/about", AboutPage, { name: "about" }),
|
|
17
|
+
path("/contact", ContactPage, { name: "contact" }),
|
|
18
|
+
]);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Route with Parameters
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
urls(({ path }) => [
|
|
25
|
+
// Single parameter
|
|
26
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
27
|
+
|
|
28
|
+
// Multiple parameters
|
|
29
|
+
path("/blog/:year/:month/:slug", BlogPostPage, { name: "blogPost" }),
|
|
30
|
+
|
|
31
|
+
// Optional parameter (add ? suffix)
|
|
32
|
+
path("/search/:query?", SearchPage, { name: "search" }),
|
|
33
|
+
]);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Route Handler Patterns
|
|
37
|
+
|
|
38
|
+
### Component Function
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
path("/about", AboutPage, { name: "about" })
|
|
42
|
+
|
|
43
|
+
// AboutPage receives context
|
|
44
|
+
function AboutPage(ctx: HandlerContext) {
|
|
45
|
+
return <div>About Us</div>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Inline JSX
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
path("/about", () => <AboutPage />, { name: "about" })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Handler with Context Access
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
path("/product/:slug", (ctx) => {
|
|
59
|
+
const { slug } = ctx.params;
|
|
60
|
+
return <ProductPage slug={slug} />;
|
|
61
|
+
}, { name: "product" })
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Async Handler (Streaming)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
path("/product/:slug", async (ctx) => {
|
|
68
|
+
const product = await fetchProduct(ctx.params.slug);
|
|
69
|
+
return <ProductPage product={product} />;
|
|
70
|
+
}, { name: "product" })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Route Options
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
path("/product/:slug", ProductPage, {
|
|
77
|
+
name: "product", // Route name for href() and navigation
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Route Children
|
|
82
|
+
|
|
83
|
+
Add loaders, loading states, and other features as children:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
87
|
+
loader(ProductLoader),
|
|
88
|
+
loading(<ProductSkeleton />),
|
|
89
|
+
revalidate(productRevalidation),
|
|
90
|
+
])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Handler Context
|
|
94
|
+
|
|
95
|
+
Every handler receives a context object:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
interface HandlerContext<TParams = Record<string, string>> {
|
|
99
|
+
params: TParams; // URL parameters
|
|
100
|
+
request: Request; // Original request
|
|
101
|
+
url: URL; // Parsed URL
|
|
102
|
+
env: TEnv; // Environment (bindings + variables)
|
|
103
|
+
use<T>(handle: Handle<T>): T; // Access handles
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Using Context
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
path("/product/:slug", (ctx) => {
|
|
111
|
+
// Access URL params
|
|
112
|
+
const { slug } = ctx.params;
|
|
113
|
+
|
|
114
|
+
// Access query params
|
|
115
|
+
const tab = ctx.url.searchParams.get("tab");
|
|
116
|
+
|
|
117
|
+
// Access environment
|
|
118
|
+
const db = ctx.env.Bindings.DB;
|
|
119
|
+
|
|
120
|
+
// Access handles
|
|
121
|
+
const breadcrumbs = ctx.use(Breadcrumbs);
|
|
122
|
+
breadcrumbs.push({ label: "Product", href: `/product/${slug}` });
|
|
123
|
+
|
|
124
|
+
return <ProductPage slug={slug} tab={tab} />;
|
|
125
|
+
}, { name: "product" })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Nested Routes
|
|
129
|
+
|
|
130
|
+
Use layouts to nest routes:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
urls(({ path, layout }) => [
|
|
134
|
+
layout(<ShopLayout />, () => [
|
|
135
|
+
path("/shop", ShopIndex, { name: "shop.index" }),
|
|
136
|
+
path("/shop/cart", CartPage, { name: "shop.cart" }),
|
|
137
|
+
path("/shop/product/:slug", ProductPage, { name: "shop.product" }),
|
|
138
|
+
]),
|
|
139
|
+
])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Complete Example
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { urls } from "@rangojs/router";
|
|
146
|
+
import { Breadcrumbs } from "./handles/breadcrumbs";
|
|
147
|
+
|
|
148
|
+
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
149
|
+
// Simple route
|
|
150
|
+
path("/", HomePage, { name: "home" }),
|
|
151
|
+
|
|
152
|
+
// Route with loader
|
|
153
|
+
path("/about", AboutPage, { name: "about" }, () => [
|
|
154
|
+
loader(TeamLoader),
|
|
155
|
+
]),
|
|
156
|
+
|
|
157
|
+
// Dynamic route with handler
|
|
158
|
+
path("/product/:slug", (ctx) => {
|
|
159
|
+
const push = ctx.use(Breadcrumbs);
|
|
160
|
+
push({ label: ctx.params.slug, href: `/product/${ctx.params.slug}` });
|
|
161
|
+
return <ProductPage slug={ctx.params.slug} />;
|
|
162
|
+
}, { name: "product" }, () => [
|
|
163
|
+
loader(ProductLoader),
|
|
164
|
+
loading(<ProductSkeleton />, { ssr: true }),
|
|
165
|
+
]),
|
|
166
|
+
|
|
167
|
+
// Nested routes in layout
|
|
168
|
+
layout(<BlogLayout />, () => [
|
|
169
|
+
path("/blog", BlogIndex, { name: "blog.index" }),
|
|
170
|
+
path("/blog/:slug", BlogPost, { name: "blog.post" }),
|
|
171
|
+
]),
|
|
172
|
+
]);
|
|
173
|
+
```
|