@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1b930379

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