@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379
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 +46 -12
- package/dist/bin/rango.js +109 -15
- package/dist/vite/index.js +323 -121
- package/package.json +15 -16
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/loader/SKILL.md +55 -15
- package/skills/prerender/SKILL.md +2 -2
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +3 -4
- package/skills/router-setup/SKILL.md +8 -3
- package/skills/typesafety/SKILL.md +25 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +95 -5
- package/src/browser/navigation-client.ts +97 -72
- package/src/browser/prefetch/cache.ts +112 -25
- package/src/browser/prefetch/fetch.ts +28 -30
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/react/Link.tsx +19 -7
- package/src/browser/rsc-router.tsx +11 -2
- package/src/browser/server-action-bridge.ts +448 -432
- package/src/browser/types.ts +24 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/router-processing.ts +125 -15
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +1 -46
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +5 -36
- package/src/index.ts +32 -66
- package/src/prerender/store.ts +56 -15
- package/src/route-definition/index.ts +0 -3
- package/src/router/handler-context.ts +30 -3
- package/src/router/loader-resolution.ts +1 -1
- package/src/router/match-api.ts +1 -1
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +53 -10
- package/src/router/middleware.ts +170 -81
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/router-interfaces.ts +14 -1
- package/src/router/router-options.ts +13 -8
- package/src/router/segment-resolution/fresh.ts +18 -0
- package/src/router/segment-resolution/helpers.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +22 -9
- package/src/router/trie-matching.ts +20 -2
- package/src/router.ts +29 -9
- package/src/rsc/handler.ts +106 -11
- package/src/rsc/index.ts +0 -20
- package/src/rsc/progressive-enhancement.ts +21 -8
- package/src/rsc/rsc-rendering.ts +30 -43
- package/src/rsc/server-action.ts +14 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +2 -0
- package/src/search-params.ts +16 -13
- package/src/server/context.ts +8 -2
- package/src/server/request-context.ts +38 -16
- package/src/server.ts +6 -0
- package/src/theme/index.ts +4 -13
- package/src/types/handler-context.ts +12 -16
- package/src/types/route-config.ts +17 -8
- package/src/types/segments.ts +0 -5
- package/src/vite/discovery/bundle-postprocess.ts +31 -56
- package/src/vite/discovery/discover-routers.ts +18 -4
- package/src/vite/discovery/prerender-collection.ts +34 -14
- package/src/vite/discovery/state.ts +4 -7
- package/src/vite/index.ts +4 -3
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/rango.ts +11 -0
- package/src/vite/router-discovery.ts +16 -0
- package/src/vite/utils/prerender-utils.ts +60 -0
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: host-router
|
|
3
|
+
description: Multi-app host routing with domain/subdomain patterns
|
|
4
|
+
argument-hint:
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Host Router
|
|
8
|
+
|
|
9
|
+
Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createHostRouter, defineHosts } from "@rangojs/router/host";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Setup
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// host-router.ts
|
|
21
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
22
|
+
|
|
23
|
+
const router = createHostRouter();
|
|
24
|
+
|
|
25
|
+
router.host(["."]).map(() => import("./apps/main"));
|
|
26
|
+
router.host(["admin.*"]).map(() => import("./apps/admin"));
|
|
27
|
+
router.host(["api.*"]).map(() => import("./apps/api"));
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
31
|
+
return router.match(request, { env, ctx });
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
|
|
37
|
+
|
|
38
|
+
## Pattern Syntax
|
|
39
|
+
|
|
40
|
+
| Pattern | Matches |
|
|
41
|
+
| ----------------- | ---------------------------------------------- |
|
|
42
|
+
| `.` or `*` | Any apex domain (`example.com`) |
|
|
43
|
+
| `**` | Any domain (apex + all subdomains) |
|
|
44
|
+
| `*.` | Any single-level subdomain (`www.example.com`) |
|
|
45
|
+
| `**. ` | Any multi-level subdomain (`a.b.example.com`) |
|
|
46
|
+
| `example.com` | Exact domain |
|
|
47
|
+
| `*.com` | Any apex `.com` domain |
|
|
48
|
+
| `*.example.com` | Single subdomain of `example.com` |
|
|
49
|
+
| `**.example.com` | Any depth subdomain of `example.com` |
|
|
50
|
+
| `admin.*` | `admin` subdomain of any apex domain |
|
|
51
|
+
| `admin.**` | `admin` subdomain of any domain |
|
|
52
|
+
| `admin.` | `admin` subdomain of any apex (no wildcard) |
|
|
53
|
+
| `example.com/api` | Domain + path prefix (prefix match) |
|
|
54
|
+
|
|
55
|
+
Patterns are tested in registration order. First match wins.
|
|
56
|
+
|
|
57
|
+
## `defineHosts` for Type Safety
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { defineHosts } from "@rangojs/router/host";
|
|
61
|
+
|
|
62
|
+
const hosts = defineHosts({
|
|
63
|
+
admin: "admin.*",
|
|
64
|
+
api: "api.*",
|
|
65
|
+
app: [".", "www.*"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
router.host(hosts.admin).map(() => import("./apps/admin"));
|
|
69
|
+
router.host(hosts.app).map(() => import("./apps/main"));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Returns a frozen object — keys are autocompleted by TypeScript.
|
|
73
|
+
|
|
74
|
+
## Middleware
|
|
75
|
+
|
|
76
|
+
Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const router = createHostRouter();
|
|
80
|
+
|
|
81
|
+
// Global — runs for all routes
|
|
82
|
+
router.use(async (request, input, next) => {
|
|
83
|
+
console.log(`[${new Date().toISOString()}] ${request.url}`);
|
|
84
|
+
return next();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Per-route
|
|
88
|
+
router
|
|
89
|
+
.host(["admin.*"])
|
|
90
|
+
.use(requireAuth)
|
|
91
|
+
.map(() => import("./apps/admin"));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
|
|
95
|
+
|
|
96
|
+
Calling `next()` more than once throws.
|
|
97
|
+
|
|
98
|
+
## Fallback Handler
|
|
99
|
+
|
|
100
|
+
Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const router = createHostRouter({
|
|
104
|
+
hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Called when cookie override fails (not for general unmatched hosts)
|
|
108
|
+
router.fallback().map((request) => {
|
|
109
|
+
return new Response("Invalid host override", { status: 400 });
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { NoRouteMatchError } from "@rangojs/router/host";
|
|
117
|
+
|
|
118
|
+
export default {
|
|
119
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
120
|
+
try {
|
|
121
|
+
return await router.match(request, { env, ctx });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (err instanceof NoRouteMatchError) {
|
|
124
|
+
return new Response("Not Found", { status: 404 });
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Cookie-Based Host Override
|
|
133
|
+
|
|
134
|
+
For development: route requests to a different app based on a cookie value, allowing developers to test different host routes from a single domain.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const router = createHostRouter({
|
|
138
|
+
hostOverride: {
|
|
139
|
+
cookieName: "x-dev-host",
|
|
140
|
+
allowedHosts: ["localhost", "**.dev.example.com"],
|
|
141
|
+
validate: (request, cookieValue, input) => {
|
|
142
|
+
// Optional custom validation — return the effective hostname
|
|
143
|
+
return cookieValue;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When a request arrives:
|
|
150
|
+
|
|
151
|
+
1. If no cookie → use actual hostname
|
|
152
|
+
2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
|
|
153
|
+
3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
|
|
154
|
+
|
|
155
|
+
Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
|
|
156
|
+
|
|
157
|
+
## Debug Mode
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const router = createHostRouter({ debug: true });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Logs pattern matching, route registration, and cookie override decisions to console.
|
|
164
|
+
|
|
165
|
+
## Testing
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
|
|
169
|
+
|
|
170
|
+
// Test pattern matching
|
|
171
|
+
testPattern("admin.*", "admin.example.com"); // true
|
|
172
|
+
testPattern([".", "www.*"], "example.com"); // true
|
|
173
|
+
|
|
174
|
+
// Create requests for integration tests
|
|
175
|
+
const request = createTestRequest({
|
|
176
|
+
host: "admin.example.com",
|
|
177
|
+
path: "/dashboard",
|
|
178
|
+
cookies: { "x-dev-host": "api.example.com" },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Test which route would match (without executing)
|
|
182
|
+
router.test("admin.example.com"); // { pattern, handler } | null
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Error Types
|
|
186
|
+
|
|
187
|
+
All errors extend `HostRouterError`:
|
|
188
|
+
|
|
189
|
+
| Error | When |
|
|
190
|
+
| ----------------------------- | ------------------------------------------- |
|
|
191
|
+
| `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
|
|
192
|
+
| `HostOverrideNotAllowedError` | Cookie override from disallowed host |
|
|
193
|
+
| `InvalidHostnameError` | Cookie value isn't a valid hostname |
|
|
194
|
+
| `HostValidationError` | Custom `validate` function threw |
|
|
195
|
+
| `NoRouteMatchError` | No host pattern matched the request |
|
|
196
|
+
| `InvalidHandlerError` | Handler is not a function |
|
|
197
|
+
|
|
198
|
+
See the fallback section above for a `NoRouteMatchError` catch example.
|
|
199
|
+
|
|
200
|
+
## Nesting Host Routers
|
|
201
|
+
|
|
202
|
+
A lazy handler can resolve to another `HostRouter`:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// apps/regional.ts
|
|
206
|
+
import { createHostRouter } from "@rangojs/router/host";
|
|
207
|
+
|
|
208
|
+
const regional = createHostRouter();
|
|
209
|
+
regional.host(["us.*"]).map(() => import("./regions/us"));
|
|
210
|
+
regional.host(["eu.*"]).map(() => import("./regions/eu"));
|
|
211
|
+
|
|
212
|
+
export default regional;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// host-router.ts
|
|
217
|
+
router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
|
|
218
|
+
```
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -65,19 +65,24 @@ export const urlpatterns = urls(({ path, loader }) => [
|
|
|
65
65
|
|
|
66
66
|
## Consuming Loader Data
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Loaders are the **live data layer** — they resolve fresh on every request.
|
|
69
|
+
The way you consume them depends on whether you're in a server component
|
|
70
|
+
(route handler) or a client component.
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
> **IMPORTANT: Prefer consuming loaders in client components.** Keeping data
|
|
73
|
+
> fetching in loaders and consumption in client components creates a clean
|
|
74
|
+
> separation: the server-side handler renders static markup that can be
|
|
75
|
+
> freely cached with `cache()`, while loader data stays fresh on every
|
|
76
|
+
> request. When you consume loaders in server handlers via `ctx.use()`, the
|
|
77
|
+
> handler output depends on the loader data, which means caching the handler
|
|
78
|
+
> also caches the data — defeating the purpose of the live data layer.
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
const { product } = await useLoader(ProductLoader);
|
|
76
|
-
return <h1>{product.name}</h1>;
|
|
77
|
-
}
|
|
78
|
-
```
|
|
80
|
+
### In Client Components (Preferred)
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
Client components use `useLoader()` from `@rangojs/router/client`.
|
|
83
|
+
The loader **must** be registered with `loader()` in the route's DSL
|
|
84
|
+
segments so the framework knows to resolve it during SSR and stream
|
|
85
|
+
the data to the client:
|
|
81
86
|
|
|
82
87
|
```typescript
|
|
83
88
|
"use client";
|
|
@@ -90,6 +95,42 @@ function ProductDetails() {
|
|
|
90
95
|
}
|
|
91
96
|
```
|
|
92
97
|
|
|
98
|
+
```typescript
|
|
99
|
+
// Route definition — loader() registration required for client consumption
|
|
100
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
101
|
+
loader(ProductLoader), // Required for useLoader() in client components
|
|
102
|
+
]);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### In Route Handlers (Server Components)
|
|
106
|
+
|
|
107
|
+
In server components, use `ctx.use(Loader)` directly in the route handler.
|
|
108
|
+
This doesn't require `loader()` registration in the DSL — it works
|
|
109
|
+
standalone. **However**, prefer client-side consumption when possible (see
|
|
110
|
+
note above).
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { ProductLoader } from "./loaders/product";
|
|
114
|
+
|
|
115
|
+
// Route handler — server component
|
|
116
|
+
path("/product/:slug", async (ctx) => {
|
|
117
|
+
const { product } = await ctx.use(ProductLoader);
|
|
118
|
+
return <h1>{product.name}</h1>;
|
|
119
|
+
}, { name: "product" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When you do register with `loader()` in the DSL, `ctx.use()` returns the
|
|
123
|
+
same memoized result — loaders never run twice per request.
|
|
124
|
+
|
|
125
|
+
**Never use `useLoader()` in server components** — it is a client-only API.
|
|
126
|
+
|
|
127
|
+
### Summary
|
|
128
|
+
|
|
129
|
+
| Context | API | `loader()` DSL required? |
|
|
130
|
+
| ---------------------------- | ------------------- | ------------------------ |
|
|
131
|
+
| Client component (preferred) | `useLoader(Loader)` | **Yes** |
|
|
132
|
+
| Route handler (server) | `ctx.use(Loader)` | No |
|
|
133
|
+
|
|
93
134
|
## Loader Context
|
|
94
135
|
|
|
95
136
|
Loaders receive the same context as route handlers:
|
|
@@ -538,13 +579,12 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
|
|
|
538
579
|
]),
|
|
539
580
|
]);
|
|
540
581
|
|
|
541
|
-
// pages/product.tsx
|
|
542
|
-
import { useLoader } from "@rangojs/router";
|
|
582
|
+
// pages/product.tsx — server component (route handler)
|
|
543
583
|
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
544
584
|
|
|
545
|
-
async function ProductPage() {
|
|
546
|
-
const { product } = await
|
|
547
|
-
const { cart } = await
|
|
585
|
+
async function ProductPage(ctx) {
|
|
586
|
+
const { product } = await ctx.use(ProductLoader);
|
|
587
|
+
const { cart } = await ctx.use(CartLoader);
|
|
548
588
|
|
|
549
589
|
return (
|
|
550
590
|
<div>
|
|
@@ -521,10 +521,10 @@ export const guidesPatterns = urls(({ path }) => [
|
|
|
521
521
|
]);
|
|
522
522
|
|
|
523
523
|
// urls.tsx
|
|
524
|
-
import { urls
|
|
524
|
+
import { urls } from "@rangojs/router";
|
|
525
525
|
import { guidesPatterns } from "./pages/guides.js";
|
|
526
526
|
|
|
527
|
-
export const urlpatterns = urls(({ path }) => [
|
|
527
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
528
528
|
path("/", HomePage, { name: "home" }),
|
|
529
529
|
include("/guides", guidesPatterns, { name: "guides" }),
|
|
530
530
|
]);
|
package/skills/rango/SKILL.md
CHANGED
|
@@ -32,7 +32,6 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
|
|
|
32
32
|
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|
|
33
33
|
| `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
|
|
34
34
|
| `/fonts` | Load web fonts with preload hints |
|
|
35
|
-
| `/testing` | Unit test route trees with `buildRouteTree()` |
|
|
36
35
|
|
|
37
36
|
## Quick Start
|
|
38
37
|
|
package/skills/route/SKILL.md
CHANGED
|
@@ -103,8 +103,8 @@ export const SearchPage: Handler<"search"> = (ctx) => {
|
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
Missing params are `undefined` regardless of required/optional. The required/optional
|
|
107
|
+
distinction is a consumer-facing contract (for `href()` and `reverse()` autocomplete).
|
|
108
108
|
|
|
109
109
|
Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
|
|
110
110
|
|
|
@@ -355,8 +355,7 @@ urls(({ path, layout }) => [
|
|
|
355
355
|
## Complete Example
|
|
356
356
|
|
|
357
357
|
```typescript
|
|
358
|
-
import { urls } from "@rangojs/router";
|
|
359
|
-
import { Breadcrumbs } from "./handles/breadcrumbs";
|
|
358
|
+
import { urls, Breadcrumbs } from "@rangojs/router";
|
|
360
359
|
|
|
361
360
|
export const urlpatterns = urls(({ path, layout, loader, loading }) => [
|
|
362
361
|
// Simple route
|
|
@@ -78,7 +78,7 @@ interface RSCRouterOptions<TEnv> {
|
|
|
78
78
|
// Document component wrapping entire app
|
|
79
79
|
document?: ComponentType<{ children: ReactNode }>;
|
|
80
80
|
|
|
81
|
-
// Enable performance
|
|
81
|
+
// Enable per-request performance timeline (console waterfall + Server-Timing header)
|
|
82
82
|
debugPerformance?: boolean;
|
|
83
83
|
|
|
84
84
|
// Default error boundary
|
|
@@ -108,6 +108,11 @@ interface RSCRouterOptions<TEnv> {
|
|
|
108
108
|
// Connection warmup (default: true)
|
|
109
109
|
warmup?: boolean;
|
|
110
110
|
|
|
111
|
+
// Prefetch cache TTL in seconds (default: 300)
|
|
112
|
+
// Controls in-memory cache duration and Cache-Control max-age for prefetch responses.
|
|
113
|
+
// Set to false to disable prefetch caching.
|
|
114
|
+
prefetchCacheTTL?: number | false;
|
|
115
|
+
|
|
111
116
|
// CSP nonce provider (for router.fetch)
|
|
112
117
|
nonce?: (
|
|
113
118
|
request: Request,
|
|
@@ -297,10 +302,10 @@ export const shopPatterns = urls(({ path, layout }) => [
|
|
|
297
302
|
]);
|
|
298
303
|
|
|
299
304
|
// src/urls.tsx
|
|
300
|
-
import { urls
|
|
305
|
+
import { urls } from "@rangojs/router";
|
|
301
306
|
import { shopPatterns } from "./urls/shop";
|
|
302
307
|
|
|
303
|
-
export const urlpatterns = urls(({ path }) => [
|
|
308
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
304
309
|
path("/", HomePage, { name: "home" }),
|
|
305
310
|
include("/shop", shopPatterns, { name: "shop" }),
|
|
306
311
|
]);
|
|
@@ -281,7 +281,7 @@ import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
|
281
281
|
|
|
282
282
|
// RouteSearchParams<"name"> resolves the search schema to a typed object
|
|
283
283
|
type SP = RouteSearchParams<"search">;
|
|
284
|
-
// { q: string; page?: number; sort?: string }
|
|
284
|
+
// { q: string | undefined; page?: number; sort?: string }
|
|
285
285
|
|
|
286
286
|
// RouteParams<"name"> resolves URL params from the route pattern
|
|
287
287
|
type P = RouteParams<"blogPost">;
|
|
@@ -334,7 +334,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
334
334
|
});
|
|
335
335
|
|
|
336
336
|
// In server component - type is inferred
|
|
337
|
-
import { useLoader } from "@rangojs/router";
|
|
337
|
+
import { useLoader } from "@rangojs/router/client";
|
|
338
338
|
|
|
339
339
|
async function ProductPage() {
|
|
340
340
|
const product = await useLoader(ProductLoader);
|
|
@@ -414,26 +414,30 @@ Both approaches coexist: `ctx.get("user")` (global via Vars) and
|
|
|
414
414
|
Handles have typed data:
|
|
415
415
|
|
|
416
416
|
```typescript
|
|
417
|
-
//
|
|
418
|
-
import {
|
|
419
|
-
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
// In client - typed array
|
|
417
|
+
// Built-in Breadcrumbs handle — import from "@rangojs/router"
|
|
418
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
419
|
+
// Type: Handle<BreadcrumbItem, BreadcrumbItem[]>
|
|
420
|
+
// BreadcrumbItem: { label: string; href: string; content?: ReactNode | Promise<ReactNode> }
|
|
421
|
+
|
|
422
|
+
// In route handler — push is fully typed
|
|
423
|
+
path("/shop/product/:slug", (ctx) => {
|
|
424
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
425
|
+
breadcrumb({ label: "Products", href: "/shop/products" });
|
|
426
|
+
return <ProductPage />;
|
|
427
|
+
}, { name: "product" });
|
|
428
|
+
|
|
429
|
+
// In client — typed array
|
|
430
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
433
431
|
function BreadcrumbNav() {
|
|
434
432
|
const crumbs = useHandle(Breadcrumbs);
|
|
435
|
-
// crumbs:
|
|
433
|
+
// crumbs: BreadcrumbItem[]
|
|
436
434
|
}
|
|
435
|
+
|
|
436
|
+
// Custom handles also work the same way
|
|
437
|
+
import { createHandle } from "@rangojs/router";
|
|
438
|
+
export const PageTitle = createHandle<string, string>(
|
|
439
|
+
(segments) => segments.flat().at(-1) ?? "Default Title"
|
|
440
|
+
);
|
|
437
441
|
```
|
|
438
442
|
|
|
439
443
|
## Ref Prop Type Safety (Loaders & Handles)
|
|
@@ -447,14 +451,12 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
447
451
|
return { product: await fetchProduct(ctx.params.slug) };
|
|
448
452
|
});
|
|
449
453
|
|
|
450
|
-
//
|
|
451
|
-
export const Breadcrumbs = createHandle<{ label: string; href: string }>();
|
|
454
|
+
// Built-in Breadcrumbs — or any custom handle created with createHandle()
|
|
452
455
|
|
|
453
456
|
// Client component — typeof infers all generics
|
|
454
457
|
("use client");
|
|
455
|
-
import { useLoader, useHandle } from "@rangojs/router/client";
|
|
458
|
+
import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
456
459
|
import type { ProductLoader } from "../loaders";
|
|
457
|
-
import type { Breadcrumbs } from "../handles";
|
|
458
460
|
|
|
459
461
|
function MyComponent({
|
|
460
462
|
loader,
|
package/src/__internal.ts
CHANGED
|
@@ -164,6 +164,98 @@ export type {
|
|
|
164
164
|
*/
|
|
165
165
|
export type { InternalHandlerContext } from "./types.js";
|
|
166
166
|
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Rendering (Internal)
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @internal
|
|
173
|
+
* Builds React element trees from route segments.
|
|
174
|
+
*/
|
|
175
|
+
export { renderSegments } from "./segment-system.js";
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Error Utilities (Internal)
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @internal
|
|
183
|
+
* Error sanitization and network error utilities.
|
|
184
|
+
*/
|
|
185
|
+
export { sanitizeError, NetworkError, isNetworkError } from "./errors.js";
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Type Utilities (Internal)
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @internal
|
|
193
|
+
* Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>.
|
|
194
|
+
*/
|
|
195
|
+
export type { ScopedRouteMap } from "./types.js";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @internal
|
|
199
|
+
* Type-level utilities for reverse URL generation.
|
|
200
|
+
*/
|
|
201
|
+
export type { MergeRoutes, SanitizePrefix } from "./reverse.js";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @internal
|
|
205
|
+
* Individual telemetry event types.
|
|
206
|
+
*/
|
|
207
|
+
export type {
|
|
208
|
+
RequestStartEvent,
|
|
209
|
+
RequestEndEvent,
|
|
210
|
+
RequestErrorEvent,
|
|
211
|
+
RequestTimeoutEvent,
|
|
212
|
+
LoaderStartEvent,
|
|
213
|
+
LoaderEndEvent,
|
|
214
|
+
LoaderErrorEvent,
|
|
215
|
+
HandlerErrorEvent,
|
|
216
|
+
CacheDecisionEvent,
|
|
217
|
+
RevalidationDecisionEvent,
|
|
218
|
+
} from "./router/telemetry.js";
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Pre-render / Static Handler Guards (Internal)
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @internal
|
|
226
|
+
* Type guard for prerender handler definitions.
|
|
227
|
+
*/
|
|
228
|
+
export { isPrerenderHandler } from "./prerender.js";
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @internal
|
|
232
|
+
* Type guard for static handler definitions.
|
|
233
|
+
*/
|
|
234
|
+
export { isStaticHandler } from "./static-handler.js";
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// URL Pattern Internals
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @internal
|
|
242
|
+
* Sentinel used to tag response-type route entries.
|
|
243
|
+
*/
|
|
244
|
+
export { RESPONSE_TYPE } from "./urls.js";
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Route Match Debug (Internal)
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @internal
|
|
252
|
+
* Debug utilities for route matching performance analysis.
|
|
253
|
+
*/
|
|
254
|
+
export {
|
|
255
|
+
enableMatchDebug,
|
|
256
|
+
getMatchDebugStats,
|
|
257
|
+
} from "./router/pattern-matching.js";
|
|
258
|
+
|
|
167
259
|
// ============================================================================
|
|
168
260
|
// Debug Utilities (Internal)
|
|
169
261
|
// ============================================================================
|
package/src/bin/rango.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
writeCombinedRouteTypes,
|
|
7
7
|
detectUnresolvableIncludes,
|
|
8
8
|
detectUnresolvableIncludesForUrlsFile,
|
|
9
|
+
findNestedRouterConflict,
|
|
10
|
+
formatNestedRouterConflictError,
|
|
9
11
|
type UnresolvableInclude,
|
|
10
12
|
} from "../build/generate-route-types.ts";
|
|
11
13
|
|
|
@@ -205,6 +207,14 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
205
207
|
console.warn("");
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFiles);
|
|
211
|
+
if (nestedRouterConflict) {
|
|
212
|
+
console.error(
|
|
213
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
// Phase 3: Write all outputs (only reached if diagnostics pass or --static)
|
|
209
219
|
for (const urlsFile of urlsFiles) {
|
|
210
220
|
writePerModuleRouteTypesForFile(urlsFile);
|
|
@@ -259,6 +269,14 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
259
269
|
process.exit(1);
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
const nestedRouterConflict = findNestedRouterConflict(routerEntries);
|
|
273
|
+
if (nestedRouterConflict) {
|
|
274
|
+
console.error(
|
|
275
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
262
280
|
let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
|
|
263
281
|
try {
|
|
264
282
|
const mod = await import("../build/runtime-discovery.ts");
|
|
@@ -117,6 +117,7 @@ export function setupLinkInterception(
|
|
|
117
117
|
// Read navigation options from data attributes (set by Link component)
|
|
118
118
|
const scrollAttr = link.getAttribute("data-scroll");
|
|
119
119
|
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
120
121
|
|
|
121
122
|
const navigateOptions: NavigateOptions = {};
|
|
122
123
|
if (scrollAttr === "false") {
|
|
@@ -125,6 +126,9 @@ export function setupLinkInterception(
|
|
|
125
126
|
if (replaceAttr === "true") {
|
|
126
127
|
navigateOptions.replace = true;
|
|
127
128
|
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
128
132
|
|
|
129
133
|
onNavigate(href, navigateOptions);
|
|
130
134
|
};
|