@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.
@@ -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.13",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.13",
3
+ "version": "0.0.0-experimental.14",
4
4
  "type": "module",
5
5
  "description": "Django-inspired RSC router with composable URL patterns",
6
6
  "author": "Ivo Todorov",
@@ -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 + scopedReverse
48
+ ### Server: ctx.reverse with global route names
49
49
 
50
- In route handlers, use `scopedReverse()` for local route name autocomplete:
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 { scopedReverse } from "@rangojs/router";
54
-
55
- path("/product/:slug", (ctx) => {
56
- const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
54
+ import type { Handler } from "@rangojs/router";
57
55
 
58
- reverse("cart"); // Type-safe local name
59
- reverse("product", { slug: "widget" }); // Type-safe with params
60
- reverse("blog.post"); // Absolute names always allowed
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
- }, { name: "product" })
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
- (standalone gen file) instead of `RegisteredRoutes` (which depends on `router.tsx`).
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 if needed:
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 "../urls.gen.js";
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 route types (`urls.gen.ts` and `GeneratedRouteMap`), routes with
244
- search schemas use `{ path, search }` objects:
257
+ In the generated `router.named-routes.gen.ts`, routes with search schemas
258
+ use `{ path, search }` objects:
245
259
 
246
260
  ```typescript
247
- // urls.gen.ts (auto-generated by `npx rango extract-names`)
248
- export const routes = {
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
- const reverse = scopedReverse<typeof urlpatterns>(ctx.reverse);
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 { createLoader, redirect } from "./route-definition.js";
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,
@@ -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
- * rsc-router/server
2
+ * @rangojs/router/server — Internal subpath
3
3
  *
4
- * Server-only exports for route definition and building
5
- * These should only be imported in server-side handler files
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
- // Route definition helpers (server-only)
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
- // Type-safe reverse utilities (Django-style URL reversal)
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
- // Request context (for accessing request data in server components/actions)
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
- // Meta types
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 static route type files (.gen.ts) by parsing url modules at startup.
115
- * Creates per-module route maps and per-router named-routes.gen.ts for type-safe
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 per-module route types from static source parsing.
968
- // Runs before the dev server starts so .gen.ts files exist immediately for IDE.
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). However, we
971
- // still need to write initial gen files here so discovery can import router
972
- // modules that reference them. preserveIfLarger prevents overwriting a
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 route types.
1177
- // Process files containing urls( (per-module types) or createRouter( (per-router types).
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;