@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
package/README.md CHANGED
@@ -21,6 +21,12 @@ Named-route RSC router with structural composability and type-safe partial rende
21
21
  - **Trailing slash control** — Per-route canonical URLs with `"never"`, `"always"`, or `"ignore"`
22
22
  - **CLI codegen** — `rango generate` for route type generation
23
23
 
24
+ ## Design Docs
25
+
26
+ - [Execution model](./docs/internal/execution-model.md)
27
+ - [Semantic change checklist](./docs/internal/semantic-change-checklist.md)
28
+ - [Stability roadmap](./docs/internal/stability-roadmap.md)
29
+
24
30
  ## Installation
25
31
 
26
32
  ```bash
@@ -417,10 +423,10 @@ import { useLoader } from "@rangojs/router/client";
417
423
  import { BlogSidebarLoader } from "./loaders/blog";
418
424
 
419
425
  function BlogSidebar() {
420
- const { posts } = useLoader(BlogSidebarLoader);
426
+ const { data } = useLoader(BlogSidebarLoader);
421
427
  return (
422
428
  <ul>
423
- {posts.map((p) => (
429
+ {data.posts.map((p) => (
424
430
  <li key={p.slug}>{p.title}</li>
425
431
  ))}
426
432
  </ul>
@@ -482,7 +488,7 @@ function Nav() {
482
488
  return (
483
489
  <nav>
484
490
  <Link to={href("/")}>Home</Link>
485
- <Link to={href("/blog")} prefetch="intent">
491
+ <Link to={href("/blog")} prefetch="hybrid">
486
492
  Blog
487
493
  </Link>
488
494
  <Link to={href("/about")}>About</Link>
@@ -493,20 +499,21 @@ function Nav() {
493
499
 
494
500
  `href()` validates that the path matches a registered route pattern at compile time (e.g. `/blog/my-post` matches `/blog/:slug`).
495
501
 
496
- ### Navigation Hook
502
+ ### Navigation Hooks
497
503
 
498
504
  ```tsx
499
505
  "use client";
500
- import { useNavigation } from "@rangojs/router/client";
506
+ import { useNavigation, useRouter } from "@rangojs/router/client";
501
507
 
502
508
  function SearchForm() {
503
- const { navigate, isPending } = useNavigation();
509
+ const router = useRouter();
510
+ const nav = useNavigation();
504
511
 
505
512
  function handleSubmit(query: string) {
506
- navigate(`/search?q=${encodeURIComponent(query)}`);
513
+ router.push(`/search?q=${encodeURIComponent(query)}`);
507
514
  }
508
515
 
509
- return <form onSubmit={...}>{isPending && <Spinner />}</form>;
516
+ return <form onSubmit={...}>{nav.state !== "idle" && <Spinner />}</form>;
510
517
  }
511
518
  ```
512
519
 
@@ -554,6 +561,20 @@ export const urlpatterns = urls(({ path, include }) => [
554
561
 
555
562
  Included route names are prefixed with the include name: `reverse("api.health")`, `reverse("api.products")`.
556
563
 
564
+ ### Include name scoping
565
+
566
+ The `name` option controls how child route names appear globally:
567
+
568
+ | Form | Child names | Generated types | Reverse resolution |
569
+ | ---------------------------------- | ------------------- | ---------------------- | -------------------------------------------------------------------- |
570
+ | `include("/x", p, { name: "ns" })` | `ns.child` | Exported as `ns.child` | `reverse("ns.child")` globally, `reverse(".child")` inside |
571
+ | `include("/x", p, { name: "" })` | `child` (flattened) | Exported as-is | `reverse("child")` globally, `reverse(".child")` inside (root-scope) |
572
+ | `include("/x", p)` | Private scope | Not exported | `reverse(".child")` inside only |
573
+
574
+ Without a `name`, included routes are local to the mounted module. They still match requests and render normally, but their names are hidden from the generated route map and cannot be reversed globally. Use `{ name: "" }` to merge children into the parent namespace without adding a prefix.
575
+
576
+ **`{ name: "" }` is flattening, not isolation.** Flattened routes behave as if defined inline at the include site — dot-local reverse (`.name`) can reach any sibling route at root scope, including routes from other `{ name: "" }` mounts. If you need module-level isolation, omit the `name` option or use a namespace.
577
+
557
578
  ## Middleware
558
579
 
559
580
  ```tsx
