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

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -3,3 +3,7 @@
3
3
  A file-system based React Server Components router.
4
4
 
5
5
  Run `/rango` to understand the API. Detailed guides for each feature are in the `skills/` directory (e.g. `node_modules/@rangojs/router/skills/loader`, `skills/caching`, `skills/middleware`, etc.).
6
+
7
+ ## Development rules
8
+
9
+ - Always commit generated files (e.g. `*.gen.ts`) alongside the source changes that produced them.
package/README.md CHANGED
@@ -45,6 +45,30 @@ For Cloudflare Workers:
45
45
  npm install @cloudflare/vite-plugin
46
46
  ```
47
47
 
48
+ ## Import Paths
49
+
50
+ Use these import paths consistently:
51
+
52
+ - `@rangojs/router` — server/RSC router APIs, route DSL, `createRouter`, `urls`, `redirect`, `Prerender`, `Static`, shared types
53
+ - `@rangojs/router/client` — hooks and components such as `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `useAction`, `useLocationState`
54
+ - `@rangojs/router/cache` — public cache APIs such as `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware`
55
+ - `@rangojs/router/host`, `@rangojs/router/theme`, `@rangojs/router/vite` — specialized public subpaths
56
+ - `@rangojs/router/rsc`, `@rangojs/router/ssr` — advanced server-only integration subpaths for custom request/HTML pipelines
57
+
58
+ Use only subpaths that are explicitly exported from the package. Avoid deep imports such as `@rangojs/router/cache/cf`.
59
+
60
+ `@rangojs/router` is conditionally resolved. Server-only root APIs such as
61
+ `createRouter()`, `urls()`, `redirect()`, `Prerender()`, and `cookies()` rely on
62
+ the `react-server` export condition and are meant to run in router definitions,
63
+ handlers, and other RSC/server modules. Outside that environment the root entry
64
+ falls back to stub implementations that throw guidance errors.
65
+
66
+ If you hit a root-entrypoint stub error:
67
+
68
+ - hooks and components like `Link`, `Outlet`, `useLoader`, `useNavigation`, and `MetaTags` belong in `@rangojs/router/client`
69
+ - cache APIs like `CFCacheStore` and `createDocumentCacheMiddleware` belong in `@rangojs/router/cache`
70
+ - host-router APIs belong in `@rangojs/router/host`
71
+
48
72
  ## Quick Start
49
73
 
50
74
  ### Vite Config
