@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.15
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 +589 -4
- package/dist/bin/rango.js +425 -204
- package/dist/vite/index.js +116 -326
- package/package.json +1 -1
- package/skills/rango/SKILL.md +63 -0
- package/skills/testing/SKILL.md +226 -0
- package/skills/typesafety/SKILL.md +49 -31
- package/src/bin/rango.ts +63 -12
- package/src/build/generate-route-types.ts +260 -450
- package/src/index.rsc.ts +13 -1
- package/src/index.ts +7 -0
- package/src/route-definition.ts +31 -0
- package/src/router/match-middleware/cache-store.ts +1 -1
- package/src/router.ts +13 -6
- package/src/server.ts +13 -158
- package/src/urls.ts +29 -0
- package/src/vite/index.ts +9 -17
package/README.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# @rangojs/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Django-inspired RSC router with type-safe partial rendering for Vite.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Experimental:** This package is under active development. APIs may change between releases. Install with `@experimental` tag.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Composable URL patterns** — Django-style `urls()` DSL with `path`, `layout`, `include`
|
|
10
|
+
- **Named routes** — `reverse("blogPost", { slug })` for type-safe URL generation (Django-style)
|
|
11
|
+
- **Data loaders** — `createLoader()` with automatic streaming and Suspense integration
|
|
12
|
+
- **Layouts & nesting** — Nested layouts with `<Outlet />` and parallel routes
|
|
13
|
+
- **Segment-level caching** — `cache()` DSL with TTL/SWR and pluggable cache stores
|
|
14
|
+
- **Middleware** — Route-level middleware with cookie and header access
|
|
15
|
+
- **Pre-rendering** — `Prerender()` and `Static()` handlers for build-time rendering
|
|
16
|
+
- **Theme support** — Light/dark mode with FOUC prevention and system detection
|
|
17
|
+
- **Host routing** — Multi-app routing by domain/subdomain via `@rangojs/router/host`
|
|
18
|
+
- **Response routes** — `path.json()`, `path.text()`, `path.xml()` for API endpoints
|
|
19
|
+
- **CLI codegen** — `rango generate` for route type generation
|
|
6
20
|
|
|
7
21
|
## Installation
|
|
8
22
|
|
|
@@ -10,9 +24,580 @@ Type-safe RSC router with partial rendering support for Vite.
|
|
|
10
24
|
npm install @rangojs/router@experimental
|
|
11
25
|
```
|
|
12
26
|
|
|
13
|
-
|
|
27
|
+
Peer dependencies:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install react @vitejs/plugin-rsc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For Cloudflare Workers:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @cloudflare/vite-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Vite Config
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// vite.config.ts
|
|
45
|
+
import react from "@vitejs/plugin-react";
|
|
46
|
+
import { defineConfig } from "vite";
|
|
47
|
+
import { rango } from "@rangojs/router/vite";
|
|
48
|
+
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
plugins: [
|
|
51
|
+
react(),
|
|
52
|
+
rango({ preset: "cloudflare" }),
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Router
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// src/router.tsx
|
|
61
|
+
import { createRouter, urls } from "@rangojs/router";
|
|
62
|
+
import { Document } from "./document";
|
|
63
|
+
|
|
64
|
+
const urlpatterns = urls(({ path, layout }) => [
|
|
65
|
+
layout(<MainLayout />, () => [
|
|
66
|
+
path("/", HomePage, { name: "home" }),
|
|
67
|
+
path("/about", AboutPage, { name: "about" }),
|
|
68
|
+
path("/blog/:slug", BlogPostPage, { name: "blogPost" }),
|
|
69
|
+
]),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
export const router = createRouter({ document: Document })
|
|
73
|
+
.routes(urlpatterns);
|
|
74
|
+
|
|
75
|
+
// Export typed reverse function for URL generation by route name
|
|
76
|
+
export const reverse = router.reverse;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Document
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// src/document.tsx
|
|
83
|
+
"use client";
|
|
84
|
+
|
|
85
|
+
import type { ReactNode } from "react";
|
|
86
|
+
import { MetaTags } from "@rangojs/router/client";
|
|
87
|
+
|
|
88
|
+
export function Document({ children }: { children: ReactNode }) {
|
|
89
|
+
return (
|
|
90
|
+
<html lang="en">
|
|
91
|
+
<head>
|
|
92
|
+
<MetaTags />
|
|
93
|
+
</head>
|
|
94
|
+
<body>{children}</body>
|
|
95
|
+
</html>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Defining Routes
|
|
101
|
+
|
|
102
|
+
### Path Patterns
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { urls } from "@rangojs/router";
|
|
106
|
+
|
|
107
|
+
const urlpatterns = urls(({ path }) => [
|
|
108
|
+
path("/", HomePage, { name: "home" }),
|
|
109
|
+
path("/product/:slug", ProductPage, { name: "product" }),
|
|
110
|
+
path("/search/:query?", SearchPage, { name: "search" }),
|
|
111
|
+
path("/files/*", FilesPage, { name: "files" }),
|
|
112
|
+
]);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Typed Handlers
|
|
116
|
+
|
|
117
|
+
Route handlers receive a typed context with params, search params, and `reverse()`:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import type { Handler } from "@rangojs/router";
|
|
121
|
+
|
|
122
|
+
export const ProductPage: Handler<"product"> = (ctx) => {
|
|
123
|
+
const { slug } = ctx.params; // typed from pattern
|
|
124
|
+
const homeUrl = ctx.reverse("home"); // type-safe URL by route name
|
|
125
|
+
return <h1>Product: {slug}</h1>;
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Search Params
|
|
130
|
+
|
|
131
|
+
Define a search schema on the route for type-safe search parameters:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const urlpatterns = urls(({ path }) => [
|
|
135
|
+
path("/search", SearchPage, {
|
|
136
|
+
name: "search",
|
|
137
|
+
search: { q: "string", page: "number?", sort: "string?" },
|
|
138
|
+
}),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// Handler receives typed searchParams
|
|
142
|
+
const SearchPage: Handler<"search"> = (ctx) => {
|
|
143
|
+
const { q, page, sort } = ctx.searchParams;
|
|
144
|
+
// q: string, page: number | undefined, sort: string | undefined
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Response Routes
|
|
149
|
+
|
|
150
|
+
Define API endpoints that bypass the RSC pipeline:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const urlpatterns = urls(({ path }) => [
|
|
154
|
+
path.json("/api/health", () => ({ status: "ok" }), { name: "health" }),
|
|
155
|
+
path.text("/robots.txt", () => "User-agent: *\nAllow: /", { name: "robots" }),
|
|
156
|
+
path.xml("/feed.xml", () => "<rss>...</rss>", { name: "feed" }),
|
|
157
|
+
]);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Response types available: `path.json()`, `path.text()`, `path.html()`, `path.xml()`, `path.image()`, `path.stream()`, `path.any()`.
|
|
161
|
+
|
|
162
|
+
## Layouts & Nesting
|
|
163
|
+
|
|
164
|
+
### Layouts with Outlet
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { urls } from "@rangojs/router";
|
|
168
|
+
|
|
169
|
+
const urlpatterns = urls(({ path, layout }) => [
|
|
170
|
+
layout(<MainLayout />, () => [
|
|
171
|
+
path("/", HomePage, { name: "home" }),
|
|
172
|
+
path("/about", AboutPage, { name: "about" }),
|
|
173
|
+
]),
|
|
174
|
+
]);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
"use client";
|
|
179
|
+
import { Outlet } from "@rangojs/router/client";
|
|
180
|
+
|
|
181
|
+
function MainLayout() {
|
|
182
|
+
return (
|
|
183
|
+
<div>
|
|
184
|
+
<nav>...</nav>
|
|
185
|
+
<Outlet />
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Loading Skeletons
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
const urlpatterns = urls(({ path, loading }) => [
|
|
195
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
196
|
+
loading(<ProductSkeleton />),
|
|
197
|
+
]),
|
|
198
|
+
]);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Parallel Routes
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
const urlpatterns = urls(({ path, layout, parallel, loader, loading }) => [
|
|
205
|
+
layout(BlogLayout, () => [
|
|
206
|
+
parallel({ "@sidebar": BlogSidebarHandler }, () => [
|
|
207
|
+
loader(BlogSidebarLoader),
|
|
208
|
+
loading(<SidebarSkeleton />),
|
|
209
|
+
]),
|
|
210
|
+
path("/blog", BlogIndexPage, { name: "blog" }),
|
|
211
|
+
path("/blog/:slug", BlogPostPage, { name: "blogPost" }),
|
|
212
|
+
]),
|
|
213
|
+
]);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Data Loaders
|
|
217
|
+
|
|
218
|
+
### Creating a Loader
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { createLoader } from "@rangojs/router";
|
|
222
|
+
|
|
223
|
+
export const BlogSidebarLoader = createLoader(async (ctx) => {
|
|
224
|
+
const posts = await db.getRecentPosts();
|
|
225
|
+
return { posts, loadedAt: new Date().toISOString() };
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Using in Server Components (Handlers)
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import type { HandlerContext } from "@rangojs/router";
|
|
233
|
+
import { BlogSidebarLoader } from "./loaders/blog";
|
|
234
|
+
|
|
235
|
+
async function BlogSidebarHandler(ctx: HandlerContext) {
|
|
236
|
+
const { posts } = await ctx.use(BlogSidebarLoader);
|
|
237
|
+
return <ul>{posts.map(p => <li key={p.slug}>{p.title}</li>)}</ul>;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Using in Client Components
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
"use client";
|
|
245
|
+
import { useLoader } from "@rangojs/router/client";
|
|
246
|
+
import { BlogSidebarLoader } from "./loaders/blog";
|
|
247
|
+
|
|
248
|
+
function BlogSidebar() {
|
|
249
|
+
const { posts } = useLoader(BlogSidebarLoader);
|
|
250
|
+
return <ul>{posts.map(p => <li key={p.slug}>{p.title}</li>)}</ul>;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Attaching Loaders to Routes
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const urlpatterns = urls(({ path, loader }) => [
|
|
258
|
+
path("/blog", BlogIndexPage, { name: "blog" }, () => [
|
|
259
|
+
loader(BlogSidebarLoader),
|
|
260
|
+
]),
|
|
261
|
+
]);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Navigation & Links
|
|
265
|
+
|
|
266
|
+
### Named Routes with `reverse()` (Server Components)
|
|
267
|
+
|
|
268
|
+
In server components, use `reverse()` to generate URLs by route name:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { Link } from "@rangojs/router/client";
|
|
272
|
+
import { reverse } from "./router";
|
|
273
|
+
|
|
274
|
+
function BlogIndex() {
|
|
275
|
+
return (
|
|
276
|
+
<nav>
|
|
277
|
+
<Link to={reverse("home")}>Home</Link>
|
|
278
|
+
<Link to={reverse("blogPost", { slug: "my-post" })}>My Post</Link>
|
|
279
|
+
<Link to={reverse("about")}>About</Link>
|
|
280
|
+
</nav>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `reverse("api.health")`.
|
|
286
|
+
|
|
287
|
+
Handlers also have `ctx.reverse()` directly on the context:
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
const BlogPostPage: Handler<"blogPost"> = (ctx) => {
|
|
291
|
+
const backUrl = ctx.reverse("blog");
|
|
292
|
+
return <Link to={backUrl}>Back to blog</Link>;
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### `href()` for Path Validation (Client Components)
|
|
297
|
+
|
|
298
|
+
In client components, use `href()` for compile-time path validation:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
"use client";
|
|
302
|
+
import { Link, href } from "@rangojs/router/client";
|
|
303
|
+
|
|
304
|
+
function Nav() {
|
|
305
|
+
return (
|
|
306
|
+
<nav>
|
|
307
|
+
<Link to={href("/")}>Home</Link>
|
|
308
|
+
<Link to={href("/blog")} prefetch="intent">Blog</Link>
|
|
309
|
+
<Link to={href("/about")}>About</Link>
|
|
310
|
+
</nav>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`href()` validates that the path matches a registered route pattern at compile time (e.g. `/blog/my-post` matches `/blog/:slug`).
|
|
316
|
+
|
|
317
|
+
### Navigation Hook
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
"use client";
|
|
321
|
+
import { useNavigation } from "@rangojs/router/client";
|
|
322
|
+
|
|
323
|
+
function SearchForm() {
|
|
324
|
+
const { navigate, isPending } = useNavigation();
|
|
325
|
+
|
|
326
|
+
function handleSubmit(query: string) {
|
|
327
|
+
navigate(`/search?q=${encodeURIComponent(query)}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return <form onSubmit={...}>{isPending && <Spinner />}</form>;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Scroll Restoration
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
"use client";
|
|
338
|
+
import { ScrollRestoration } from "@rangojs/router/client";
|
|
339
|
+
|
|
340
|
+
function Document({ children }) {
|
|
341
|
+
return (
|
|
342
|
+
<html>
|
|
343
|
+
<body>
|
|
344
|
+
{children}
|
|
345
|
+
<ScrollRestoration />
|
|
346
|
+
</body>
|
|
347
|
+
</html>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Includes (Composable Modules)
|
|
353
|
+
|
|
354
|
+
Split URL patterns into composable modules with `include()`:
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
// src/api/urls.tsx
|
|
358
|
+
import { urls } from "@rangojs/router";
|
|
359
|
+
|
|
360
|
+
export const apiPatterns = urls(({ path }) => [
|
|
361
|
+
path.json("/health", () => ({ status: "ok" }), { name: "health" }),
|
|
362
|
+
path.json("/products", getProducts, { name: "products" }),
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
// src/urls.tsx
|
|
366
|
+
import { urls } from "@rangojs/router";
|
|
367
|
+
import { apiPatterns } from "./api/urls";
|
|
368
|
+
|
|
369
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
370
|
+
path("/", HomePage, { name: "home" }),
|
|
371
|
+
include("/api", apiPatterns, { name: "api" }),
|
|
372
|
+
// Mounts apiPatterns under /api: /api/health, /api/products
|
|
373
|
+
]);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Included route names are prefixed with the include name: `reverse("api.health")`, `reverse("api.products")`.
|
|
377
|
+
|
|
378
|
+
## Middleware
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
const urlpatterns = urls(({ path, middleware }) => [
|
|
382
|
+
middleware(async (ctx, next) => {
|
|
383
|
+
const start = Date.now();
|
|
384
|
+
const response = await next();
|
|
385
|
+
console.log(`${ctx.request.method} ${ctx.url.pathname} ${Date.now() - start}ms`);
|
|
386
|
+
return response;
|
|
387
|
+
}, () => [
|
|
388
|
+
path("/dashboard", DashboardPage, { name: "dashboard" }),
|
|
389
|
+
]),
|
|
390
|
+
]);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Caching
|
|
394
|
+
|
|
395
|
+
### Route-Level Caching
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
const urlpatterns = urls(({ path, cache }) => [
|
|
399
|
+
cache({ ttl: 60, swr: 300 }, () => [
|
|
400
|
+
path("/blog", BlogIndexPage, { name: "blog" }),
|
|
401
|
+
path("/blog/:slug", BlogPostPage, { name: "blogPost" }),
|
|
402
|
+
]),
|
|
403
|
+
]);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Cache Store Configuration
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
import { createRouter } from "@rangojs/router";
|
|
410
|
+
import { CFCacheStore, createDocumentCacheMiddleware } from "@rangojs/router/cache";
|
|
411
|
+
|
|
412
|
+
export const router = createRouter({
|
|
413
|
+
document: Document,
|
|
414
|
+
cache: (env) => ({
|
|
415
|
+
store: new CFCacheStore({
|
|
416
|
+
defaults: { ttl: 60, swr: 300 },
|
|
417
|
+
ctx: env.ctx,
|
|
418
|
+
}),
|
|
419
|
+
}),
|
|
420
|
+
})
|
|
421
|
+
.use(createDocumentCacheMiddleware())
|
|
422
|
+
.routes(urlpatterns);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Available cache stores:
|
|
426
|
+
- `CFCacheStore` — Cloudflare edge cache (production)
|
|
427
|
+
- `MemorySegmentCacheStore` — In-memory cache (development/testing)
|
|
428
|
+
|
|
429
|
+
## Pre-rendering
|
|
430
|
+
|
|
431
|
+
Pre-rendering generates route segments at build time. The worker handles all requests — there are no static files served from assets.
|
|
432
|
+
|
|
433
|
+
### Static Segments
|
|
434
|
+
|
|
435
|
+
Use `Static()` for segments rendered once at build time (no params). Works on `path()`, `layout()`, and `parallel()`:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
import { Static } from "@rangojs/router";
|
|
439
|
+
|
|
440
|
+
export const AboutPage = Static(async () => {
|
|
441
|
+
return <article>...</article>;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
export const DocsNav = Static(async () => {
|
|
445
|
+
const items = await readDocsNavItems();
|
|
446
|
+
return <nav>{items.map(i => <a key={i.slug} href={i.slug}>{i.title}</a>)}</nav>;
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Dynamic Routes with Prerender
|
|
451
|
+
|
|
452
|
+
Use `Prerender()` for route-scoped pre-rendering. With params, provide `getParams` first, handler second:
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
import { Prerender } from "@rangojs/router";
|
|
456
|
+
|
|
457
|
+
export const BlogPost = Prerender(
|
|
458
|
+
async () => {
|
|
459
|
+
const slugs = await getAllBlogSlugs();
|
|
460
|
+
return slugs.map(slug => ({ slug }));
|
|
461
|
+
},
|
|
462
|
+
async (ctx) => {
|
|
463
|
+
const post = await getPost(ctx.params.slug);
|
|
464
|
+
return <article>{post.content}</article>;
|
|
465
|
+
},
|
|
466
|
+
);
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Passthrough for Unknown Params
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import { Prerender } from "@rangojs/router";
|
|
473
|
+
|
|
474
|
+
export const ProductPage = Prerender(
|
|
475
|
+
async () => {
|
|
476
|
+
const featured = await db.getFeaturedProducts();
|
|
477
|
+
return featured.map(p => ({ id: p.id }));
|
|
478
|
+
},
|
|
479
|
+
async (ctx) => {
|
|
480
|
+
const product = await db.getProduct(ctx.params.id);
|
|
481
|
+
return <Product data={product} />;
|
|
482
|
+
},
|
|
483
|
+
{ passthrough: true },
|
|
484
|
+
);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
With `passthrough: true`, known params are served from the build-time cache and unknown params fall through to live rendering.
|
|
488
|
+
|
|
489
|
+
## Theme
|
|
490
|
+
|
|
491
|
+
### Router Configuration
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
export const router = createRouter({
|
|
495
|
+
document: Document,
|
|
496
|
+
theme: {
|
|
497
|
+
defaultTheme: "light",
|
|
498
|
+
themes: ["light", "dark", "system"],
|
|
499
|
+
attribute: "class",
|
|
500
|
+
enableSystem: true,
|
|
501
|
+
},
|
|
502
|
+
}).routes(urlpatterns);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Theme Toggle
|
|
506
|
+
|
|
507
|
+
```tsx
|
|
508
|
+
"use client";
|
|
509
|
+
import { useTheme } from "@rangojs/router/theme";
|
|
510
|
+
|
|
511
|
+
function ThemeToggle() {
|
|
512
|
+
const { theme, setTheme, themes } = useTheme();
|
|
513
|
+
return (
|
|
514
|
+
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
515
|
+
{themes.map(t => <option key={t}>{t}</option>)}
|
|
516
|
+
</select>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## Host Routing
|
|
522
|
+
|
|
523
|
+
Route requests to different apps based on domain/subdomain patterns using `@rangojs/router/host`:
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
// worker.rsc.tsx
|
|
527
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
528
|
+
|
|
529
|
+
const hostRouter = createHostRouter();
|
|
530
|
+
|
|
531
|
+
hostRouter.host(["*.localhost"]).map(() => import("./apps/admin/handler.js"));
|
|
532
|
+
hostRouter.host(["localhost"]).map(() => import("./apps/site/handler.js"));
|
|
533
|
+
hostRouter.fallback().map(() => import("./apps/site/handler.js"));
|
|
534
|
+
|
|
535
|
+
export default {
|
|
536
|
+
async fetch(request, env, ctx) {
|
|
537
|
+
return hostRouter.match(request, { Bindings: env, Variables: {}, ctx });
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Each sub-app has its own `createRouter()` and `urls()`. The host router lazily imports the matched app's handler. Patterns are matched in registration order — register more specific patterns (subdomains) before catch-alls.
|
|
543
|
+
|
|
544
|
+
## Meta Tags
|
|
545
|
+
|
|
546
|
+
Accumulate meta tags across route segments using the built-in `Meta` handle:
|
|
547
|
+
|
|
548
|
+
```tsx
|
|
549
|
+
import { Meta } from "@rangojs/router";
|
|
550
|
+
import type { HandlerContext } from "@rangojs/router";
|
|
551
|
+
|
|
552
|
+
export function BlogPostPage(ctx: HandlerContext) {
|
|
553
|
+
const meta = ctx.use(Meta);
|
|
554
|
+
meta({ title: "My Blog Post" });
|
|
555
|
+
meta({ name: "description", content: "A great blog post" });
|
|
556
|
+
meta({ property: "og:title", content: "My Blog Post" });
|
|
557
|
+
|
|
558
|
+
return <article>...</article>;
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Render collected tags in the document with `<MetaTags />` from `@rangojs/router/client`.
|
|
563
|
+
|
|
564
|
+
## CLI: `rango generate`
|
|
565
|
+
|
|
566
|
+
Route types are generated automatically by the Vite plugin. The CLI is a manual fallback for generating types outside the dev server (e.g. in CI or for IDE support before first `pnpm dev`):
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
npx rango generate src/router.tsx
|
|
570
|
+
npx rango generate src/ # recursive scan
|
|
571
|
+
npx rango generate src/urls.tsx src/api/ # mix files and directories
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Auto-detects file type:
|
|
575
|
+
- Files with `createRouter` → `*.named-routes.gen.ts` with global route map
|
|
576
|
+
- Files with `urls()` → `*.gen.ts` with per-module route names, params, and search types
|
|
577
|
+
|
|
578
|
+
## Type Safety
|
|
579
|
+
|
|
580
|
+
The Vite plugin automatically generates a `router.named-routes.gen.ts` file that globally registers all route names, patterns, and search schemas. Type-safe `reverse()`, `Handler<"name">`, `href()`, and `RouteParams<"name">` work out of the box — no manual type registration needed. The gen file is updated on dev server startup, HMR, and production builds.
|
|
581
|
+
|
|
582
|
+
## Subpath Exports
|
|
583
|
+
|
|
584
|
+
| Export | Description |
|
|
585
|
+
|--------|-------------|
|
|
586
|
+
| `@rangojs/router` | Core: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
|
|
587
|
+
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
|
|
588
|
+
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
|
|
589
|
+
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
|
|
590
|
+
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
|
|
591
|
+
| `@rangojs/router/vite` | Vite plugin: `rango()` |
|
|
592
|
+
| `@rangojs/router/server` | Server utilities |
|
|
593
|
+
| `@rangojs/router/build` | Build utilities |
|
|
594
|
+
|
|
595
|
+
## Examples
|
|
596
|
+
|
|
597
|
+
See the `examples/` directory for full working applications:
|
|
14
598
|
|
|
15
|
-
|
|
599
|
+
- [`cloudflare-basic`](../../examples/cloudflare-basic) — Cloudflare Workers with caching, loaders, theme, and pre-rendering
|
|
600
|
+
- [`cloudflare-multi-router`](../../examples/cloudflare-multi-router) — Multi-app host routing
|
|
16
601
|
|
|
17
602
|
## License
|
|
18
603
|
|