@rangojs/router 0.0.0-experimental.21 → 0.0.0-experimental.22
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 +43 -10
- package/dist/vite/index.js +2 -3
- package/package.json +1 -1
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +13 -12
- package/skills/host-router/SKILL.md +218 -0
- package/skills/loader/SKILL.md +2 -2
- package/skills/prerender/SKILL.md +2 -2
- package/skills/rango/SKILL.md +0 -1
- package/skills/router-setup/SKILL.md +2 -2
- package/skills/typesafety/SKILL.md +1 -1
- package/src/host/index.ts +0 -3
- package/src/index.ts +30 -31
- package/src/route-definition/index.ts +0 -3
- package/src/router/router-options.ts +1 -1
- package/src/rsc/index.ts +0 -20
- package/src/server.ts +6 -0
- package/src/theme/index.ts +4 -13
- package/src/vite/discovery/discover-routers.ts +2 -3
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
package/README.md
CHANGED
|
@@ -45,6 +45,30 @@ For Cloudflare Workers:
|
|
|
45
45
|
npm install @cloudflare/vite-plugin
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## Import Paths
|
|
49
|
+
|
|
50
|
+
Use these import paths consistently:
|
|
51
|
+
|
|
52
|
+
- `@rangojs/router` — server/RSC router APIs, route DSL, `createRouter`, `urls`, `redirect`, `Prerender`, `Static`, shared types
|
|
53
|
+
- `@rangojs/router/client` — hooks and components such as `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `useAction`, `useLocationState`
|
|
54
|
+
- `@rangojs/router/cache` — public cache APIs such as `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware`
|
|
55
|
+
- `@rangojs/router/host`, `@rangojs/router/theme`, `@rangojs/router/vite` — specialized public subpaths
|
|
56
|
+
- `@rangojs/router/rsc`, `@rangojs/router/ssr` — advanced server-only integration subpaths for custom request/HTML pipelines
|
|
57
|
+
|
|
58
|
+
Use only subpaths that are explicitly exported from the package. Avoid deep imports such as `@rangojs/router/cache/cf`.
|
|
59
|
+
|
|
60
|
+
`@rangojs/router` is conditionally resolved. Server-only root APIs such as
|
|
61
|
+
`createRouter()`, `urls()`, `redirect()`, `Prerender()`, and `cookies()` rely on
|
|
62
|
+
the `react-server` export condition and are meant to run in router definitions,
|
|
63
|
+
handlers, and other RSC/server modules. Outside that environment the root entry
|
|
64
|
+
falls back to stub implementations that throw guidance errors.
|
|
65
|
+
|
|
66
|
+
If you hit a root-entrypoint stub error:
|
|
67
|
+
|
|
68
|
+
- hooks and components like `Link`, `Outlet`, `useLoader`, `useNavigation`, and `MetaTags` belong in `@rangojs/router/client`
|
|
69
|
+
- cache APIs like `CFCacheStore` and `createDocumentCacheMiddleware` belong in `@rangojs/router/cache`
|
|
70
|
+
- host-router APIs belong in `@rangojs/router/host`
|
|
71
|
+
|
|
48
72
|
## Quick Start
|
|
49
73
|
|
|
50
74
|
### Vite Config
|
|
@@ -62,6 +86,9 @@ export default defineConfig({
|
|
|
62
86
|
|
|
63
87
|
### Router
|
|
64
88
|
|
|
89
|
+
This file is a server/RSC module and should import router construction APIs from
|
|
90
|
+
`@rangojs/router`.
|
|
91
|
+
|
|
65
92
|
```tsx
|
|
66
93
|
// src/router.tsx
|
|
67
94
|
import { createRouter, urls } from "@rangojs/router";
|
|
@@ -842,16 +869,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
|
|
|
842
869
|
|
|
843
870
|
## Subpath Exports
|
|
844
871
|
|
|
845
|
-
| Export | Description
|
|
846
|
-
| ------------------------ |
|
|
847
|
-
| `@rangojs/router` |
|
|
848
|
-
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags`
|
|
849
|
-
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware`
|
|
850
|
-
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript`
|
|
851
|
-
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts`
|
|
852
|
-
| `@rangojs/router/vite` | Vite plugin: `rango()`
|
|
853
|
-
| `@rangojs/router/server`
|
|
854
|
-
| `@rangojs/router/
|
|
872
|
+
| Export | Description |
|
|
873
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
874
|
+
| `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
|
|
875
|
+
| `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
|
|
876
|
+
| `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
|
|
877
|
+
| `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
|
|
878
|
+
| `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
|
|
879
|
+
| `@rangojs/router/vite` | Vite plugin: `rango()` |
|
|
880
|
+
| `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
|
|
881
|
+
| `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
|
|
882
|
+
| `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
|
|
883
|
+
| `@rangojs/router/build` | Build utilities |
|
|
884
|
+
|
|
885
|
+
The root entrypoint is not a generic client/runtime barrel. If you need hooks
|
|
886
|
+
or components, import from `@rangojs/router/client`; if you need cache or host
|
|
887
|
+
APIs, use their dedicated subpaths.
|
|
855
888
|
|
|
856
889
|
## Examples
|
|
857
890
|
|
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.22",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
|
@@ -3733,8 +3733,7 @@ async function discoverRouters(state, rscEnv) {
|
|
|
3733
3733
|
let registry = serverMod.RouterRegistry;
|
|
3734
3734
|
if (!registry || registry.size === 0) {
|
|
3735
3735
|
try {
|
|
3736
|
-
const
|
|
3737
|
-
const hostRegistry = hostMod.HostRouterRegistry;
|
|
3736
|
+
const hostRegistry = serverMod.HostRouterRegistry;
|
|
3738
3737
|
if (hostRegistry && hostRegistry.size > 0) {
|
|
3739
3738
|
console.log(
|
|
3740
3739
|
`[rsc-router] Found ${hostRegistry.size} host router(s), resolving lazy handlers...`
|
package/package.json
CHANGED
package/skills/caching/SKILL.md
CHANGED
|
@@ -89,7 +89,7 @@ Configure a cache store in the router:
|
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
91
|
import { createRouter } from "@rangojs/router";
|
|
92
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
92
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
93
93
|
|
|
94
94
|
const store = new MemorySegmentCacheStore({
|
|
95
95
|
defaults: { ttl: 60, swr: 300 },
|
|
@@ -112,7 +112,7 @@ const router = createRouter({
|
|
|
112
112
|
For single-instance deployments:
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
115
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
116
116
|
|
|
117
117
|
const store = new MemorySegmentCacheStore({
|
|
118
118
|
defaults: { ttl: 60, swr: 300 },
|
|
@@ -125,7 +125,7 @@ const store = new MemorySegmentCacheStore({
|
|
|
125
125
|
For distributed caching on Cloudflare Workers:
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
128
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
129
129
|
|
|
130
130
|
const router = createRouter<AppBindings>({
|
|
131
131
|
document: Document,
|
|
@@ -175,7 +175,7 @@ cache({ store: checkoutCache }, () => [
|
|
|
175
175
|
|
|
176
176
|
```typescript
|
|
177
177
|
import { urls } from "@rangojs/router";
|
|
178
|
-
import { MemorySegmentCacheStore } from "@rangojs/router/
|
|
178
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
179
179
|
|
|
180
180
|
// Custom store for checkout (short TTL)
|
|
181
181
|
const checkoutCache = new MemorySegmentCacheStore({
|
|
@@ -14,7 +14,7 @@ Configure document cache in router:
|
|
|
14
14
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { createRouter } from "@rangojs/router";
|
|
17
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
17
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
18
18
|
import { urlpatterns } from "./urls";
|
|
19
19
|
|
|
20
20
|
const router = createRouter<AppBindings>({
|
|
@@ -134,7 +134,7 @@ Segment hash ensures different cached responses for navigations from different s
|
|
|
134
134
|
```typescript
|
|
135
135
|
// router.tsx
|
|
136
136
|
import { createRouter } from "@rangojs/router";
|
|
137
|
-
import { CFCacheStore } from "@rangojs/router/cache
|
|
137
|
+
import { CFCacheStore } from "@rangojs/router/cache";
|
|
138
138
|
import { urlpatterns } from "./urls";
|
|
139
139
|
|
|
140
140
|
const router = createRouter<AppBindings>({
|
package/skills/hooks/SKILL.md
CHANGED
|
@@ -6,7 +6,8 @@ argument-hint: [hook-name]
|
|
|
6
6
|
|
|
7
7
|
# Client-Side React Hooks
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Import the hooks and components in this skill from `@rangojs/router/client`.
|
|
10
|
+
The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
|
|
10
11
|
|
|
11
12
|
## Navigation Hooks
|
|
12
13
|
|
|
@@ -63,7 +64,7 @@ Access current URL path and matched route segments:
|
|
|
63
64
|
|
|
64
65
|
```tsx
|
|
65
66
|
"use client";
|
|
66
|
-
import { useSegments } from "@rangojs/router";
|
|
67
|
+
import { useSegments } from "@rangojs/router/client";
|
|
67
68
|
|
|
68
69
|
function Breadcrumbs() {
|
|
69
70
|
const { path, segmentIds, location } = useSegments();
|
|
@@ -107,7 +108,7 @@ Access loader data (strict - data guaranteed):
|
|
|
107
108
|
|
|
108
109
|
```tsx
|
|
109
110
|
"use client";
|
|
110
|
-
import { useLoader } from "@rangojs/router";
|
|
111
|
+
import { useLoader } from "@rangojs/router/client";
|
|
111
112
|
import { ProductLoader } from "../loaders/product";
|
|
112
113
|
|
|
113
114
|
function ProductPrice() {
|
|
@@ -143,7 +144,7 @@ Access loader with on-demand fetching (flexible):
|
|
|
143
144
|
|
|
144
145
|
```tsx
|
|
145
146
|
"use client";
|
|
146
|
-
import { useFetchLoader } from "@rangojs/router";
|
|
147
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
147
148
|
import { SearchLoader } from "../loaders/search";
|
|
148
149
|
|
|
149
150
|
function SearchResults() {
|
|
@@ -197,7 +198,7 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
|
|
|
197
198
|
|
|
198
199
|
```tsx
|
|
199
200
|
"use client";
|
|
200
|
-
import { useFetchLoader } from "@rangojs/router";
|
|
201
|
+
import { useFetchLoader } from "@rangojs/router/client";
|
|
201
202
|
import { FileUploadLoader } from "../loaders/upload";
|
|
202
203
|
|
|
203
204
|
function FileUploader() {
|
|
@@ -244,7 +245,7 @@ Get all loader data in current context:
|
|
|
244
245
|
|
|
245
246
|
```tsx
|
|
246
247
|
"use client";
|
|
247
|
-
import { useLoaderData } from "@rangojs/router";
|
|
248
|
+
import { useLoaderData } from "@rangojs/router/client";
|
|
248
249
|
|
|
249
250
|
function DebugPanel() {
|
|
250
251
|
const allData = useLoaderData();
|
|
@@ -262,7 +263,7 @@ Access accumulated handle data from route segments:
|
|
|
262
263
|
|
|
263
264
|
```tsx
|
|
264
265
|
"use client";
|
|
265
|
-
import { useHandle } from "@rangojs/router";
|
|
266
|
+
import { useHandle } from "@rangojs/router/client";
|
|
266
267
|
import { Breadcrumbs } from "../handles/breadcrumbs";
|
|
267
268
|
|
|
268
269
|
function BreadcrumbNav() {
|
|
@@ -324,7 +325,7 @@ Track state of server action invocations:
|
|
|
324
325
|
|
|
325
326
|
```tsx
|
|
326
327
|
"use client";
|
|
327
|
-
import { useAction } from "@rangojs/router";
|
|
328
|
+
import { useAction } from "@rangojs/router/client";
|
|
328
329
|
import { addToCart } from "../actions/cart";
|
|
329
330
|
|
|
330
331
|
function AddToCartButton({ productId }: { productId: string }) {
|
|
@@ -359,7 +360,7 @@ Read type-safe state from history:
|
|
|
359
360
|
|
|
360
361
|
```tsx
|
|
361
362
|
"use client";
|
|
362
|
-
import { useLocationState, createLocationState } from "@rangojs/router";
|
|
363
|
+
import { useLocationState, createLocationState } from "@rangojs/router/client";
|
|
363
364
|
|
|
364
365
|
// Define typed state (all export patterns supported)
|
|
365
366
|
// Keys are auto-injected by the Vite plugin -- no manual key needed.
|
|
@@ -509,7 +510,7 @@ Manually control client-side navigation cache:
|
|
|
509
510
|
|
|
510
511
|
```tsx
|
|
511
512
|
"use client";
|
|
512
|
-
import { useClientCache } from "@rangojs/router";
|
|
513
|
+
import { useClientCache } from "@rangojs/router/client";
|
|
513
514
|
|
|
514
515
|
function SaveButton() {
|
|
515
516
|
const { clear } = useClientCache();
|
|
@@ -537,7 +538,7 @@ function SaveButton() {
|
|
|
537
538
|
Render child content in layouts:
|
|
538
539
|
|
|
539
540
|
```tsx
|
|
540
|
-
import { Outlet, ParallelOutlet } from "@rangojs/router";
|
|
541
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
541
542
|
|
|
542
543
|
function DashboardLayout({ children }: { children?: React.ReactNode }) {
|
|
543
544
|
return (
|
|
@@ -558,7 +559,7 @@ Access outlet content programmatically:
|
|
|
558
559
|
|
|
559
560
|
```tsx
|
|
560
561
|
"use client";
|
|
561
|
-
import { useOutlet } from "@rangojs/router";
|
|
562
|
+
import { useOutlet } from "@rangojs/router/client";
|
|
562
563
|
|
|
563
564
|
function ConditionalLayout() {
|
|
564
565
|
const outlet = useOutlet();
|
|
@@ -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
|
@@ -68,7 +68,7 @@ export const urlpatterns = urls(({ path, loader }) => [
|
|
|
68
68
|
### In Server Components
|
|
69
69
|
|
|
70
70
|
```typescript
|
|
71
|
-
import { useLoader } from "@rangojs/router";
|
|
71
|
+
import { useLoader } from "@rangojs/router/client";
|
|
72
72
|
import { ProductLoader } from "./loaders/product";
|
|
73
73
|
|
|
74
74
|
async function ProductPage() {
|
|
@@ -539,7 +539,7 @@ export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalid
|
|
|
539
539
|
]);
|
|
540
540
|
|
|
541
541
|
// pages/product.tsx
|
|
542
|
-
import { useLoader } from "@rangojs/router";
|
|
542
|
+
import { useLoader } from "@rangojs/router/client";
|
|
543
543
|
import { ProductLoader, CartLoader } from "./loaders/shop";
|
|
544
544
|
|
|
545
545
|
async function ProductPage() {
|
|
@@ -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
|
|
|
@@ -297,10 +297,10 @@ export const shopPatterns = urls(({ path, layout }) => [
|
|
|
297
297
|
]);
|
|
298
298
|
|
|
299
299
|
// src/urls.tsx
|
|
300
|
-
import { urls
|
|
300
|
+
import { urls } from "@rangojs/router";
|
|
301
301
|
import { shopPatterns } from "./urls/shop";
|
|
302
302
|
|
|
303
|
-
export const urlpatterns = urls(({ path }) => [
|
|
303
|
+
export const urlpatterns = urls(({ path, include }) => [
|
|
304
304
|
path("/", HomePage, { name: "home" }),
|
|
305
305
|
include("/shop", shopPatterns, { name: "shop" }),
|
|
306
306
|
]);
|
|
@@ -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);
|
package/src/host/index.ts
CHANGED
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
// Core router
|
|
26
26
|
export { createHostRouter } from "./router.js";
|
|
27
27
|
|
|
28
|
-
// Host router registry for build-time discovery
|
|
29
|
-
export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
|
|
30
|
-
|
|
31
28
|
// Utilities
|
|
32
29
|
export { defineHosts } from "./utils.js";
|
|
33
30
|
|
package/src/index.ts
CHANGED
|
@@ -115,25 +115,32 @@ export type {
|
|
|
115
115
|
// Middleware context types
|
|
116
116
|
export type { MiddlewareContext, CookieOptions } from "./router/middleware.js";
|
|
117
117
|
|
|
118
|
+
function serverOnlyStubError(name: string): Error {
|
|
119
|
+
return new Error(
|
|
120
|
+
`${name}() is only available from "@rangojs/router" in a react-server/RSC environment. ` +
|
|
121
|
+
`For client hooks and components, import from "@rangojs/router/client".`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
/**
|
|
119
126
|
* Error-throwing stub for server-only `urls` function.
|
|
120
127
|
*/
|
|
121
128
|
export function urls(): never {
|
|
122
|
-
throw
|
|
129
|
+
throw serverOnlyStubError("urls");
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/**
|
|
126
133
|
* Error-throwing stub for server-only `createRouter` function.
|
|
127
134
|
*/
|
|
128
135
|
export function createRouter(): never {
|
|
129
|
-
throw
|
|
136
|
+
throw serverOnlyStubError("createRouter");
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
/**
|
|
133
140
|
* Error-throwing stub for server-only `redirect` function.
|
|
134
141
|
*/
|
|
135
142
|
export function redirect(): never {
|
|
136
|
-
throw
|
|
143
|
+
throw serverOnlyStubError("redirect");
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// Handle API (universal - works on both server and client)
|
|
@@ -149,102 +156,94 @@ export { nonce } from "./rsc/nonce.js";
|
|
|
149
156
|
* Error-throwing stub for server-only `Prerender` function.
|
|
150
157
|
*/
|
|
151
158
|
export function Prerender(): never {
|
|
152
|
-
throw
|
|
159
|
+
throw serverOnlyStubError("Prerender");
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
/**
|
|
156
163
|
* Error-throwing stub for server-only `Static` function.
|
|
157
164
|
*/
|
|
158
165
|
export function Static(): never {
|
|
159
|
-
throw
|
|
166
|
+
throw serverOnlyStubError("Static");
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
/**
|
|
163
170
|
* Error-throwing stub for server-only `getRequestContext` function.
|
|
164
171
|
*/
|
|
165
172
|
export function getRequestContext(): never {
|
|
166
|
-
throw
|
|
167
|
-
"getRequestContext() is server-only and requires RSC context.",
|
|
168
|
-
);
|
|
173
|
+
throw serverOnlyStubError("getRequestContext");
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
/**
|
|
172
177
|
* Error-throwing stub for server-only `cookies` function.
|
|
173
178
|
*/
|
|
174
179
|
export function cookies(): never {
|
|
175
|
-
throw
|
|
180
|
+
throw serverOnlyStubError("cookies");
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
/**
|
|
179
184
|
* Error-throwing stub for server-only `headers` function.
|
|
180
185
|
*/
|
|
181
186
|
export function headers(): never {
|
|
182
|
-
throw
|
|
187
|
+
throw serverOnlyStubError("headers");
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
/**
|
|
186
191
|
* Error-throwing stub for server-only `createReverse` function.
|
|
187
192
|
*/
|
|
188
193
|
export function createReverse(): never {
|
|
189
|
-
throw
|
|
194
|
+
throw serverOnlyStubError("createReverse");
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
/**
|
|
193
198
|
* Error-throwing stub for server-only `enableMatchDebug` function.
|
|
194
199
|
*/
|
|
195
200
|
export function enableMatchDebug(): never {
|
|
196
|
-
throw
|
|
197
|
-
"enableMatchDebug() is server-only and requires RSC context.",
|
|
198
|
-
);
|
|
201
|
+
throw serverOnlyStubError("enableMatchDebug");
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
/**
|
|
202
205
|
* Error-throwing stub for server-only `getMatchDebugStats` function.
|
|
203
206
|
*/
|
|
204
207
|
export function getMatchDebugStats(): never {
|
|
205
|
-
throw
|
|
206
|
-
"getMatchDebugStats() is server-only and requires RSC context.",
|
|
207
|
-
);
|
|
208
|
+
throw serverOnlyStubError("getMatchDebugStats");
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
// Error-throwing stubs for server-only route helpers
|
|
211
212
|
export function layout(): never {
|
|
212
|
-
throw
|
|
213
|
+
throw serverOnlyStubError("layout");
|
|
213
214
|
}
|
|
214
215
|
export function cache(): never {
|
|
215
|
-
throw
|
|
216
|
+
throw serverOnlyStubError("cache");
|
|
216
217
|
}
|
|
217
218
|
export function middleware(): never {
|
|
218
|
-
throw
|
|
219
|
+
throw serverOnlyStubError("middleware");
|
|
219
220
|
}
|
|
220
221
|
export function revalidate(): never {
|
|
221
|
-
throw
|
|
222
|
+
throw serverOnlyStubError("revalidate");
|
|
222
223
|
}
|
|
223
224
|
export function loader(): never {
|
|
224
|
-
throw
|
|
225
|
+
throw serverOnlyStubError("loader");
|
|
225
226
|
}
|
|
226
227
|
export function loading(): never {
|
|
227
|
-
throw
|
|
228
|
+
throw serverOnlyStubError("loading");
|
|
228
229
|
}
|
|
229
230
|
export function parallel(): never {
|
|
230
|
-
throw
|
|
231
|
+
throw serverOnlyStubError("parallel");
|
|
231
232
|
}
|
|
232
233
|
export function intercept(): never {
|
|
233
|
-
throw
|
|
234
|
+
throw serverOnlyStubError("intercept");
|
|
234
235
|
}
|
|
235
236
|
export function when(): never {
|
|
236
|
-
throw
|
|
237
|
+
throw serverOnlyStubError("when");
|
|
237
238
|
}
|
|
238
239
|
export function errorBoundary(): never {
|
|
239
|
-
throw
|
|
240
|
+
throw serverOnlyStubError("errorBoundary");
|
|
240
241
|
}
|
|
241
242
|
export function notFoundBoundary(): never {
|
|
242
|
-
throw
|
|
243
|
-
"notFoundBoundary() is server-only and requires RSC context.",
|
|
244
|
-
);
|
|
243
|
+
throw serverOnlyStubError("notFoundBoundary");
|
|
245
244
|
}
|
|
246
245
|
export function transition(): never {
|
|
247
|
-
throw
|
|
246
|
+
throw serverOnlyStubError("transition");
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
// Request context type (safe for client)
|
|
@@ -239,7 +239,7 @@ export interface RSCRouterOptions<TEnv = any> {
|
|
|
239
239
|
*
|
|
240
240
|
* @example Static config
|
|
241
241
|
* ```typescript
|
|
242
|
-
* import { MemorySegmentCacheStore } from "
|
|
242
|
+
* import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
243
243
|
*
|
|
244
244
|
* const router = createRouter({
|
|
245
245
|
* cache: {
|
package/src/rsc/index.ts
CHANGED
|
@@ -29,28 +29,8 @@ export type {
|
|
|
29
29
|
NonceProvider,
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
|
|
32
|
-
// Re-export HandleStore types for consumers who need custom handling
|
|
33
|
-
export {
|
|
34
|
-
createHandleStore,
|
|
35
|
-
type HandleStore,
|
|
36
|
-
type HandleData,
|
|
37
|
-
} from "../server/handle-store.js";
|
|
38
|
-
|
|
39
32
|
// Re-export request context utilities for server-side access to env/request/params
|
|
40
33
|
export {
|
|
41
34
|
getRequestContext,
|
|
42
35
|
requireRequestContext,
|
|
43
|
-
setRequestContextParams,
|
|
44
36
|
} from "../server/request-context.js";
|
|
45
|
-
|
|
46
|
-
// Re-export cache store types and implementations
|
|
47
|
-
export type {
|
|
48
|
-
SegmentCacheStore,
|
|
49
|
-
CachedEntryData,
|
|
50
|
-
CachedEntryResult,
|
|
51
|
-
SegmentCacheProvider,
|
|
52
|
-
SegmentHandleData,
|
|
53
|
-
} from "../cache/types.js";
|
|
54
|
-
|
|
55
|
-
export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
|
|
56
|
-
export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// Router registry (used by Vite plugin for build-time discovery)
|
|
12
12
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router.js";
|
|
13
13
|
|
|
14
|
+
// Host router registry (used by Vite plugin for host-router lazy discovery)
|
|
15
|
+
export {
|
|
16
|
+
HostRouterRegistry,
|
|
17
|
+
type HostRouterRegistryEntry,
|
|
18
|
+
} from "./host/router.js";
|
|
19
|
+
|
|
14
20
|
// Route map builder (Vite plugin injects these via virtual modules)
|
|
15
21
|
export {
|
|
16
22
|
registerRouteMap,
|
package/src/theme/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme module exports for @rangojs/router/theme
|
|
3
3
|
*
|
|
4
|
-
* This module provides
|
|
4
|
+
* This module provides the public theme API:
|
|
5
5
|
* - useTheme: Hook for accessing theme state in client components
|
|
6
6
|
* - ThemeProvider: Component for manual theme provider setup (typically not needed)
|
|
7
|
+
* - ThemeScript: FOUC-prevention script component for document/head usage
|
|
7
8
|
* - Types for theme configuration
|
|
8
9
|
*
|
|
9
10
|
* @example
|
|
@@ -43,15 +44,5 @@ export type {
|
|
|
43
44
|
ThemeContextValue,
|
|
44
45
|
} from "./types.js";
|
|
45
46
|
|
|
46
|
-
// Constants
|
|
47
|
-
export {
|
|
48
|
-
THEME_DEFAULTS,
|
|
49
|
-
THEME_COOKIE,
|
|
50
|
-
resolveThemeConfig,
|
|
51
|
-
} from "./constants.js";
|
|
52
|
-
|
|
53
|
-
// Script generation (for advanced SSR use cases)
|
|
54
|
-
export { generateThemeScript, getNonceAttribute } from "./theme-script.js";
|
|
55
|
-
|
|
56
|
-
// Context (for advanced use cases)
|
|
57
|
-
export { ThemeContext, useThemeContext } from "./theme-context.js";
|
|
47
|
+
// Constants
|
|
48
|
+
export { THEME_DEFAULTS, THEME_COOKIE } from "./constants.js";
|
|
@@ -48,9 +48,8 @@ export async function discoverRouters(
|
|
|
48
48
|
// No RSC routers found directly. Check for host routers with lazy handlers
|
|
49
49
|
// that need to be resolved to trigger sub-app createRouter() calls.
|
|
50
50
|
try {
|
|
51
|
-
const hostMod = await rscEnv.runner.import("@rangojs/router/host");
|
|
52
51
|
const hostRegistry: Map<string, any> | undefined =
|
|
53
|
-
|
|
52
|
+
serverMod.HostRouterRegistry;
|
|
54
53
|
|
|
55
54
|
if (hostRegistry && hostRegistry.size > 0) {
|
|
56
55
|
console.log(
|
|
@@ -89,7 +88,7 @@ export async function discoverRouters(
|
|
|
89
88
|
}
|
|
90
89
|
}
|
|
91
90
|
} catch {
|
|
92
|
-
//
|
|
91
|
+
// Host-router discovery is best-effort; skip if unavailable
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
// If still no routers after host router resolution, fail
|
package/skills/testing/SKILL.md
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: testing
|
|
3
|
-
description: Unit test route trees with buildRouteTree()
|
|
4
|
-
argument-hint:
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Route Tree Unit Testing
|
|
8
|
-
|
|
9
|
-
Unit test route definitions by inspecting the route tree, segment IDs, middleware, intercepts, loaders, and pattern matching without running a dev server.
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
The `buildRouteTree` helper lives in `src/__tests__/helpers/route-tree.ts` (not shipped with npm). Import it in your test files:
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { buildRouteTree } from "./helpers/route-tree.js";
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## buildRouteTree(urlPatterns)
|
|
20
|
-
|
|
21
|
-
Takes a `urls()` result and returns a `RouteTree` with inspection methods:
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import { urls } from "@rangojs/router";
|
|
25
|
-
import { buildRouteTree } from "./helpers/route-tree.js";
|
|
26
|
-
|
|
27
|
-
const tree = buildRouteTree(
|
|
28
|
-
urls(({ path, layout, middleware, loader, intercept, when }) => [
|
|
29
|
-
layout(RootLayout, () => [
|
|
30
|
-
middleware(authMiddleware),
|
|
31
|
-
path("/", HomePage, { name: "home" }),
|
|
32
|
-
path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
|
|
33
|
-
loader(PostLoader),
|
|
34
|
-
]),
|
|
35
|
-
]),
|
|
36
|
-
]),
|
|
37
|
-
);
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## RouteTree API
|
|
41
|
-
|
|
42
|
-
### Route Patterns
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
tree.routes(); // { home: "/", "blog.post": "/blog/:slug" }
|
|
46
|
-
tree.routeNames(); // ["home", "blog.post"]
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### URL Matching
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
const m = tree.match("/blog/hello");
|
|
53
|
-
m.routeKey; // "blog.post"
|
|
54
|
-
m.params; // { slug: "hello" }
|
|
55
|
-
|
|
56
|
-
tree.match("/nonexistent"); // null
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Segment IDs
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
tree.segmentId("home"); // "M0L0L0R0"
|
|
63
|
-
tree.segmentIds(); // { home: "M0L0L0R0", "blog.post": "M0L0L0R1" }
|
|
64
|
-
tree.segmentPath("blog.post");
|
|
65
|
-
// [
|
|
66
|
-
// { id: "M0L0", type: "layout" }, // synthetic root
|
|
67
|
-
// { id: "M0L0L0", type: "layout" }, // RootLayout
|
|
68
|
-
// { id: "M0L0L0R1", type: "route", pattern: "/blog/:slug" },
|
|
69
|
-
// ]
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Entry Access
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
tree.entry("blog.post"); // EntryData
|
|
76
|
-
tree.entry("blog.post")!.parent!.type; // "layout"
|
|
77
|
-
tree.entryByPattern("/blog/:slug"); // EntryData (lookup by URL pattern)
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Middleware
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
tree.hasMiddleware("home"); // true (inherited from layout)
|
|
84
|
-
tree.middleware("home"); // [authMiddleware] (direct only)
|
|
85
|
-
tree.middlewareChain("home");
|
|
86
|
-
// [{ segmentId: "M0L0L0", count: 1 }] // all middleware root-to-route
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### Loaders
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
tree.hasLoaders("blog.post"); // true
|
|
93
|
-
tree.loaders("blog.post"); // [LoaderEntry { loader, revalidate, cache? }]
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Intercepts
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
tree.intercepts("home");
|
|
100
|
-
// [{ slotName: "@modal", routeName: "card", hasWhen: true, whenCount: 1, hasLoader: false, hasMiddleware: false }]
|
|
101
|
-
tree.interceptEntries("home"); // raw InterceptEntry[]
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Parallel Slots
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
tree.parallelSlots("home"); // EntryData[] of type="parallel"
|
|
108
|
-
tree.parallelSlotNames("home"); // ["@sidebar", "@main"]
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Boundaries
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
tree.hasErrorBoundary("home"); // boolean
|
|
115
|
-
tree.hasNotFoundBoundary("home"); // boolean
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Cache & Loading
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
tree.hasCache("home"); // boolean
|
|
122
|
-
tree.hasLoading("home"); // boolean
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Debug
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
console.log(tree.debug());
|
|
129
|
-
// Route Tree:
|
|
130
|
-
// home: / [M0L0L0R0] (M0L0 > M0L0L0 > M0L0L0R0) {mw:1}
|
|
131
|
-
// blog.post: /blog/:slug [M0L0L0R1] (M0L0 > M0L0L0 > M0L0L0R1) {mw:1, ld:1}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Segment ID Format
|
|
135
|
-
|
|
136
|
-
| Prefix | Meaning |
|
|
137
|
-
| ------ | ----------------------------- |
|
|
138
|
-
| `M0` | Mount index (router instance) |
|
|
139
|
-
| `L` | Layout |
|
|
140
|
-
| `R` | Route |
|
|
141
|
-
| `P` | Parallel slot |
|
|
142
|
-
| `D` | Loader (data) |
|
|
143
|
-
| `C` | Cache boundary |
|
|
144
|
-
|
|
145
|
-
Example: `M0L0L0R1` = mount 0, synthetic root layout, user layout, second route.
|
|
146
|
-
|
|
147
|
-
## Examples
|
|
148
|
-
|
|
149
|
-
### include() with prefix
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
const blogPatterns = urls(({ path }) => [
|
|
153
|
-
path("/", BlogIndex, { name: "index" }),
|
|
154
|
-
path("/:slug", BlogPost, { name: "post" }),
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
const tree = buildRouteTree(
|
|
158
|
-
urls(({ path, include }) => [
|
|
159
|
-
path("/", HomePage, { name: "home" }),
|
|
160
|
-
include("/blog", blogPatterns, { name: "blog" }),
|
|
161
|
-
]),
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
expect(tree.routes()).toEqual({
|
|
165
|
-
home: "/",
|
|
166
|
-
"blog.index": "/blog",
|
|
167
|
-
"blog.post": "/blog/:slug",
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Middleware chain
|
|
172
|
-
|
|
173
|
-
```typescript
|
|
174
|
-
const authMw = async (ctx, next) => next();
|
|
175
|
-
const logMw = async (ctx, next) => next();
|
|
176
|
-
|
|
177
|
-
const tree = buildRouteTree(
|
|
178
|
-
urls(({ path, layout, middleware }) => [
|
|
179
|
-
layout(RootLayout, () => [
|
|
180
|
-
middleware(logMw),
|
|
181
|
-
layout(AuthLayout, () => [
|
|
182
|
-
middleware(authMw),
|
|
183
|
-
path("/dashboard", Dashboard, { name: "dashboard" }),
|
|
184
|
-
]),
|
|
185
|
-
]),
|
|
186
|
-
]),
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
expect(tree.middlewareChain("dashboard")).toEqual([
|
|
190
|
-
{ segmentId: "M0L0L0", count: 1 }, // logMw on RootLayout
|
|
191
|
-
{ segmentId: "M0L0L0L0", count: 1 }, // authMw on AuthLayout
|
|
192
|
-
]);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Intercepts with when()
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
const tree = buildRouteTree(
|
|
199
|
-
urls(({ path, layout, intercept, when }) => [
|
|
200
|
-
layout(ShopLayout, () => [
|
|
201
|
-
path("/products", ProductList, { name: "products" }),
|
|
202
|
-
path("/products/:id", ProductDetail, { name: "product.detail" }),
|
|
203
|
-
intercept("@modal", "product.detail", ProductModal, () => [
|
|
204
|
-
when((ctx) => ctx.from.pathname.startsWith("/products")),
|
|
205
|
-
]),
|
|
206
|
-
]),
|
|
207
|
-
]),
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const intercepts = tree.intercepts("products");
|
|
211
|
-
// Note: intercepts are on the parent where intercept() is called
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Constrained parameters
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
const tree = buildRouteTree(
|
|
218
|
-
urls(({ path }) => [
|
|
219
|
-
path("/:locale(en|fr)?/about", AboutPage, { name: "about" }),
|
|
220
|
-
]),
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
expect(tree.match("/about")).not.toBeNull();
|
|
224
|
-
expect(tree.match("/fr/about")!.params).toEqual({ locale: "fr" });
|
|
225
|
-
expect(tree.match("/de/about")).toBeNull();
|
|
226
|
-
```
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ResolvedRouteMap,
|
|
3
|
-
RouteConfig,
|
|
4
|
-
RouteDefinition,
|
|
5
|
-
RouteDefinitionOptions,
|
|
6
|
-
TrailingSlashMode,
|
|
7
|
-
} from "../types.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Result of route() function with paths and trailing slash config
|
|
11
|
-
*/
|
|
12
|
-
export interface RouteDefinitionResult<T extends RouteDefinition> {
|
|
13
|
-
routes: ResolvedRouteMap<T>;
|
|
14
|
-
trailingSlash: Record<string, TrailingSlashMode>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Check if a value is a RouteConfig object
|
|
19
|
-
*/
|
|
20
|
-
function isRouteConfig(value: unknown): value is RouteConfig {
|
|
21
|
-
return (
|
|
22
|
-
typeof value === "object" &&
|
|
23
|
-
value !== null &&
|
|
24
|
-
"path" in value &&
|
|
25
|
-
typeof (value as RouteConfig).path === "string"
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Define routes with optional trailing slash configuration
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```typescript
|
|
34
|
-
* // Simple string paths
|
|
35
|
-
* const routes = route({
|
|
36
|
-
* blog: "/blog",
|
|
37
|
-
* post: "/blog/:id",
|
|
38
|
-
* });
|
|
39
|
-
*
|
|
40
|
-
* // With trailing slash config
|
|
41
|
-
* const routes = route({
|
|
42
|
-
* blog: "/blog",
|
|
43
|
-
* api: { path: "/api", trailingSlash: "ignore" },
|
|
44
|
-
* }, { trailingSlash: "never" }); // global default
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export function route<const T extends RouteDefinition>(
|
|
48
|
-
input: T,
|
|
49
|
-
options?: RouteDefinitionOptions,
|
|
50
|
-
): ResolvedRouteMap<T> & {
|
|
51
|
-
__trailingSlash?: Record<string, TrailingSlashMode>;
|
|
52
|
-
} {
|
|
53
|
-
const trailingSlash: Record<string, TrailingSlashMode> = {};
|
|
54
|
-
const routes = flattenRoutes(
|
|
55
|
-
input as RouteDefinition,
|
|
56
|
-
"",
|
|
57
|
-
trailingSlash,
|
|
58
|
-
options?.trailingSlash,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Attach trailing slash config as a non-enumerable property
|
|
62
|
-
// This keeps backwards compatibility while passing the config through
|
|
63
|
-
const result = routes as ResolvedRouteMap<T> & {
|
|
64
|
-
__trailingSlash?: Record<string, TrailingSlashMode>;
|
|
65
|
-
};
|
|
66
|
-
if (Object.keys(trailingSlash).length > 0) {
|
|
67
|
-
Object.defineProperty(result, "__trailingSlash", {
|
|
68
|
-
value: trailingSlash,
|
|
69
|
-
enumerable: false,
|
|
70
|
-
writable: false,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Flatten nested route definitions
|
|
79
|
-
*/
|
|
80
|
-
function flattenRoutes(
|
|
81
|
-
routes: RouteDefinition,
|
|
82
|
-
prefix: string,
|
|
83
|
-
trailingSlashConfig: Record<string, TrailingSlashMode>,
|
|
84
|
-
defaultTrailingSlash?: TrailingSlashMode,
|
|
85
|
-
): Record<string, string> {
|
|
86
|
-
const flattened: Record<string, string> = {};
|
|
87
|
-
|
|
88
|
-
for (const [key, value] of Object.entries(routes)) {
|
|
89
|
-
const fullKey = prefix + key;
|
|
90
|
-
|
|
91
|
-
if (typeof value === "string") {
|
|
92
|
-
// Direct route pattern - include prefix
|
|
93
|
-
flattened[fullKey] = value;
|
|
94
|
-
// Apply default trailing slash if set
|
|
95
|
-
if (defaultTrailingSlash) {
|
|
96
|
-
trailingSlashConfig[fullKey] = defaultTrailingSlash;
|
|
97
|
-
}
|
|
98
|
-
} else if (isRouteConfig(value)) {
|
|
99
|
-
// Route config object with path and optional trailingSlash
|
|
100
|
-
flattened[fullKey] = value.path;
|
|
101
|
-
// Use route-specific config or fall back to default
|
|
102
|
-
const mode = value.trailingSlash ?? defaultTrailingSlash;
|
|
103
|
-
if (mode) {
|
|
104
|
-
trailingSlashConfig[fullKey] = mode;
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
// Nested routes - flatten recursively
|
|
108
|
-
const nested = flattenRoutes(
|
|
109
|
-
value,
|
|
110
|
-
`${fullKey}.`,
|
|
111
|
-
trailingSlashConfig,
|
|
112
|
-
defaultTrailingSlash,
|
|
113
|
-
);
|
|
114
|
-
Object.assign(flattened, nested);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return flattened;
|
|
119
|
-
}
|