@@ -680,6 +701,23 @@ export const ProductPage = Prerender(
680
701
 
681
702
  With `passthrough: true`, known params are served from the build-time cache and unknown params fall through to live rendering.
682
703
 
704
+ Handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler at runtime:
705
+
706
+ ```tsx
707
+ export const ProductPage = Prerender(
708
+ async () => {
709
+ const all = await db.getAllProducts();
710
+ return all.map((p) => ({ id: p.id }));
711
+ },
712
+ async (ctx) => {
713
+ const product = await db.getProduct(ctx.params.id);
714
+ if (!product.published) return ctx.passthrough();
715
+ return <Product data={product} />;
716
+ },
717
+ { passthrough: true },
718
+ );
719
+ ```
720
+
683
721
  ## Theme
684
722
 
685
723
  ### Router Configuration
package/dist/bin/rango.js CHANGED
@@ -119,6 +119,21 @@ var init_ast_route_extraction = __esm({
119
119
  }
120
120
  });
121
121
 
122
+ // src/route-name.ts
123
+ function isAutoGeneratedRouteName(name) {
124
+ return name.split(".").some((segment) => {
125
+ return segment.startsWith(AUTO_GENERATED_ROUTE_PREFIX) || segment.startsWith(INTERNAL_INCLUDE_SCOPE_PREFIX);
126
+ });
127
+ }
128
+ var AUTO_GENERATED_ROUTE_PREFIX, INTERNAL_INCLUDE_SCOPE_PREFIX;
129
+ var init_route_name = __esm({
130
+ "src/route-name.ts"() {
131
+ "use strict";
132
+ AUTO_GENERATED_ROUTE_PREFIX = "$path_";
133
+ INTERNAL_INCLUDE_SCOPE_PREFIX = "$prefix_";
134
+ }
135
+ });
136
+
122
137
  // src/build/route-types/codegen.ts
123
138
  function generatePerModuleTypesSource(routes) {
124
139
  const valid = routes.filter(({ name }) => {
@@ -153,13 +168,16 @@ export type routes = typeof routes;
153
168
  `;
154
169
  }
155
170
  function generateRouteTypesSource(routeManifest, searchSchemas) {
156
- const entries = Object.entries(routeManifest).sort(
157
- ([a], [b]) => a.localeCompare(b)
158
- );
171
+ const entries = Object.entries(routeManifest).filter(([name]) => !isAutoGeneratedRouteName(name)).sort(([a], [b]) => a.localeCompare(b));
172
+ const filteredSearchSchemas = searchSchemas ? Object.fromEntries(
173
+ Object.entries(searchSchemas).filter(
174
+ ([name]) => !isAutoGeneratedRouteName(name)
175
+ )
176
+ ) : void 0;
159
177
  const objectBody = entries.map(([name, pattern]) => {
160
178
  const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
161
179
  const params = extractParamsFromPattern(pattern);
162
- const search = searchSchemas?.[name];
180
+ const search = filteredSearchSchemas?.[name];
163
181
  return formatRouteEntry(key, pattern, params, search);
164
182
  }).join("\n");
165
183
  return `// Auto-generated by @rangojs/router - do not edit
@@ -178,6 +196,7 @@ var init_codegen = __esm({
178
196
  "src/build/route-types/codegen.ts"() {
179
197
  "use strict";
180
198
  init_param_extraction();
199
+ init_route_name();
181
200
  }
182
201
  });
183
202
 
@@ -410,6 +429,9 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
410
429
  diagnosticsOut
411
430
  );
412
431
  }
432
+ if (namePrefix === null) {
433
+ continue;
434
+ }
413
435
  for (const [name, pattern] of Object.entries(childResult.routes)) {
414
436
  const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
415
437
  let prefixedPattern;
@@ -460,6 +482,7 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
460
482
  searchSchemas,
461
483
  diagnosticsOut
462
484
  );
485
+ visited.delete(key);
463
486
  return { routes, searchSchemas };
464
487
  }
