@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.14
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/dist/vite/index.js +1 -56
- package/package.json +1 -1
- package/skills/rango/SKILL.md +1 -0
- package/skills/testing/SKILL.md +226 -0
- package/skills/typesafety/SKILL.md +34 -22
- package/src/index.rsc.ts +13 -1
- package/src/index.ts +7 -0
- package/src/route-definition.ts +31 -0
- package/src/router/match-middleware/cache-store.ts +1 -1
- package/src/server.ts +13 -158
- package/src/urls.ts +18 -0
- package/src/vite/index.ts +9 -17
package/dist/vite/index.js
CHANGED
|
@@ -20,34 +20,6 @@ function extractRoutesFromSource(code) {
|
|
|
20
20
|
}
|
|
21
21
|
return routes;
|
|
22
22
|
}
|
|
23
|
-
function generatePerModuleTypesSource(routes) {
|
|
24
|
-
const valid = routes.filter(({ name }) => {
|
|
25
|
-
if (!name || /["'\\`\n\r]/.test(name)) {
|
|
26
|
-
console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
return true;
|
|
30
|
-
});
|
|
31
|
-
const deduped = /* @__PURE__ */ new Map();
|
|
32
|
-
for (const { name, pattern, search } of valid) {
|
|
33
|
-
deduped.set(name, { pattern, search });
|
|
34
|
-
}
|
|
35
|
-
const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
36
|
-
const body = sorted.map(([name, { pattern, search }]) => {
|
|
37
|
-
const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
|
|
38
|
-
if (search && Object.keys(search).length > 0) {
|
|
39
|
-
const searchBody = Object.entries(search).map(([k, v]) => `${k}: "${v}"`).join(", ");
|
|
40
|
-
return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
|
|
41
|
-
}
|
|
42
|
-
return ` ${key}: "${pattern}",`;
|
|
43
|
-
}).join("\n");
|
|
44
|
-
return `// Auto-generated by @rangojs/router - do not edit
|
|
45
|
-
export const routes = {
|
|
46
|
-
${body}
|
|
47
|
-
} as const;
|
|
48
|
-
export type routes = typeof routes;
|
|
49
|
-
`;
|
|
50
|
-
}
|
|
51
23
|
function isWhitespace(ch) {
|
|
52
24
|
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
53
25
|
}
|
|
@@ -299,29 +271,6 @@ function findTsFiles(dir, filter) {
|
|
|
299
271
|
}
|
|
300
272
|
return results;
|
|
301
273
|
}
|
|
302
|
-
function writePerModuleRouteTypes(root, filter) {
|
|
303
|
-
const files = findTsFiles(root, filter);
|
|
304
|
-
for (const filePath of files) {
|
|
305
|
-
writePerModuleRouteTypesForFile(filePath);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
function writePerModuleRouteTypesForFile(filePath) {
|
|
309
|
-
try {
|
|
310
|
-
const source = readFileSync(filePath, "utf-8");
|
|
311
|
-
if (!source.includes("urls(")) return;
|
|
312
|
-
const routes = extractRoutesFromSource(source);
|
|
313
|
-
if (routes.length === 0) return;
|
|
314
|
-
const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
|
|
315
|
-
const genSource = generatePerModuleTypesSource(routes);
|
|
316
|
-
const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
|
|
317
|
-
if (existing !== genSource) {
|
|
318
|
-
writeFileSync(genPath, genSource);
|
|
319
|
-
console.log(`[rsc-router] Generated route types -> ${genPath}`);
|
|
320
|
-
}
|
|
321
|
-
} catch (err) {
|
|
322
|
-
console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${err.message}`);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
274
|
function extractIncludesFromSource(code) {
|
|
326
275
|
const results = [];
|
|
327
276
|
const regex = /\binclude\s*\(/g;
|
|
@@ -1984,7 +1933,7 @@ import { resolve as resolve2 } from "node:path";
|
|
|
1984
1933
|
// package.json
|
|
1985
1934
|
var package_default = {
|
|
1986
1935
|
name: "@rangojs/router",
|
|
1987
|
-
version: "0.0.0-experimental.
|
|
1936
|
+
version: "0.0.0-experimental.14",
|
|
1988
1937
|
type: "module",
|
|
1989
1938
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1990
1939
|
author: "Ivo Todorov",
|
|
@@ -2717,7 +2666,6 @@ Set an explicit \`id\` on createRouter() or check the call site.`
|
|
|
2717
2666
|
});
|
|
2718
2667
|
}
|
|
2719
2668
|
if (opts?.staticRouteTypesGeneration !== false) {
|
|
2720
|
-
writePerModuleRouteTypes(projectRoot, scanFilter);
|
|
2721
2669
|
cachedRouterFiles = findRouterFiles(projectRoot, scanFilter);
|
|
2722
2670
|
writeCombinedRouteTypes(projectRoot, cachedRouterFiles, { preserveIfLarger: true });
|
|
2723
2671
|
}
|
|
@@ -2870,9 +2818,6 @@ ${err.stack}`
|
|
|
2870
2818
|
const hasUrls = source.includes("urls(");
|
|
2871
2819
|
const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
2872
2820
|
if (!hasUrls && !hasCreateRouter) return;
|
|
2873
|
-
if (hasUrls) {
|
|
2874
|
-
writePerModuleRouteTypesForFile(filePath);
|
|
2875
|
-
}
|
|
2876
2821
|
if (hasCreateRouter) {
|
|
2877
2822
|
cachedRouterFiles = void 0;
|
|
2878
2823
|
}
|
package/package.json
CHANGED
package/skills/rango/SKILL.md
CHANGED
|
@@ -30,6 +30,7 @@ Django-inspired RSC router with composable URL patterns, type-safe href, and ser
|
|
|
30
30
|
| `/response-routes` | JSON/text/HTML/XML/stream endpoints with `path.json()`, `path.text()` |
|
|
31
31
|
| `/mime-routes` | Content negotiation — same URL, different response types via Accept header |
|
|
32
32
|
| `/fonts` | Load web fonts with preload hints |
|
|
33
|
+
| `/testing` | Unit test route trees with `buildRouteTree()` |
|
|
33
34
|
|
|
34
35
|
## Quick Start
|
|
35
36
|
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
```
|
|
@@ -45,22 +45,35 @@ export const urlpatterns = urls(({ path, layout }) => [
|
|
|
45
45
|
|
|
46
46
|
## Type-Safe href()
|
|
47
47
|
|
|
48
|
-
### Server: ctx.reverse
|
|
48
|
+
### Server: ctx.reverse with global route names
|
|
49
49
|
|
|
50
|
-
In route handlers, use `
|
|
50
|
+
In route handlers, use `ctx.reverse()` with the global dotted route names
|
|
51
|
+
from `router.named-routes.gen.ts`:
|
|
51
52
|
|
|
52
53
|
```typescript
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
path("/product/:slug", (ctx) => {
|
|
56
|
-
const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
|
|
54
|
+
import type { Handler } from "@rangojs/router";
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
reverse("
|
|
60
|
-
reverse("
|
|
56
|
+
export const ProductHandler: Handler<"shop.product"> = (ctx) => {
|
|
57
|
+
ctx.reverse("shop.cart"); // Global name
|
|
58
|
+
ctx.reverse("shop.product", { slug: "widget" }); // With params
|
|
59
|
+
ctx.reverse("blog.post", { postId: "1" }); // Cross-module
|
|
61
60
|
|
|
62
61
|
return <ProductPage slug={ctx.params.slug} />;
|
|
63
|
-
}
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For opt-in per-module isolation (after running `npx rango generate urls/shop.tsx`),
|
|
66
|
+
use `scopedReverse()` with a local route map:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { scopedReverse } from "@rangojs/router";
|
|
70
|
+
import type { routes } from "./shop.gen.js";
|
|
71
|
+
|
|
72
|
+
export const ProductHandler: Handler<"product", routes> = (ctx) => {
|
|
73
|
+
const reverse = scopedReverse<routes>(ctx.reverse);
|
|
74
|
+
reverse("cart"); // Local name
|
|
75
|
+
reverse("product", { slug: "widget" }); // Local with params
|
|
76
|
+
};
|
|
64
77
|
```
|
|
65
78
|
|
|
66
79
|
### Client: href + useHref
|
|
@@ -192,13 +205,14 @@ export const SearchPage: Handler<"search"> = (ctx) => {
|
|
|
192
205
|
```
|
|
193
206
|
|
|
194
207
|
This avoids circular references because `Handler` defaults to `GeneratedRouteMap`
|
|
195
|
-
(
|
|
208
|
+
(from `router.named-routes.gen.ts`) instead of `RegisteredRoutes` (which depends on `router.tsx`).
|
|
196
209
|
|
|
197
|
-
You can also pass an explicit route map
|
|
210
|
+
You can also pass an explicit route map for per-module isolation (opt-in,
|
|
211
|
+
after running `npx rango generate`):
|
|
198
212
|
|
|
199
213
|
```typescript
|
|
200
214
|
import type { Handler } from "@rangojs/router";
|
|
201
|
-
import type { routes } from "
|
|
215
|
+
import type { routes } from "./urls.gen.js";
|
|
202
216
|
|
|
203
217
|
export const SearchPage: Handler<"search", routes> = (ctx) => { ... };
|
|
204
218
|
```
|
|
@@ -240,16 +254,15 @@ type P = RouteParams<"blogPost", routes>;
|
|
|
240
254
|
|
|
241
255
|
### Generated route types
|
|
242
256
|
|
|
243
|
-
In the generated
|
|
244
|
-
|
|
257
|
+
In the generated `router.named-routes.gen.ts`, routes with search schemas
|
|
258
|
+
use `{ path, search }` objects:
|
|
245
259
|
|
|
246
260
|
```typescript
|
|
247
|
-
//
|
|
248
|
-
export const
|
|
249
|
-
search: { path: "/search", search: { q: "string", page: "number?", sort: "string?" } },
|
|
250
|
-
home: "/", // No search schema -> plain string
|
|
261
|
+
// router.named-routes.gen.ts (auto-generated)
|
|
262
|
+
export const NamedRoutes = {
|
|
263
|
+
"search.index": { path: "/search", search: { q: "string", page: "number?", sort: "string?" } },
|
|
264
|
+
"home.index": "/", // No search schema -> plain string
|
|
251
265
|
} as const;
|
|
252
|
-
export type routes = typeof routes;
|
|
253
266
|
```
|
|
254
267
|
|
|
255
268
|
## Loader Type Safety
|
|
@@ -474,8 +487,7 @@ export const ProductLoader = createLoader("product", async (ctx) => {
|
|
|
474
487
|
|
|
475
488
|
// 5. Server: ctx.reverse for named routes
|
|
476
489
|
path("/product/:slug", (ctx) => {
|
|
477
|
-
|
|
478
|
-
return <Link to={reverse("shop")}>Back to Shop</Link>;
|
|
490
|
+
return <Link to={ctx.reverse("shop")}>Back to Shop</Link>;
|
|
479
491
|
}, { name: "product" })
|
|
480
492
|
|
|
481
493
|
// 6. Client: useHref for mounted paths, href for absolute
|
package/src/index.rsc.ts
CHANGED
|
@@ -67,13 +67,22 @@ export type {
|
|
|
67
67
|
NotFoundInfo,
|
|
68
68
|
NotFoundBoundaryFallbackProps,
|
|
69
69
|
NotFoundBoundaryHandler,
|
|
70
|
+
// Error handling callback types
|
|
71
|
+
ErrorPhase,
|
|
72
|
+
OnErrorContext,
|
|
73
|
+
OnErrorCallback,
|
|
70
74
|
} from "./types.js";
|
|
71
75
|
|
|
72
76
|
// Router options type (server-only, so import directly)
|
|
73
77
|
export type { RSCRouterOptions } from "./router.js";
|
|
74
78
|
|
|
75
79
|
// Server-side createLoader and redirect
|
|
76
|
-
export {
|
|
80
|
+
export {
|
|
81
|
+
createLoader,
|
|
82
|
+
redirect,
|
|
83
|
+
type RouteHelpers,
|
|
84
|
+
type RouteHandlers,
|
|
85
|
+
} from "./route-definition.js";
|
|
77
86
|
|
|
78
87
|
// Handle API
|
|
79
88
|
export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
@@ -167,3 +176,6 @@ export {
|
|
|
167
176
|
type LocationStateDefinition,
|
|
168
177
|
type LocationStateEntry,
|
|
169
178
|
} from "./browser/react/location-state-shared.js";
|
|
179
|
+
|
|
180
|
+
// Path-based response type lookup from RegisteredRoutes
|
|
181
|
+
export type { PathResponse } from "./href-client.js";
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,10 @@ export type {
|
|
|
68
68
|
NotFoundInfo,
|
|
69
69
|
NotFoundBoundaryFallbackProps,
|
|
70
70
|
NotFoundBoundaryHandler,
|
|
71
|
+
// Error handling callback types
|
|
72
|
+
ErrorPhase,
|
|
73
|
+
OnErrorContext,
|
|
74
|
+
OnErrorCallback,
|
|
71
75
|
} from "./types.js";
|
|
72
76
|
|
|
73
77
|
// Search params schema types
|
|
@@ -77,6 +81,9 @@ export type { SearchSchema, SearchSchemaValue, ResolveSearchSchema, RouteSearchP
|
|
|
77
81
|
// Use this when defining loaders that will be imported by client components
|
|
78
82
|
export { createLoader } from "./loader.js";
|
|
79
83
|
|
|
84
|
+
// Route definition types (safe to import anywhere)
|
|
85
|
+
export type { RouteHelpers, RouteHandlers } from "./route-definition.js";
|
|
86
|
+
|
|
80
87
|
// Response route types (usable in both server and client contexts)
|
|
81
88
|
export type {
|
|
82
89
|
ResponseHandler,
|
package/src/route-definition.ts
CHANGED
|
@@ -517,6 +517,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
517
517
|
*/
|
|
518
518
|
const hasRoutesInItem = (item: AllUseItems): boolean => {
|
|
519
519
|
if (item.type === "route") return true;
|
|
520
|
+
// Lazy includes contain deferred routes — treat them as having routes
|
|
521
|
+
// to prevent the parent layout from being misclassified as orphan,
|
|
522
|
+
// which would clear its parent pointer and break the middleware chain.
|
|
523
|
+
if (item.type === "include") return true;
|
|
520
524
|
if (item.type === "cache" && item.uses) {
|
|
521
525
|
return item.uses.some((child) => hasRoutesInItem(child));
|
|
522
526
|
}
|
|
@@ -823,6 +827,11 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
|
|
|
823
827
|
invariant(false, "No parent entry available for parallel()");
|
|
824
828
|
}
|
|
825
829
|
|
|
830
|
+
invariant(
|
|
831
|
+
ctx.parent.type !== "parallel",
|
|
832
|
+
"parallel() cannot be nested inside another parallel()"
|
|
833
|
+
);
|
|
834
|
+
|
|
826
835
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
|
|
827
836
|
|
|
828
837
|
// Unwrap any static handler definitions in parallel slots
|
|
@@ -888,6 +897,11 @@ const intercept: RouteHelpers<any, any>["intercept"] = (
|
|
|
888
897
|
invariant(false, "No parent entry available for intercept()");
|
|
889
898
|
}
|
|
890
899
|
|
|
900
|
+
invariant(
|
|
901
|
+
ctx.parent.type !== "parallel",
|
|
902
|
+
"intercept() cannot be used inside parallel()"
|
|
903
|
+
);
|
|
904
|
+
|
|
891
905
|
const namespace = `${ctx.namespace}.$${store.getNextIndex("intercept")}.${slotName}`;
|
|
892
906
|
|
|
893
907
|
// Apply name prefix to routeName (from include())
|
|
@@ -1080,6 +1094,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1080
1094
|
const store = getContext();
|
|
1081
1095
|
const ctx = store.getStore();
|
|
1082
1096
|
if (!ctx) throw new Error("layout() must be called inside map()");
|
|
1097
|
+
|
|
1098
|
+
invariant(
|
|
1099
|
+
!ctx.parent || ctx.parent.type !== "parallel",
|
|
1100
|
+
"layout() cannot be used inside parallel()"
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1083
1103
|
const isRoot = !ctx.parent || ctx.parent === null;
|
|
1084
1104
|
const nextIndex = isRoot ? "$root" : store.getNextIndex("layout");
|
|
1085
1105
|
const namespace = `${ctx.namespace}.${nextIndex}`;
|
|
@@ -1127,6 +1147,17 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
|
|
|
1127
1147
|
result.some((item) => hasRoutesInItem(item));
|
|
1128
1148
|
|
|
1129
1149
|
if (!hasRoutes) {
|
|
1150
|
+
// Orphan layouts must not contain other layouts as children.
|
|
1151
|
+
// If we're here, all child layouts are also orphan (if any had routes,
|
|
1152
|
+
// hasRoutesInItem would have returned true). Nested orphan chains are
|
|
1153
|
+
// confusing — use sibling orphan layouts instead.
|
|
1154
|
+
if (result) {
|
|
1155
|
+
invariant(
|
|
1156
|
+
!result.some((item) => item?.type === "layout"),
|
|
1157
|
+
`orphan layout cannot contain other layouts as children [${namespace}]`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1130
1161
|
const parent = ctx.parent;
|
|
1131
1162
|
|
|
1132
1163
|
// Allow orphan layouts at root level if they're part of map() builder result
|
|
@@ -104,8 +104,8 @@ import type { ResolvedSegment } from "../../types.js";
|
|
|
104
104
|
import { getRequestContext } from "../../server/request-context.js";
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
106
|
import { getRouterContext } from "../router-context.js";
|
|
107
|
-
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
108
107
|
import { debugLog, debugWarn } from "../logging.js";
|
|
108
|
+
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Creates cache store middleware
|
package/src/server.ts
CHANGED
|
@@ -1,95 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @rangojs/router/server — Internal subpath
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* This module is NOT user-facing. Import from "@rangojs/router" instead.
|
|
5
|
+
*
|
|
6
|
+
* Exports here are consumed by the Vite plugin (discovery, manifest injection,
|
|
7
|
+
* virtual modules) and the RSC handler internals. They are not part of the
|
|
8
|
+
* public API and may change without notice.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
//
|
|
9
|
-
export {
|
|
10
|
-
createLoader,
|
|
11
|
-
redirect,
|
|
12
|
-
type RouteHelpers,
|
|
13
|
-
type RouteHandlers,
|
|
14
|
-
} from "./route-definition.js";
|
|
15
|
-
|
|
16
|
-
// Django-style URL patterns (server-only)
|
|
11
|
+
// Router registry (used by Vite plugin for build-time discovery)
|
|
17
12
|
export {
|
|
18
|
-
urls,
|
|
19
|
-
RESPONSE_TYPE,
|
|
20
|
-
type PathHelpers,
|
|
21
|
-
type PathOptions,
|
|
22
|
-
type UrlPatterns,
|
|
23
|
-
type IncludeOptions,
|
|
24
|
-
type ResponseHandler,
|
|
25
|
-
type ResponseHandlerContext,
|
|
26
|
-
type JsonResponseHandler,
|
|
27
|
-
type TextResponseHandler,
|
|
28
|
-
type JsonValue,
|
|
29
|
-
type ResponsePathFn,
|
|
30
|
-
type JsonResponsePathFn,
|
|
31
|
-
type TextResponsePathFn,
|
|
32
|
-
type RouteResponse,
|
|
33
|
-
type ResponseError,
|
|
34
|
-
type ResponseEnvelope,
|
|
35
|
-
} from "./urls.js";
|
|
36
|
-
|
|
37
|
-
// Re-export IncludeItem from route-types
|
|
38
|
-
export type { IncludeItem } from "./route-types.js";
|
|
39
|
-
|
|
40
|
-
// Core router (server-only)
|
|
41
|
-
export {
|
|
42
|
-
createRouter,
|
|
43
13
|
RSC_ROUTER_BRAND,
|
|
44
14
|
RouterRegistry,
|
|
45
|
-
type RSCRouter,
|
|
46
|
-
type RSCRouterOptions,
|
|
47
|
-
type RootLayoutProps,
|
|
48
15
|
} from "./router.js";
|
|
49
16
|
|
|
50
|
-
//
|
|
51
|
-
export {
|
|
52
|
-
createReverse,
|
|
53
|
-
type ReverseFunction,
|
|
54
|
-
type PrefixedRoutes,
|
|
55
|
-
type PrefixRoutePatterns,
|
|
56
|
-
type ParamsFor,
|
|
57
|
-
type SanitizePrefix,
|
|
58
|
-
type MergeRoutes,
|
|
59
|
-
} from "./reverse.js";
|
|
60
|
-
|
|
61
|
-
// Segment system (server-only)
|
|
62
|
-
export { renderSegments } from "./segment-system.js";
|
|
63
|
-
|
|
64
|
-
// Performance tracking (server-only)
|
|
65
|
-
export { track } from "./server/context.js";
|
|
66
|
-
|
|
67
|
-
// Handle API (works in both server and client contexts)
|
|
68
|
-
export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
69
|
-
|
|
70
|
-
// Pre-render handler API
|
|
71
|
-
export {
|
|
72
|
-
Prerender,
|
|
73
|
-
isPrerenderHandler,
|
|
74
|
-
type PrerenderHandlerDefinition,
|
|
75
|
-
type PrerenderOptions,
|
|
76
|
-
type BuildContext,
|
|
77
|
-
} from "./prerender.js";
|
|
78
|
-
|
|
79
|
-
// Static handler API
|
|
80
|
-
export {
|
|
81
|
-
Static,
|
|
82
|
-
isStaticHandler,
|
|
83
|
-
type StaticHandlerDefinition,
|
|
84
|
-
} from "./static-handler.js";
|
|
85
|
-
|
|
86
|
-
// Built-in handles
|
|
87
|
-
export { Meta } from "./handles/meta.js";
|
|
88
|
-
|
|
89
|
-
// Loader registry (for GET-based loader fetching)
|
|
90
|
-
export { registerLoaderById, setLoaderImports } from "./server/loader-registry.js";
|
|
91
|
-
|
|
92
|
-
// Route map builder (for build-time manifest registration)
|
|
17
|
+
// Route map builder (Vite plugin injects these via virtual modules)
|
|
93
18
|
export {
|
|
94
19
|
registerRouteMap,
|
|
95
20
|
setCachedManifest,
|
|
@@ -103,87 +28,17 @@ export {
|
|
|
103
28
|
ensureRouterManifest,
|
|
104
29
|
} from "./route-map-builder.js";
|
|
105
30
|
|
|
106
|
-
//
|
|
31
|
+
// Loader registry (Vite plugin registers lazy loader imports)
|
|
32
|
+
export { registerLoaderById, setLoaderImports } from "./server/loader-registry.js";
|
|
33
|
+
|
|
34
|
+
// Request context creation (used by RSC handler, not user-facing)
|
|
107
35
|
export {
|
|
108
|
-
getRequestContext,
|
|
109
|
-
requireRequestContext,
|
|
110
36
|
createRequestContext,
|
|
111
|
-
type RequestContext,
|
|
112
37
|
type CreateRequestContextOptions,
|
|
113
38
|
} from "./server/request-context.js";
|
|
114
39
|
|
|
115
|
-
//
|
|
116
|
-
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
117
|
-
|
|
118
|
-
// Middleware context types (Middleware type is exported from types.ts)
|
|
119
|
-
export type {
|
|
120
|
-
MiddlewareContext,
|
|
121
|
-
CookieOptions,
|
|
122
|
-
} from "./router/middleware.js";
|
|
123
|
-
|
|
124
|
-
// Error classes and utilities
|
|
125
|
-
export {
|
|
126
|
-
RouteNotFoundError,
|
|
127
|
-
DataNotFoundError,
|
|
128
|
-
notFound,
|
|
129
|
-
MiddlewareError,
|
|
130
|
-
HandlerError,
|
|
131
|
-
BuildError,
|
|
132
|
-
InvalidHandlerError,
|
|
133
|
-
sanitizeError,
|
|
134
|
-
RouterError,
|
|
135
|
-
} from "./errors.js";
|
|
136
|
-
|
|
137
|
-
// Component utilities
|
|
40
|
+
// Component utilities (used internally for server/client boundary checks)
|
|
138
41
|
export {
|
|
139
42
|
isClientComponent,
|
|
140
43
|
assertClientComponent,
|
|
141
44
|
} from "./component-utils.js";
|
|
142
|
-
|
|
143
|
-
// Debug utilities for route matching (development only)
|
|
144
|
-
export {
|
|
145
|
-
enableMatchDebug,
|
|
146
|
-
getMatchDebugStats,
|
|
147
|
-
} from "./router/pattern-matching.js";
|
|
148
|
-
|
|
149
|
-
// Types (re-exported for convenience - user-facing only)
|
|
150
|
-
export type {
|
|
151
|
-
// Configuration types
|
|
152
|
-
RouterEnv,
|
|
153
|
-
DefaultEnv,
|
|
154
|
-
RouteDefinition,
|
|
155
|
-
RouteConfig,
|
|
156
|
-
RouteDefinitionOptions,
|
|
157
|
-
TrailingSlashMode,
|
|
158
|
-
// Handler types
|
|
159
|
-
Handler, // Supports params object, path pattern, or route name
|
|
160
|
-
HandlerContext,
|
|
161
|
-
ExtractParams,
|
|
162
|
-
GenericParams,
|
|
163
|
-
// Middleware types (also exported from router/middleware.js above)
|
|
164
|
-
Middleware, // Supports env type and optional route name for params
|
|
165
|
-
// Revalidation types
|
|
166
|
-
RevalidateParams,
|
|
167
|
-
Revalidate,
|
|
168
|
-
RouteKeys,
|
|
169
|
-
// Loader types
|
|
170
|
-
LoaderDefinition,
|
|
171
|
-
LoaderFn,
|
|
172
|
-
LoaderContext,
|
|
173
|
-
// Error boundary types
|
|
174
|
-
ErrorInfo,
|
|
175
|
-
ErrorBoundaryFallbackProps,
|
|
176
|
-
ErrorBoundaryHandler,
|
|
177
|
-
ClientErrorBoundaryFallbackProps,
|
|
178
|
-
// NotFound boundary types
|
|
179
|
-
NotFoundInfo,
|
|
180
|
-
NotFoundBoundaryFallbackProps,
|
|
181
|
-
NotFoundBoundaryHandler,
|
|
182
|
-
// Error handling callback types
|
|
183
|
-
ErrorPhase,
|
|
184
|
-
OnErrorContext,
|
|
185
|
-
OnErrorCallback,
|
|
186
|
-
} from "./types.js";
|
|
187
|
-
|
|
188
|
-
// Path-based response type lookup from RegisteredRoutes
|
|
189
|
-
export type { PathResponse } from "./href-client.js";
|
package/src/urls.ts
CHANGED
|
@@ -813,6 +813,24 @@ function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
813
813
|
const ctx = store.getStore();
|
|
814
814
|
if (!ctx) throw new Error("path() must be called inside urls()");
|
|
815
815
|
|
|
816
|
+
invariant(
|
|
817
|
+
!ctx.parent || ctx.parent.type !== "parallel",
|
|
818
|
+
"path() cannot be used inside parallel()"
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// Walk the parent chain to prevent path() nested under another path(),
|
|
822
|
+
// even when separated by intermediate layouts (e.g. path(layout(path())))
|
|
823
|
+
{
|
|
824
|
+
let ancestor = ctx.parent;
|
|
825
|
+
while (ancestor) {
|
|
826
|
+
invariant(
|
|
827
|
+
ancestor.type !== "route",
|
|
828
|
+
"path() cannot be nested inside another path()"
|
|
829
|
+
);
|
|
830
|
+
ancestor = ancestor.parent;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
816
834
|
// Determine options and use based on argument types
|
|
817
835
|
let options: PathOptions | undefined;
|
|
818
836
|
let use: (() => RouteUseItem[]) | undefined;
|
package/src/vite/index.ts
CHANGED
|
@@ -7,8 +7,6 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";
|
|
8
8
|
import {
|
|
9
9
|
generateRouteTypesSource,
|
|
10
|
-
writePerModuleRouteTypes,
|
|
11
|
-
writePerModuleRouteTypesForFile,
|
|
12
10
|
writeCombinedRouteTypes,
|
|
13
11
|
findRouterFiles,
|
|
14
12
|
createScanFilter,
|
|
@@ -111,9 +109,8 @@ interface RangoBaseOptions {
|
|
|
111
109
|
banner?: boolean;
|
|
112
110
|
|
|
113
111
|
/**
|
|
114
|
-
* Generate
|
|
115
|
-
*
|
|
116
|
-
* Handler<"name", routes> and href() without executing router code.
|
|
112
|
+
* Generate named-routes.gen.ts by parsing url modules at startup.
|
|
113
|
+
* Provides type-safe Handler<"name"> and href() without executing router code.
|
|
117
114
|
* Set to `false` to disable (run `npx rango extract-names` manually instead).
|
|
118
115
|
* @default true
|
|
119
116
|
*/
|
|
@@ -964,15 +961,13 @@ function createRouterDiscoveryPlugin(
|
|
|
964
961
|
exclude: opts.exclude,
|
|
965
962
|
});
|
|
966
963
|
}
|
|
967
|
-
// Generate
|
|
968
|
-
// Runs before the dev server starts so
|
|
964
|
+
// Generate combined named-routes.gen.ts from static source parsing.
|
|
965
|
+
// Runs before the dev server starts so the gen file exists immediately for IDE.
|
|
969
966
|
// In build mode, the runtime discovery in buildStart produces the definitive
|
|
970
|
-
// named-routes.gen.ts (including dynamically generated routes).
|
|
971
|
-
//
|
|
972
|
-
//
|
|
973
|
-
// previously generated complete file with a partial one.
|
|
967
|
+
// named-routes.gen.ts (including dynamically generated routes).
|
|
968
|
+
// preserveIfLarger prevents overwriting a previously generated complete
|
|
969
|
+
// file with a partial one.
|
|
974
970
|
if (opts?.staticRouteTypesGeneration !== false) {
|
|
975
|
-
writePerModuleRouteTypes(projectRoot, scanFilter);
|
|
976
971
|
cachedRouterFiles = findRouterFiles(projectRoot, scanFilter);
|
|
977
972
|
writeCombinedRouteTypes(projectRoot, cachedRouterFiles, { preserveIfLarger: true });
|
|
978
973
|
}
|
|
@@ -1173,8 +1168,8 @@ function createRouterDiscoveryPlugin(
|
|
|
1173
1168
|
res.end("No prerender match");
|
|
1174
1169
|
});
|
|
1175
1170
|
|
|
1176
|
-
// Watch url module and router files for changes and regenerate
|
|
1177
|
-
// Process files containing urls(
|
|
1171
|
+
// Watch url module and router files for changes and regenerate named-routes.gen.ts.
|
|
1172
|
+
// Process files containing urls( or createRouter( to update the combined route map.
|
|
1178
1173
|
if (opts?.staticRouteTypesGeneration !== false) {
|
|
1179
1174
|
server.watcher.on("change", (filePath) => {
|
|
1180
1175
|
if (filePath.endsWith(".gen.ts")) return;
|
|
@@ -1188,9 +1183,6 @@ function createRouterDiscoveryPlugin(
|
|
|
1188
1183
|
const hasUrls = source.includes("urls(");
|
|
1189
1184
|
const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
1190
1185
|
if (!hasUrls && !hasCreateRouter) return;
|
|
1191
|
-
if (hasUrls) {
|
|
1192
|
-
writePerModuleRouteTypesForFile(filePath);
|
|
1193
|
-
}
|
|
1194
1186
|
// Invalidate cache when a router file changes (new router added/removed)
|
|
1195
1187
|
if (hasCreateRouter) {
|
|
1196
1188
|
cachedRouterFiles = undefined;
|