@@ -62,26 +86,34 @@ export default defineConfig({
62
86
 
63
87
  ### Router
64
88
 
89
+ This file is a server/RSC module and should import router construction APIs from
90
+ `@rangojs/router`.
91
+
65
92
  ```tsx
66
93
  // src/router.tsx
67
- import { createRouter, urls } from "@rangojs/router";
68
- import { Document } from "./document";
94
+ import { createRouter } from "@rangojs/router";
69
95
 
70
- const blogPatterns = urls(({ path }) => [
71
- path("/", BlogIndexPage, { name: "index" }),
72
- path("/:slug", BlogPostPage, { name: "post" }),
96
+ export const router = createRouter().routes(({ path }) => [
97
+ path("/", HomePage, { name: "home" }),
98
+ path("/about", AboutPage, { name: "about" }),
73
99
  ]);
74
100
 
101
+ export const reverse = router.reverse;
102
+ // reverse("home") -> "/"
103
+ ```
104
+
105
+ For larger apps, extract route modules with `urls()` and compose with `include()`:
106
+
107
+ ```tsx
108
+ import { createRouter, urls } from "@rangojs/router";
109
+ import { blogPatterns } from "./urls/blog";
110
+
75
111
  const urlpatterns = urls(({ path, include }) => [
76
112
  path("/", HomePage, { name: "home" }),
77
113
  include("/blog", blogPatterns, { name: "blog" }),
78
114
  ]);
79
115
 
80
- export const router = createRouter({ document: Document }).routes(urlpatterns);
81
-
82
- // Export typed reverse function for URL generation by route name
83
- export const reverse = router.reverse;
84
-
116
+ export const router = createRouter().routes(urlpatterns);
85
117
  // reverse("blog.post", { slug: "hello-world" }) -> "/blog/hello-world"
86
118
  ```
87
119
 
@@ -248,7 +280,8 @@ All handler typing styles are supported, but they solve different problems:
248
280
  Example of a scoped local name inside a mounted module:
249
281
 
250
282
  ```tsx
251
- import type { Handler, ScopedRouteMap } from "@rangojs/router";
283
+ import type { Handler } from "@rangojs/router";
284
+ import type { ScopedRouteMap } from "@rangojs/router/__internal";
252
285
 
253
286
  type BlogRoutes = ScopedRouteMap<"blog">;
254
287
 
@@ -488,7 +521,7 @@ function Nav() {
488
521
  return (
489
522
  <nav>
490
523
  <Link to={href("/")}>Home</Link>
491
- <Link to={href("/blog")} prefetch="hybrid">
524
+ <Link to={href("/blog")} prefetch="adaptive">
492
525
  Blog
493
526
  </Link>
494
527
  <Link to={href("/about")}>About</Link>
@@ -683,10 +716,12 @@ export const BlogPost = Prerender(
683
716
 
684
717
  ### Passthrough for Unknown Params
685
718
 
719
+ Wrap a `Prerender` definition with `Passthrough()` to add a live handler for unknown params at runtime. The build handler runs at build time, the live handler runs at request time for params not in the prerender cache.
720
+
686
721
  ```tsx
687
- import { Prerender } from "@rangojs/router";
722
+ import { Prerender, Passthrough } from "@rangojs/router";
688
723
 
689
- export const ProductPage = Prerender(
724
+ export const ProductPageDef = Prerender(
690
725
  async () => {
691
726
  const featured = await db.getFeaturedProducts();
692
727
  return featured.map((p) => ({ id: p.id }));
@@ -695,16 +730,22 @@ export const ProductPage = Prerender(
695
730
  const product = await db.getProduct(ctx.params.id);
696
731
  return <Product data={product} />;
697
732
  },
698
- { passthrough: true },
699
733
  );
700
- ```
701
734
 
702
- With `passthrough: true`, known params are served from the build-time cache and unknown params fall through to live rendering.
735
+ // In route definition:
736
+ path(
737
+ "/products/:id",
738
+ Passthrough(ProductPageDef, async (ctx) => {
739
+ const product = await ctx.env.DB.getProduct(ctx.params.id);
740
+ return <Product data={product} />;
741
+ }),
742
+ );
743
+ ```
703
744
 
704
- Handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler at runtime:
745
+ Build handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler:
705
746
 
706
747
  ```tsx
707
- export const ProductPage = Prerender(
748
+ export const ProductPageDef = Prerender(
708
749
  async () => {
709
750
  const all = await db.getAllProducts();
710
751
  return all.map((p) => ({ id: p.id }));
@@ -714,10 +755,55 @@ export const ProductPage = Prerender(
714
755
  if (!product.published) return ctx.passthrough();
715
756
  return <Product data={product} />;
716
757
  },
717
- { passthrough: true },
718
758
  );
719
759
  ```
720
760
 
761
+ ### Build-Time Environment Bindings
762
+
763
+ Prerender handlers can access platform bindings (KV, D1, R2) at build time when `buildEnv` is configured in the Vite plugin:
764
+
765
+ ```ts
766
+ // vite.config.ts
767
+ import { rango } from "@rangojs/router/vite";
768
+
769
+ rango({ preset: "cloudflare", buildEnv: "auto" });
770
+ ```
771
+
772
+ With `buildEnv: "auto"`, the plugin calls `wrangler.getPlatformProxy()` to provide local bindings. Handlers then access `ctx.env` during build:
773
+
774
+ ```tsx
775
+ export const BlogPosts = Prerender<{ slug: string }>(
776
+ async (ctx) => {
777
+ const rows = await ctx.env.DB.prepare("SELECT slug FROM posts").all();
778
+ return rows.map((r) => ({ slug: r.slug }));
779
+ },
780
+ async (ctx) => {
781
+ const post = await ctx.env.DB.prepare("SELECT * FROM posts WHERE slug = ?")
782
+ .bind(ctx.params.slug)
783
+ .first();
784
+ return <BlogPost post={post} />;
785
+ },
786
+ );
787
+ ```
788
+
789
+ `buildEnv` also accepts a factory function or plain object:
790
+
791
+ ```ts
792
+ // Custom factory
793
+ rango({
794
+ buildEnv: async (ctx) => {
795
+ const { getPlatformProxy } = await import("wrangler");
796
+ const proxy = await getPlatformProxy();
797
+ return { env: proxy.env, dispose: proxy.dispose };
798
+ },
799
+ });
800
+
801
+ // Plain object (Node.js)
802
+ rango({ buildEnv: { DATABASE_URL: process.env.DATABASE_URL } });
803
+ ```
804
+
805
+ Build-time env applies to both production builds and dev on-demand prerender. Without `buildEnv`, accessing `ctx.env` in a Prerender handler throws with a clear error.
806
+
721
807
  ## Theme
722
808
 
723
809
  ### Router Configuration
@@ -842,16 +928,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
842
928
 
843
929
  ## Subpath Exports
844
930
 
845
- | Export | Description |
846
- | ------------------------ | --------------------------------------------------------------------------------- |
847
- | `@rangojs/router` | Core: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
848
- | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
849
- | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
850
- | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
851
- | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
852
- | `@rangojs/router/vite` | Vite plugin: `rango()` |
853
- | `@rangojs/router/server` | Server utilities |
854
- | `@rangojs/router/build` | Build utilities |
931
+ | Export | Description |
932
+ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
933
+ | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
934
+ | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
935
+ | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
936
+ | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
937
+ | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
938
+ | `@rangojs/router/vite` | Vite plugin: `rango()` |
939
+ | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
940
+ | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
941
+ | `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
942
+ | `@rangojs/router/build` | Build utilities |
943
+
944
+ The root entrypoint is not a generic client/runtime barrel. If you need hooks
945
+ or components, import from `@rangojs/router/client`; if you need cache or host
946
+ APIs, use their dedicated subpaths.
855
947
 
856
948
  ## Examples
857
949
 
package/dist/bin/rango.js CHANGED
@@ -218,7 +218,8 @@ function findTsFiles(dir, filter) {
218
218
  for (const entry of entries) {
219
219
  const fullPath = join(dir, entry.name);
220
220
  if (entry.isDirectory()) {
221
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
221
+ if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "dist" || entry.name === "build" || entry.name === "coverage")
222
+ continue;
222
223
  results.push(...findTsFiles(fullPath, filter));
223
224
  } else if ((entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx")) && !entry.name.includes(".gen.")) {
224
225
  if (filter && !filter(fullPath)) continue;
@@ -450,7 +451,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
450
451
  }
451
452
  return routeMap;
452
453
  }
453
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
454
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
454
455
  visited = visited ?? /* @__PURE__ */ new Set();
455
456
  const realPath = resolve(filePath);
456
457
  const key = variableName ? `${realPath}:${variableName}` : realPath;
@@ -466,7 +467,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
466
467
  return { routes: {}, searchSchemas: {} };
467
468
  }
468
469
  let block;
469
- if (variableName) {
470
+ if (inlineBlock) {
471
+ block = inlineBlock;
472
+ } else if (variableName) {
470
473
  const extracted = extractUrlsBlockForVariable(source, variableName);
471
474
  if (!extracted) return { routes: {}, searchSchemas: {} };
472
475
  block = extracted;
@@ -574,8 +577,20 @@ var init_per_module_writer = __esm({
574
577
  });
575
578
 
576
579
  // src/build/route-types/router-processing.ts
577
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync } from "node:fs";
578
- import { join as join2, dirname as dirname2, resolve as resolve2, basename as pathBasename } from "node:path";
580
+ import {
581
+ readFileSync as readFileSync3,
582
+ writeFileSync as writeFileSync2,
583
+ existsSync as existsSync3,
584
+ unlinkSync,
585
+ readdirSync as readdirSync2
586
+ } from "node:fs";
587
+ import {
588
+ join as join2,
589
+ dirname as dirname2,
590
+ resolve as resolve2,
591
+ sep,
592
+ basename as pathBasename
593
+ } from "node:path";
579
594
  import ts5 from "typescript";
580
595
  function countPublicRouteEntries(source) {
581
596
  const matches = source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
@@ -588,7 +603,78 @@ function countPublicRouteEntries(source) {
588
603
  }
589
604
  return count;
590
605
  }
591
- function extractUrlsVariableFromRouter(code) {
606
+ function isRoutableSourceFile(name) {
607
+ return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
608
+ }
609
+ function findRouterFilesRecursive(dir, filter, results) {
610
+ let entries;
611
+ try {
612
+ entries = readdirSync2(dir, { withFileTypes: true });
613
+ } catch (err) {
614
+ console.warn(
615
+ `[rsc-router] Failed to scan directory ${dir}: ${err.message}`
616
+ );
617
+ return;
618
+ }
619
+ const childDirs = [];
620
+ const routerFilesInDir = [];
621
+ for (const entry of entries) {
622
+ const fullPath = join2(dir, entry.name);
623
+ if (entry.isDirectory()) {
624
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
625
+ continue;
626
+ childDirs.push(fullPath);
627
+ continue;
628
+ }
629
+ if (!isRoutableSourceFile(entry.name)) continue;
630
+ if (filter && !filter(fullPath)) continue;
631
+ try {
632
+ const source = readFileSync3(fullPath, "utf-8");
633
+ if (ROUTER_CALL_PATTERN.test(source)) {
634
+ routerFilesInDir.push(fullPath);
635
+ }
636
+ } catch {
637
+ continue;
638
+ }
639
+ }
640
+ if (routerFilesInDir.length > 0) {
641
+ results.push(...routerFilesInDir);
642
+ return;
643
+ }
644
+ for (const childDir of childDirs) {
645
+ findRouterFilesRecursive(childDir, filter, results);
646
+ }
647
+ }
648
+ function findNestedRouterConflict(routerFiles) {
649
+ const routerDirs = [
650
+ ...new Set(routerFiles.map((filePath) => dirname2(resolve2(filePath))))
651
+ ].sort((a, b) => a.length - b.length);
652
+ for (let i = 0; i < routerDirs.length; i++) {
653
+ const ancestorDir = routerDirs[i];
654
+ const prefix = ancestorDir.endsWith(sep) ? ancestorDir : `${ancestorDir}${sep}`;
655
+ for (let j = i + 1; j < routerDirs.length; j++) {
656
+ const nestedDir = routerDirs[j];
657
+ if (!nestedDir.startsWith(prefix)) continue;
658
+ const ancestorFile = routerFiles.find(
659
+ (filePath) => dirname2(resolve2(filePath)) === ancestorDir
660
+ );
661
+ const nestedFile = routerFiles.find(
662
+ (filePath) => dirname2(resolve2(filePath)) === nestedDir
663
+ );
664
+ if (ancestorFile && nestedFile) {
665
+ return { ancestor: ancestorFile, nested: nestedFile };
666
+ }
667
+ }
668
+ }
669
+ return null;
670
+ }
671
+ function formatNestedRouterConflictError(conflict, prefix = "[rsc-router]") {
672
+ return `${prefix} Nested router roots are not supported.
673
+ Router root: ${conflict.ancestor}
674
+ Nested router: ${conflict.nested}
675
+ Move the nested router into a sibling directory or configure it as a separate app root.`;
676
+ }
677
+ function extractUrlsFromRouter(code) {
592
678
  const sourceFile = ts5.createSourceFile(
593
679
  "router.tsx",
594
680
  code,
@@ -602,24 +688,70 @@ function extractUrlsVariableFromRouter(code) {
602
688
  const callee = node.expression;
603
689
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
604
690
  }
691
+ function isInlineBuilder(node) {
692
+ return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
693
+ }
694
+ function isRoutesOnCreateRouter(node) {
695
+ if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
696
+ return false;
697
+ let inner = node.expression.expression;
698
+ while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
699
+ inner = inner.expression.expression;
700
+ }
701
+ return isCreateRouterCall(inner);
702
+ }
605
703
  function visit(node) {
606
704
  if (result) return;
607
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
608
- let inner = node.expression.expression;
609
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
610
- inner = inner.expression.expression;
611
- }
612
- if (isCreateRouterCall(inner)) {
613
- result = node.arguments[0].text;
614
- return;
705
+ if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
706
+ const arg = node.arguments[0];
707
+ if (ts5.isIdentifier(arg)) {
708
+ result = { kind: "variable", name: arg.text };
709
+ } else if (isInlineBuilder(arg)) {
710
+ result = { kind: "inline", block: arg.getText(sourceFile) };
615
711
  }
712
+ return;
616
713
  }
617
714
  if (isCreateRouterCall(node)) {
618
715
  const callExpr = node;
619
- for (const arg of callExpr.arguments) {
716
+ for (const callArg of callExpr.arguments) {
717
+ if (ts5.isObjectLiteralExpression(callArg)) {
718
+ for (const prop of callArg.properties) {
719
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
720
+ if (ts5.isIdentifier(prop.initializer)) {
721
+ result = { kind: "variable", name: prop.initializer.text };
722
+ } else if (isInlineBuilder(prop.initializer)) {
723
+ result = {
724
+ kind: "inline",
725
+ block: prop.initializer.getText(sourceFile)
726
+ };
727
+ }
728
+ return;
729
+ }
730
+ }
731
+ }
732
+ }
733
+ }
734
+ ts5.forEachChild(node, visit);
735
+ }
736
+ visit(sourceFile);
737
+ return result;
738
+ }
739
+ function extractBasenameFromRouter(code) {
740
+ const sourceFile = ts5.createSourceFile(
741
+ "router.tsx",
742
+ code,
743
+ ts5.ScriptTarget.Latest,
744
+ true,
745
+ ts5.ScriptKind.TSX
746
+ );
747
+ let result;
748
+ function visit(node) {
749
+ if (result !== void 0) return;
750
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
751
+ for (const arg of node.arguments) {
620
752
  if (ts5.isObjectLiteralExpression(arg)) {
621
753
  for (const prop of arg.properties) {
622
- if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls" && ts5.isIdentifier(prop.initializer)) {
754
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
623
755
  result = prop.initializer.text;
624
756
  return;
625
757
  }
@@ -632,6 +764,19 @@ function extractUrlsVariableFromRouter(code) {
632
764
  visit(sourceFile);
633
765
  return result;
634
766
  }
767
+ function applyBasenameToRoutes(result, basename2) {
768
+ const prefixed = {};
769
+ for (const [name, pattern] of Object.entries(result.routes)) {
770
+ if (pattern === "/") {
771
+ prefixed[name] = basename2;
772
+ } else if (basename2.endsWith("/") && pattern.startsWith("/")) {
773
+ prefixed[name] = basename2 + pattern.slice(1);
774
+ } else {
775
+ prefixed[name] = basename2 + pattern;
776
+ }
777
+ }
778
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
779
+ }
635
780
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
636
781
  let routerSource;
637
782
  try {
@@ -639,19 +784,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
639
784
  } catch {
640
785
  return { routes: {}, searchSchemas: {} };
641
786
  }
642
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
643
- if (!urlsVarName) {
787
+ const extraction = extractUrlsFromRouter(routerSource);
788
+ if (!extraction) {
644
789
  return { routes: {}, searchSchemas: {} };
645
790
  }
646
- const imported = resolveImportedVariable(routerSource, urlsVarName);
647
- if (imported) {
648
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
649
- if (!targetFile) {
650
- return { routes: {}, searchSchemas: {} };
791
+ const rawBasename = extractBasenameFromRouter(routerSource);
792
+ const basename2 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
793
+ let result;
794
+ if (extraction.kind === "inline") {
795
+ result = buildCombinedRouteMapWithSearch(
796
+ routerFilePath,
797
+ void 0,
798
+ void 0,
799
+ void 0,
800
+ extraction.block
801
+ );
802
+ } else {
803
+ const imported = resolveImportedVariable(routerSource, extraction.name);
804
+ if (imported) {
805
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
806
+ if (!targetFile) {
807
+ return { routes: {}, searchSchemas: {} };
808
+ }
809
+ result = buildCombinedRouteMapWithSearch(
810
+ targetFile,
811
+ imported.exportedName
812
+ );
813
+ } else {
814
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
651
815
  }
652
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
653
816
  }
654
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
817
+ if (basename2) {
818
+ result = applyBasenameToRoutes(result, basename2);
819
+ }
820
+ return result;
655
821
  }
656
822
  function detectUnresolvableIncludes(routerFilePath) {
657
823
  const realPath = resolve2(routerFilePath);
@@ -661,9 +827,20 @@ function detectUnresolvableIncludes(routerFilePath) {
661
827
  } catch {
662
828
  return [];
663
829
  }
664
- const urlsVarName = extractUrlsVariableFromRouter(source);
665
- if (!urlsVarName) return [];
666
- const imported = resolveImportedVariable(source, urlsVarName);
830
+ const extraction = extractUrlsFromRouter(source);
831
+ if (!extraction) return [];
832
+ const diagnostics = [];
833
+ if (extraction.kind === "inline") {
834
+ buildCombinedRouteMapWithSearch(
835
+ realPath,
836
+ void 0,
837
+ /* @__PURE__ */ new Set(),
838
+ diagnostics,
839
+ extraction.block
840
+ );
841
+ return diagnostics;
842
+ }
843
+ const imported = resolveImportedVariable(source, extraction.name);
667
844
  let targetFile;
668
845
  let exportedName;
669
846
  if (imported) {
@@ -683,9 +860,8 @@ function detectUnresolvableIncludes(routerFilePath) {
683
860
  exportedName = imported.exportedName;
684
861
  } else {
685
862
  targetFile = realPath;
686
- exportedName = urlsVarName;
863
+ exportedName = extraction.name;
687
864
  }
688
- const diagnostics = [];
689
865
  buildCombinedRouteMapWithSearch(
690
866
  targetFile,
691
867
  exportedName,
@@ -711,19 +887,8 @@ function detectUnresolvableIncludesForUrlsFile(filePath) {
711
887
  return diagnostics;
712
888
  }
713
889
  function findRouterFiles(root, filter) {
714
- const files = findTsFiles(root, filter);
715
890
  const result = [];
716
- for (const filePath of files) {
717
- if (filePath.includes(".gen.")) continue;
718
- try {
719
- const source = readFileSync3(filePath, "utf-8");
720
- if (/\bcreateRouter\s*[<(]/.test(source)) {
721
- result.push(filePath);
722
- }
723
- } catch {
724
- continue;
725
- }
726
- }
891
+ findRouterFilesRecursive(root, filter, result);
727
892
  return result;
728
893
  }
729
894
  function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
@@ -739,26 +904,20 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
739
904
  }
740
905
  const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
741
906
  if (routerFilePaths.length === 0) return;
907
+ const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
908
+ if (nestedRouterConflict) {
909
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
910
+ }
742
911
  for (const routerFilePath of routerFilePaths) {
743
- let routerSource;
744
- try {
745
- routerSource = readFileSync3(routerFilePath, "utf-8");
746
- } catch {
747
- continue;
748
- }
749
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
750
- if (!urlsVarName) continue;
751
- let result;
752
- const imported = resolveImportedVariable(routerSource, urlsVarName);
753
- if (imported) {
754
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
755
- if (!targetFile) continue;
756
- result = buildCombinedRouteMapWithSearch(
757
- targetFile,
758
- imported.exportedName
759
- );
760
- } else {
761
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
912
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
913
+ if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
914
+ let routerSource;
915
+ try {
916
+ routerSource = readFileSync3(routerFilePath, "utf-8");
917
+ } catch {
918
+ continue;
919
+ }
920
+ if (!extractUrlsFromRouter(routerSource)) continue;
762
921
  }
763
922
  const routerBasename = pathBasename(routerFilePath).replace(
764
923
  /\.(tsx?|jsx?)$/,
@@ -798,14 +957,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
798
957
  }
799
958
  }
800
959
  }
960
+ var ROUTER_CALL_PATTERN;
801
961
  var init_router_processing = __esm({
802
962
  "src/build/route-types/router-processing.ts"() {
803
963
  "use strict";
804
964
  init_codegen();
805
- init_scan_filter();
806
965
  init_include_resolution();
807
966
  init_per_module_writer();
808
967
  init_route_name();
968
+ ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
809
969
  }
810
970
  });
811
971
 
@@ -986,8 +1146,9 @@ function createVersionPlugin() {
986
1146
  let isDev = false;
987
1147
  let server = null;
988
1148
  const clientModuleSignatures = /* @__PURE__ */ new Map();
1149
+ let versionCounter = 0;
989
1150
  const bumpVersion = (reason) => {
990
- currentVersion = Date.now().toString(16);
1151
+ currentVersion = Date.now().toString(16) + String(++versionCounter);
991
1152
  console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
992
1153
  const rscEnv = server?.environments?.rsc;
993
1154
  const versionMod = rscEnv?.moduleGraph?.getModuleById(
@@ -1043,6 +1204,9 @@ function createVersionPlugin() {
1043
1204
  if (!isDev) return;
1044
1205
  const isRscModule = this.environment?.name === "rsc";
1045
1206
  if (!isRscModule) return;
1207
+ if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
1208
+ return;
1209
+ }
1046
1210
  if (isCodeModule(ctx.file)) {
1047
1211
  const filePath = normalizeModuleId(ctx.file);
1048
1212
  const previousSignature = clientModuleSignatures.get(filePath);
@@ -1428,6 +1592,15 @@ function runStaticGeneration(args, mode) {
1428
1592
  formatDiagnostics(uniqueDiagnostics);
1429
1593
  console.warn("");
1430
1594
  }
1595
+ const nestedRouterConflict = findNestedRouterConflict(routerFiles);
1596
+ if (nestedRouterConflict) {
1597
+ console.error(
1598
+ `
1599
+ ${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
1600
+ `
1601
+ );
1602
+ process.exit(1);
1603
+ }
1431
1604
  for (const urlsFile of urlsFiles) {
1432
1605
  writePerModuleRouteTypesForFile(urlsFile);
1433
1606
  }
@@ -1471,6 +1644,15 @@ async function runRuntimeDiscovery(args, configFile) {
1471
1644
  console.error("[rango] No router files found in the provided paths");
1472
1645
  process.exit(1);
1473
1646
  }
1647
+ const nestedRouterConflict = findNestedRouterConflict(routerEntries);
1648
+ if (nestedRouterConflict) {
1649
+ console.error(
1650
+ `
1651
+ ${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
1652
+ `
1653
+ );
1654
+ process.exit(1);
1655
+ }
1474
1656
  let discoverAndWriteRouteTypes2;
1475
1657
  try {
1476
1658
  const mod = await Promise.resolve().then(() => (init_runtime_discovery(), runtime_discovery_exports));