@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,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loader
|
|
3
|
+
description: Define data loaders for fetching data in routes with createLoader
|
|
4
|
+
argument-hint: [name]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Data Loaders with loader()
|
|
8
|
+
|
|
9
|
+
Loaders fetch data on the server and stream it to the client.
|
|
10
|
+
|
|
11
|
+
## Creating a Loader
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createLoader } from "@rangojs/router";
|
|
15
|
+
|
|
16
|
+
export const ProductLoader = createLoader("product", async (ctx) => {
|
|
17
|
+
const product = await ctx.env.Bindings.DB
|
|
18
|
+
.prepare("SELECT * FROM products WHERE slug = ?")
|
|
19
|
+
.bind(ctx.params.slug)
|
|
20
|
+
.first();
|
|
21
|
+
|
|
22
|
+
return { product };
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Using Loaders in Routes
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { urls } from "@rangojs/router";
|
|
30
|
+
import { ProductLoader } from "./loaders/product";
|
|
31
|
+
|
|
32
|
+
export const urlpatterns = urls(({ path, loader }) => [
|
|
33
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
34
|
+
loader(ProductLoader),
|
|
35
|
+
]),
|
|
36
|
+
]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Consuming Loader Data
|
|
40
|
+
|
|
41
|
+
### In Server Components
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { useLoader } from "@rangojs/router";
|
|
45
|
+
import { ProductLoader } from "./loaders/product";
|
|
46
|
+
|
|
47
|
+
async function ProductPage() {
|
|
48
|
+
const { product } = await useLoader(ProductLoader);
|
|
49
|
+
return <h1>{product.name}</h1>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### In Client Components
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
"use client";
|
|
57
|
+
import { useLoaderData } from "@rangojs/router/client";
|
|
58
|
+
import { ProductLoader } from "./loaders/product";
|
|
59
|
+
|
|
60
|
+
function ProductDetails() {
|
|
61
|
+
const { product } = useLoaderData(ProductLoader);
|
|
62
|
+
return <div>{product.description}</div>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Loader Context
|
|
67
|
+
|
|
68
|
+
Loaders receive the same context as route handlers:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export const ProductLoader = createLoader("product", async (ctx) => {
|
|
72
|
+
// URL params
|
|
73
|
+
const { slug } = ctx.params;
|
|
74
|
+
|
|
75
|
+
// Query params
|
|
76
|
+
const variant = ctx.url.searchParams.get("variant");
|
|
77
|
+
|
|
78
|
+
// Environment (DB, KV, etc.)
|
|
79
|
+
const db = ctx.env.Bindings.DB;
|
|
80
|
+
|
|
81
|
+
// Request headers
|
|
82
|
+
const auth = ctx.request.headers.get("Authorization");
|
|
83
|
+
|
|
84
|
+
// Variables set by middleware
|
|
85
|
+
const user = ctx.env.Variables.user;
|
|
86
|
+
|
|
87
|
+
return { product: await fetchProduct(slug) };
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Loader with Children
|
|
92
|
+
|
|
93
|
+
Add caching or revalidation to specific loaders:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
97
|
+
// Cached loader
|
|
98
|
+
loader(ProductLoader, () => [
|
|
99
|
+
cache({ ttl: 300 }),
|
|
100
|
+
]),
|
|
101
|
+
|
|
102
|
+
// Loader with revalidation control
|
|
103
|
+
loader(RelatedProductsLoader, () => [
|
|
104
|
+
revalidate(() => false), // Never revalidate
|
|
105
|
+
]),
|
|
106
|
+
|
|
107
|
+
// Loader that revalidates after cart actions
|
|
108
|
+
loader(CartLoader, () => [
|
|
109
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
110
|
+
]),
|
|
111
|
+
])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Multiple Loaders
|
|
115
|
+
|
|
116
|
+
Routes can have multiple loaders that run in parallel:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
120
|
+
loader(ProductLoader),
|
|
121
|
+
loader(RelatedProductsLoader),
|
|
122
|
+
loader(ReviewsLoader),
|
|
123
|
+
])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Layout Loaders
|
|
127
|
+
|
|
128
|
+
Loaders on layouts are shared by all child routes:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
layout(<ShopLayout />, () => [
|
|
132
|
+
// These loaders are available to all shop routes
|
|
133
|
+
loader(CartLoader),
|
|
134
|
+
loader(CategoriesLoader),
|
|
135
|
+
|
|
136
|
+
path("/shop", ShopIndex, { name: "index" }),
|
|
137
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }),
|
|
138
|
+
])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Passing Loaders as Props
|
|
142
|
+
|
|
143
|
+
Loaders can be passed as props from server to client components. RSC serialization
|
|
144
|
+
uses `toJSON()` to send only `{ __brand, $$id }` — the loader function is stripped.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Server component (route handler)
|
|
148
|
+
import { SlowLoader } from "../loaders";
|
|
149
|
+
|
|
150
|
+
path("/dashboard", () => <DashboardContent loader={SlowLoader} />, { name: "dashboard" }, () => [
|
|
151
|
+
loader(SlowLoader),
|
|
152
|
+
loading(<DashboardSkeleton />),
|
|
153
|
+
])
|
|
154
|
+
|
|
155
|
+
// Client component — use typeof for type-safe props
|
|
156
|
+
"use client";
|
|
157
|
+
import { useLoader } from "@rangojs/router/client";
|
|
158
|
+
import type { SlowLoader } from "../loaders";
|
|
159
|
+
|
|
160
|
+
function DashboardContent({ loader }: { loader: typeof SlowLoader }) {
|
|
161
|
+
const { data } = useLoader(loader);
|
|
162
|
+
return <div>{data.message}</div>;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Use `typeof MyLoader` for the prop type — it infers the full generic automatically.
|
|
167
|
+
|
|
168
|
+
## Streaming with Suspense
|
|
169
|
+
|
|
170
|
+
Loaders stream data. Use Suspense for loading states:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// In route definition
|
|
174
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
175
|
+
loader(ProductLoader),
|
|
176
|
+
loading(<ProductSkeleton />), // Shows while loader streams
|
|
177
|
+
])
|
|
178
|
+
|
|
179
|
+
// Or in component
|
|
180
|
+
function ProductPage() {
|
|
181
|
+
return (
|
|
182
|
+
<Suspense fallback={<ProductSkeleton />}>
|
|
183
|
+
<ProductDetails />
|
|
184
|
+
</Suspense>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Complete Example
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// loaders/shop.ts
|
|
193
|
+
import { createLoader } from "@rangojs/router";
|
|
194
|
+
|
|
195
|
+
export const ProductLoader = createLoader("product", async (ctx) => {
|
|
196
|
+
const product = await ctx.env.Bindings.DB
|
|
197
|
+
.prepare("SELECT * FROM products WHERE slug = ?")
|
|
198
|
+
.bind(ctx.params.slug)
|
|
199
|
+
.first();
|
|
200
|
+
|
|
201
|
+
if (!product) {
|
|
202
|
+
throw new Response("Product not found", { status: 404 });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { product };
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const CartLoader = createLoader("cart", async (ctx) => {
|
|
209
|
+
const user = ctx.env.Variables.user;
|
|
210
|
+
if (!user) return { cart: null };
|
|
211
|
+
|
|
212
|
+
const cart = await ctx.env.Bindings.KV.get(`cart:${user.id}`, "json");
|
|
213
|
+
return { cart };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// urls.tsx
|
|
217
|
+
export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
|
|
218
|
+
layout(<ShopLayout />, () => [
|
|
219
|
+
// Shared cart loader for all shop routes
|
|
220
|
+
loader(CartLoader, () => [
|
|
221
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
222
|
+
]),
|
|
223
|
+
|
|
224
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
225
|
+
loader(ProductLoader, () => [cache({ ttl: 60 })]),
|
|
226
|
+
loading(<ProductSkeleton />),
|
|
227
|
+
]),
|
|
228
|
+
]),
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
// pages/product.tsx
|
|
232
|
+
import { useLoader } from "@rangojs/router";
|
|
233
|
+
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
234
|
+
|
|
235
|
+
async function ProductPage() {
|
|
236
|
+
const { product } = await useLoader(ProductLoader);
|
|
237
|
+
const { cart } = await useLoader(CartLoader);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div>
|
|
241
|
+
<h1>{product.name}</h1>
|
|
242
|
+
<AddToCartButton productId={product.id} inCart={cart?.items.includes(product.id)} />
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: middleware
|
|
3
|
+
description: Define middleware for authentication, logging, and request processing in @rangojs/router
|
|
4
|
+
argument-hint: [middleware-name]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Middleware
|
|
8
|
+
|
|
9
|
+
Middleware runs before/after route handlers using the onion model.
|
|
10
|
+
|
|
11
|
+
## Basic Middleware
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createMiddleware } from "@rangojs/router";
|
|
15
|
+
|
|
16
|
+
export const authMiddleware = createMiddleware(async (ctx, next) => {
|
|
17
|
+
const token = ctx.request.headers.get("Authorization");
|
|
18
|
+
|
|
19
|
+
if (!token) {
|
|
20
|
+
throw new Response("Unauthorized", { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const user = await verifyToken(token);
|
|
24
|
+
ctx.env.Variables.user = user;
|
|
25
|
+
|
|
26
|
+
await next();
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Using Middleware in Routes
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { urls } from "@rangojs/router";
|
|
34
|
+
import { authMiddleware, loggerMiddleware } from "./middleware";
|
|
35
|
+
|
|
36
|
+
export const urlpatterns = urls(({ path, layout, middleware }) => [
|
|
37
|
+
// Global middleware for all routes in this file
|
|
38
|
+
middleware(loggerMiddleware),
|
|
39
|
+
|
|
40
|
+
// Layout with scoped middleware
|
|
41
|
+
layout(<AdminLayout />, () => [
|
|
42
|
+
middleware(authMiddleware), // Only for admin routes
|
|
43
|
+
|
|
44
|
+
path("/admin", AdminDashboard, { name: "admin.index" }),
|
|
45
|
+
path("/admin/users", AdminUsers, { name: "admin.users" }),
|
|
46
|
+
]),
|
|
47
|
+
|
|
48
|
+
// Public routes (no auth middleware)
|
|
49
|
+
path("/", HomePage, { name: "home" }),
|
|
50
|
+
path("/about", AboutPage, { name: "about" }),
|
|
51
|
+
]);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Middleware with Multiple Handlers
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Spread multiple middleware from a single export
|
|
58
|
+
export const shopMiddleware = [loggerMiddleware, mockAuthMiddleware];
|
|
59
|
+
|
|
60
|
+
// In routes
|
|
61
|
+
layout(<ShopLayout />, () => [
|
|
62
|
+
middleware(...shopMiddleware),
|
|
63
|
+
|
|
64
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
65
|
+
])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Middleware Context
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export const myMiddleware = createMiddleware(async (ctx, next) => {
|
|
72
|
+
// Access request
|
|
73
|
+
ctx.request; // Request object
|
|
74
|
+
ctx.url; // Parsed URL
|
|
75
|
+
ctx.params; // Route parameters
|
|
76
|
+
|
|
77
|
+
// Access environment
|
|
78
|
+
ctx.env.Bindings.DB; // Cloudflare bindings
|
|
79
|
+
ctx.env.Variables; // Mutable variables
|
|
80
|
+
|
|
81
|
+
// Set variables for downstream handlers
|
|
82
|
+
ctx.env.Variables.user = { id: "123", name: "John" };
|
|
83
|
+
|
|
84
|
+
// Continue to next middleware/handler
|
|
85
|
+
await next();
|
|
86
|
+
|
|
87
|
+
// After handler (response intercepting)
|
|
88
|
+
console.log("Handler completed");
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Authentication Middleware
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
|
|
96
|
+
const user = ctx.env.Variables.user;
|
|
97
|
+
|
|
98
|
+
if (!user) {
|
|
99
|
+
throw new Response("Unauthorized", { status: 401 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await next();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const permissionsMiddleware = createMiddleware(async (ctx, next) => {
|
|
106
|
+
const user = ctx.env.Variables.user;
|
|
107
|
+
const requiredPermission = "admin";
|
|
108
|
+
|
|
109
|
+
if (!user?.permissions?.includes(requiredPermission)) {
|
|
110
|
+
throw new Response("Forbidden", { status: 403 });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await next();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Logger Middleware
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
export const loggerMiddleware = createMiddleware(async (ctx, next) => {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
|
|
123
|
+
console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
|
|
124
|
+
|
|
125
|
+
await next();
|
|
126
|
+
|
|
127
|
+
const duration = Date.now() - start;
|
|
128
|
+
console.log(`[${ctx.request.method}] ${ctx.url.pathname} - ${duration}ms`);
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Rate Limiting Middleware
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
export const rateLimitMiddleware = createMiddleware(async (ctx, next) => {
|
|
136
|
+
const ip = ctx.request.headers.get("CF-Connecting-IP") ?? "unknown";
|
|
137
|
+
const key = `rate-limit:${ip}`;
|
|
138
|
+
|
|
139
|
+
const count = await ctx.env.Bindings.KV.get(key);
|
|
140
|
+
const requests = count ? parseInt(count) : 0;
|
|
141
|
+
|
|
142
|
+
if (requests > 100) {
|
|
143
|
+
throw new Response("Too Many Requests", { status: 429 });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await ctx.env.Bindings.KV.put(key, String(requests + 1), {
|
|
147
|
+
expirationTtl: 60,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await next();
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Complete Example
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// middleware/index.ts
|
|
158
|
+
import { createMiddleware } from "@rangojs/router";
|
|
159
|
+
|
|
160
|
+
export const loggerMiddleware = createMiddleware(async (ctx, next) => {
|
|
161
|
+
console.log(`[${ctx.request.method}] ${ctx.url.pathname}`);
|
|
162
|
+
await next();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const mockAuthMiddleware = createMiddleware(async (ctx, next) => {
|
|
166
|
+
// Mock user for development
|
|
167
|
+
ctx.env.Variables.user = { id: "1", name: "Demo User" };
|
|
168
|
+
await next();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const requireAuthMiddleware = createMiddleware(async (ctx, next) => {
|
|
172
|
+
if (!ctx.env.Variables.user) {
|
|
173
|
+
throw new Response("Unauthorized", { status: 401 });
|
|
174
|
+
}
|
|
175
|
+
await next();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// urls.tsx
|
|
179
|
+
import { urls } from "@rangojs/router";
|
|
180
|
+
import {
|
|
181
|
+
loggerMiddleware,
|
|
182
|
+
mockAuthMiddleware,
|
|
183
|
+
requireAuthMiddleware,
|
|
184
|
+
} from "./middleware";
|
|
185
|
+
|
|
186
|
+
export const urlpatterns = urls(({ path, layout, middleware }) => [
|
|
187
|
+
// Global middleware
|
|
188
|
+
middleware(loggerMiddleware),
|
|
189
|
+
middleware(mockAuthMiddleware),
|
|
190
|
+
|
|
191
|
+
// Public routes
|
|
192
|
+
path("/", HomePage, { name: "home" }),
|
|
193
|
+
|
|
194
|
+
// Protected routes
|
|
195
|
+
layout(<AccountLayout />, () => [
|
|
196
|
+
middleware(requireAuthMiddleware),
|
|
197
|
+
|
|
198
|
+
path("/account", AccountPage, { name: "account" }),
|
|
199
|
+
path("/account/settings", SettingsPage, { name: "settings" }),
|
|
200
|
+
]),
|
|
201
|
+
]);
|
|
202
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mime-routes
|
|
3
|
+
description: Content negotiation — serve different response types (RSC, JSON, text, XML) from the same URL based on Accept header
|
|
4
|
+
argument-hint: [negotiate|vary|accept]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Content Negotiation (MIME Routes)
|
|
8
|
+
|
|
9
|
+
Content negotiation lets you register multiple response types on the same URL pattern.
|
|
10
|
+
The router inspects the `Accept` header and dispatches to the matching handler.
|
|
11
|
+
All negotiated responses include `Vary: Accept` for correct CDN/cache behavior.
|
|
12
|
+
|
|
13
|
+
See also: `/response-routes` for the base response route API (path.json, path.text, etc.).
|
|
14
|
+
|
|
15
|
+
## Defining Negotiated Routes
|
|
16
|
+
|
|
17
|
+
Declare the same URL pattern with both an RSC route and one or more response-type routes.
|
|
18
|
+
Order within the `urls()` array does not matter — the trie merges them at build time.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { urls } from "@rangojs/router/server";
|
|
22
|
+
|
|
23
|
+
export const urlpatterns = urls(({ path, layout, include }) => [
|
|
24
|
+
// RSC page + JSON API on the same URL
|
|
25
|
+
path("/products/:id", ProductPage, { name: "product" }),
|
|
26
|
+
path.json("/products/:id", (ctx) => {
|
|
27
|
+
return db.getProduct(ctx.params.id);
|
|
28
|
+
}, { name: "productJson" }),
|
|
29
|
+
]);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
When a browser requests `/products/42` (`Accept: text/html`), the RSC page renders.
|
|
33
|
+
When an API client requests the same URL (`Accept: application/json`), the JSON handler runs.
|
|
34
|
+
|
|
35
|
+
## Negotiation Rules
|
|
36
|
+
|
|
37
|
+
1. **Q-value priority** — higher `q` wins (`Accept: application/json;q=0.9, text/html;q=1.0` serves RSC)
|
|
38
|
+
2. **Client order tiebreaker** — when q-values are equal, the type listed first in Accept wins (matches Express/Hono behavior)
|
|
39
|
+
3. **Specific MIME match** — the variant whose MIME type appears in Accept wins
|
|
40
|
+
4. **Wildcard / empty Accept** — `*/*` and missing Accept fall back to route definition order (the first-defined variant wins)
|
|
41
|
+
5. **All responses** on a negotiated URL get `Vary: Accept` header, including the RSC side
|
|
42
|
+
|
|
43
|
+
RSC participates as a `text/html` candidate alongside response-type variants.
|
|
44
|
+
There is no special short-circuit — RSC follows the same negotiation rules as other types.
|
|
45
|
+
|
|
46
|
+
The MIME mapping used for matching:
|
|
47
|
+
|
|
48
|
+
| Tag | MIME type |
|
|
49
|
+
|-----|-----------|
|
|
50
|
+
| RSC (plain `path()`) | `text/html` (negotiation) / `text/x-component` (wire format) |
|
|
51
|
+
| `json` | `application/json` |
|
|
52
|
+
| `text` | `text/plain` |
|
|
53
|
+
| `xml` | `application/xml` |
|
|
54
|
+
| `html` | `text/html` |
|
|
55
|
+
| `md` | `text/markdown` |
|
|
56
|
+
|
|
57
|
+
RSC routes negotiate as `text/html` but respond with `text/x-component` (the RSC wire format).
|
|
58
|
+
The browser's RSC runtime decodes this transparently — clients requesting `text/html` get
|
|
59
|
+
the RSC page rendered normally.
|
|
60
|
+
|
|
61
|
+
Tags `image`, `stream`, and `any` are pass-through and do not participate in Accept matching.
|
|
62
|
+
|
|
63
|
+
## Multiple Response Types
|
|
64
|
+
|
|
65
|
+
A single URL can have an RSC route plus multiple response-type variants:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export const urlpatterns = urls(({ path }) => [
|
|
69
|
+
path("/data", DataPage, { name: "data" }),
|
|
70
|
+
path.json("/data", () => ({ format: "json" }), { name: "dataJson" }),
|
|
71
|
+
path.text("/data", () => "plain text", { name: "dataText" }),
|
|
72
|
+
path.xml("/data", () => "<root>xml</root>", { name: "dataXml" }),
|
|
73
|
+
]);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `Accept: text/html` — RSC page
|
|
77
|
+
- `Accept: application/json` — JSON handler
|
|
78
|
+
- `Accept: text/plain` — text handler
|
|
79
|
+
- `Accept: application/xml` — XML handler
|
|
80
|
+
- `Accept: */*` — first variant (JSON, since it was registered first)
|
|
81
|
+
|
|
82
|
+
## Wildcard Routes
|
|
83
|
+
|
|
84
|
+
Content negotiation works with wildcard `/*` patterns:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
path("/files/*", FileBrowserPage, { name: "files" }),
|
|
88
|
+
path.json("/files/*", (ctx) => {
|
|
89
|
+
const filePath = ctx.params["*"];
|
|
90
|
+
return { entries: listDir(filePath) };
|
|
91
|
+
}, { name: "filesJson" }),
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Response-Only Negotiation (No RSC Primary)
|
|
95
|
+
|
|
96
|
+
Two or more response-type routes can share a URL without an RSC route.
|
|
97
|
+
The last registered route becomes the primary; earlier ones become variants:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
path.json("/api/data", () => ({ format: "json" }), { name: "dataJson" }),
|
|
101
|
+
path.text("/api/data", () => "plain text version", { name: "dataText" }),
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Without an RSC primary, there is no `text/html` candidate — the Accept header
|
|
105
|
+
picks among the response-type candidates directly.
|
|
106
|
+
|
|
107
|
+
## How It Works
|
|
108
|
+
|
|
109
|
+
1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
|
|
110
|
+
RSC routes become the primary trie leaf; response-type routes are stored in the `nv`
|
|
111
|
+
(negotiate variants) array on the leaf. The `rf` (rsc-first) flag tracks definition order.
|
|
112
|
+
2. **Runtime**: `previewRoute()` reads `negotiateVariants` from the trie match result.
|
|
113
|
+
It parses the `Accept` header (extracting q-values and order), builds a candidate list
|
|
114
|
+
(RSC as `text/html` + response-type variants), and calls `pickNegotiateVariant()`.
|
|
115
|
+
3. **Candidate matching**: walks the client's sorted Accept list (by q desc, then order asc),
|
|
116
|
+
matching each entry against candidates. Wildcards (`*/*`, `text/*`) fall back to definition order.
|
|
117
|
+
4. **Vary header**: both the response-route handler wrapper and the RSC handler wrapper
|
|
118
|
+
append `Vary: Accept` when the `negotiated` flag is set on the preview result.
|
|
119
|
+
|
|
120
|
+
## Caching Considerations
|
|
121
|
+
|
|
122
|
+
`Vary: Accept` is set automatically on all negotiated responses. This tells CDNs and
|
|
123
|
+
HTTP caches to store separate entries per Accept header value. No additional cache
|
|
124
|
+
configuration is needed for negotiated routes — the framework handles it.
|