465
488
  var init_include_resolution = __esm({
@@ -517,8 +540,16 @@ function writePerModuleRouteTypesForFile(filePath) {
517
540
  } else {
518
541
  routes = extractRoutesFromSource(source);
519
542
  }
520
- if (routes.length === 0) return;
521
543
  const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
544
+ if (routes.length === 0) {
545
+ if (varNames.length > 0 && !existsSync2(genPath)) {
546
+ writeFileSync(genPath, generatePerModuleTypesSource([]));
547
+ console.log(
548
+ `[rsc-router] Generated route types (placeholder) -> ${genPath}`
549
+ );
550
+ }
551
+ return;
552
+ }
522
553
  const genSource = generatePerModuleTypesSource(routes);
523
554
  const existing = existsSync2(genPath) ? readFileSync2(genPath, "utf-8") : null;
524
555
  if (existing !== genSource) {
@@ -546,6 +577,17 @@ var init_per_module_writer = __esm({
546
577
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync } from "node:fs";
547
578
  import { join as join2, dirname as dirname2, resolve as resolve2, basename as pathBasename } from "node:path";
548
579
  import ts5 from "typescript";
580
+ function countPublicRouteEntries(source) {
581
+ const matches = source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
582
+ let count = 0;
583
+ for (const match of matches) {
584
+ const routeName = match[1] || match[2];
585
+ if (routeName && !isAutoGeneratedRouteName(routeName.trim())) {
586
+ count++;
587
+ }
588
+ }
589
+ return count;
590
+ }
549
591
  function extractUrlsVariableFromRouter(code) {
550
592
  const sourceFile = ts5.createSourceFile(
551
593
  "router.tsx",
@@ -652,6 +694,22 @@ function detectUnresolvableIncludes(routerFilePath) {
652
694
  );
653
695
  return diagnostics;
654
696
  }
697
+ function detectUnresolvableIncludesForUrlsFile(filePath) {
698
+ const realPath = resolve2(filePath);
699
+ let source;
700
+ try {
701
+ source = readFileSync3(realPath, "utf-8");
702
+ } catch {
703
+ return [];
704
+ }
705
+ const varNames = findUrlsVariableNames(source);
706
+ if (varNames.length === 0) return [];
707
+ const diagnostics = [];
708
+ for (const varName of varNames) {
709
+ buildCombinedRouteMapWithSearch(realPath, varName, /* @__PURE__ */ new Set(), diagnostics);
710
+ }
711
+ return diagnostics;
712
+ }
655
713
  function findRouterFiles(root, filter) {
656
714
  const files = findTsFiles(root, filter);
657
715
  const result = [];
@@ -725,8 +783,10 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
725
783
  );
726
784
  if (existing !== source) {
727
785
  if (opts?.preserveIfLarger && existing) {
728
- const existingCount = (existing.match(/^\s+["a-zA-Z_$][^:]*:\s*["{]/gm) || []).length;
729
- const newCount = Object.keys(result.routes).length;
786
+ const existingCount = countPublicRouteEntries(existing);
787
+ const newCount = Object.keys(result.routes).filter(
788
+ (name) => !isAutoGeneratedRouteName(name)
789
+ ).length;
730
790
  if (existingCount > newCount) {
731
791
  continue;
732
792
  }
@@ -744,6 +804,8 @@ var init_router_processing = __esm({
744
804
  init_codegen();
745
805
  init_scan_filter();
746
806
  init_include_resolution();
807
+ init_per_module_writer();
808
+ init_route_name();
747
809
  }
748
810
  });
749
811
 
@@ -758,6 +820,7 @@ var init_generate_route_types = __esm({
758
820
  init_per_module_writer();
759
821
  init_include_resolution();
760
822
  init_router_processing();
823
+ init_per_module_writer();
761
824
  }
762
825
  });
763
826
 
@@ -1139,7 +1202,13 @@ async function discoverAndWriteRouteTypes(opts) {
1139
1202
  if (!router.urlpatterns) continue;
1140
1203
  const manifest = generateManifest(router.urlpatterns, routerMountIndex);
1141
1204
  routerMountIndex++;
1142
- const routeManifest = manifest.routeManifest;
1205
+ const rawManifest = manifest.routeManifest;
1206
+ const routeManifest = {};
1207
+ for (const [name, pattern] of Object.entries(rawManifest)) {
1208
+ if (!isAutoGeneratedRouteName(name)) {
1209
+ routeManifest[name] = pattern;
1210
+ }
1211
+ }
1143
1212
  let routeSearchSchemas = manifest.routeSearchSchemas;
1144
1213
  const sourceFile = router.__sourceFile;
1145
1214
  if (!sourceFile) {
@@ -1201,6 +1270,7 @@ var init_runtime_discovery = __esm({
1201
1270
  "src/build/runtime-discovery.ts"() {
1202
1271
  "use strict";
1203
1272
  init_generate_route_types();
1273
+ init_route_name();
1204
1274
  }
1205
1275
  });
1206
1276
 
@@ -1305,16 +1375,15 @@ function runStaticGeneration(args, mode) {
1305
1375
  process.exit(0);
1306
1376
  }
1307
1377
  const routerFiles = [];
1378
+ const urlsFiles = [];
1308
1379
  for (const filePath of files) {
1309
1380
  try {
1310
1381
  const source = readFileSync5(filePath, "utf-8");
1311
- const isRouter = /\bcreateRouter\s*[<(]/.test(source);
1312
- const isUrls = source.includes("urls(");
1313
- if (isRouter) {
1382
+ if (/\bcreateRouter\s*[<(]/.test(source)) {
1314
1383
  routerFiles.push(filePath);
1315
1384
  }
1316
- if (isUrls) {
1317
- writePerModuleRouteTypesForFile(filePath);
1385
+ if (source.includes("urls(")) {
1386
+ urlsFiles.push(filePath);
1318
1387
  }
1319
1388
  } catch (err) {
1320
1389
  console.warn(
@@ -1329,21 +1398,39 @@ function runStaticGeneration(args, mode) {
1329
1398
  allDiagnostics.push({ ...d, routerFile });
1330
1399
  }
1331
1400
  }
1332
- if (allDiagnostics.length > 0 && mode === "default") {
1401
+ const routerFileSet = new Set(routerFiles);
1402
+ for (const urlsFile of urlsFiles) {
1403
+ if (routerFileSet.has(urlsFile)) continue;
1404
+ const diagnostics = detectUnresolvableIncludesForUrlsFile(urlsFile);
1405
+ for (const d of diagnostics) {
1406
+ allDiagnostics.push({ ...d, routerFile: urlsFile });
1407
+ }
1408
+ }
1409
+ const seen = /* @__PURE__ */ new Set();
1410
+ const uniqueDiagnostics = allDiagnostics.filter((d) => {
1411
+ const key = `${d.sourceFile}:${d.pathPrefix}:${d.reason}`;
1412
+ if (seen.has(key)) return false;
1413
+ seen.add(key);
1414
+ return true;
1415
+ });
1416
+ if (uniqueDiagnostics.length > 0 && mode === "default") {
1333
1417
  console.error("\n[rango] Unresolvable includes detected:\n");
1334
- formatDiagnostics(allDiagnostics);
1418
+ formatDiagnostics(uniqueDiagnostics);
1335
1419
  console.error(
1336
1420
  "\nThe static parser cannot resolve these includes because they use factory functions or dynamic expressions.\n\nOptions:\n rango generate <path> --runtime Use Vite-based discovery (requires vite)\n rango generate <path> --static Accept partial output (missing routes above)\n"
1337
1421
  );
1338
1422
  process.exit(1);
1339
1423
  }
1340
- if (allDiagnostics.length > 0 && mode === "static") {
1424
+ if (uniqueDiagnostics.length > 0 && mode === "static") {
1341
1425
  console.warn(
1342
1426
  "\n[rango] Warning: partial output (unresolvable includes):\n"
1343
1427
  );
1344
- formatDiagnostics(allDiagnostics);
1428
+ formatDiagnostics(uniqueDiagnostics);
1345
1429
  console.warn("");
1346
1430
  }
1431
+ for (const urlsFile of urlsFiles) {
1432
+ writePerModuleRouteTypesForFile(urlsFile);
1433
+ }
1347
1434
  for (const routerFile of routerFiles) {
1348
1435
  const projectRoot = findProjectRoot(routerFile);
1349
1436
  writeCombinedRouteTypes(projectRoot, [routerFile]);
@@ -1398,8 +1485,8 @@ async function runRuntimeDiscovery(args, configFile) {
1398
1485
  }
1399
1486
  process.exit(1);
1400
1487
  }
1401
- const projectRoot = findProjectRoot(routerEntries[0]);
1402
1488
  for (const entry of routerEntries) {
1489
+ const projectRoot = findProjectRoot(entry);
1403
1490
  const result = await discoverAndWriteRouteTypes2({
1404
1491
  root: projectRoot,
1405
1492
  configFile,