@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c

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 (189) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +172 -50
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1160 -508
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +17 -16
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/cache-guide/SKILL.md +32 -0
  10. package/skills/caching/SKILL.md +49 -8
  11. package/skills/document-cache/SKILL.md +2 -2
  12. package/skills/handler-use/SKILL.md +362 -0
  13. package/skills/hooks/SKILL.md +61 -51
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +20 -0
  16. package/skills/layout/SKILL.md +22 -0
  17. package/skills/links/SKILL.md +91 -17
  18. package/skills/loader/SKILL.md +107 -24
  19. package/skills/middleware/SKILL.md +34 -3
  20. package/skills/migrate-nextjs/SKILL.md +560 -0
  21. package/skills/migrate-react-router/SKILL.md +765 -0
  22. package/skills/parallel/SKILL.md +185 -0
  23. package/skills/prerender/SKILL.md +112 -70
  24. package/skills/rango/SKILL.md +24 -23
  25. package/skills/response-routes/SKILL.md +8 -0
  26. package/skills/route/SKILL.md +58 -4
  27. package/skills/router-setup/SKILL.md +95 -5
  28. package/skills/streams-and-websockets/SKILL.md +283 -0
  29. package/skills/typesafety/SKILL.md +38 -24
  30. package/src/__internal.ts +92 -0
  31. package/src/browser/app-shell.ts +52 -0
  32. package/src/browser/app-version.ts +14 -0
  33. package/src/browser/event-controller.ts +5 -0
  34. package/src/browser/link-interceptor.ts +4 -0
  35. package/src/browser/navigation-bridge.ts +175 -17
  36. package/src/browser/navigation-client.ts +177 -44
  37. package/src/browser/navigation-store.ts +68 -9
  38. package/src/browser/navigation-transaction.ts +11 -9
  39. package/src/browser/partial-update.ts +113 -17
  40. package/src/browser/prefetch/cache.ts +275 -28
  41. package/src/browser/prefetch/fetch.ts +191 -46
  42. package/src/browser/prefetch/policy.ts +6 -0
  43. package/src/browser/prefetch/queue.ts +123 -20
  44. package/src/browser/prefetch/resource-ready.ts +77 -0
  45. package/src/browser/rango-state.ts +53 -13
  46. package/src/browser/react/Link.tsx +98 -14
  47. package/src/browser/react/NavigationProvider.tsx +89 -14
  48. package/src/browser/react/context.ts +7 -2
  49. package/src/browser/react/use-handle.ts +9 -58
  50. package/src/browser/react/use-navigation.ts +22 -2
  51. package/src/browser/react/use-params.ts +11 -1
  52. package/src/browser/react/use-router.ts +29 -9
  53. package/src/browser/rsc-router.tsx +177 -66
  54. package/src/browser/scroll-restoration.ts +41 -42
  55. package/src/browser/segment-reconciler.ts +36 -9
  56. package/src/browser/server-action-bridge.ts +8 -6
  57. package/src/browser/types.ts +73 -5
  58. package/src/build/generate-manifest.ts +6 -6
  59. package/src/build/generate-route-types.ts +3 -0
  60. package/src/build/route-trie.ts +67 -25
  61. package/src/build/route-types/include-resolution.ts +8 -1
  62. package/src/build/route-types/router-processing.ts +223 -74
  63. package/src/build/route-types/scan-filter.ts +8 -1
  64. package/src/cache/cache-runtime.ts +15 -11
  65. package/src/cache/cache-scope.ts +48 -7
  66. package/src/cache/cf/cf-cache-store.ts +455 -15
  67. package/src/cache/cf/index.ts +5 -1
  68. package/src/cache/document-cache.ts +17 -7
  69. package/src/cache/index.ts +1 -0
  70. package/src/cache/taint.ts +55 -0
  71. package/src/client.rsc.tsx +2 -1
  72. package/src/client.tsx +85 -276
  73. package/src/context-var.ts +72 -2
  74. package/src/debug.ts +2 -2
  75. package/src/handle.ts +40 -0
  76. package/src/handles/breadcrumbs.ts +66 -0
  77. package/src/handles/index.ts +1 -0
  78. package/src/host/index.ts +0 -3
  79. package/src/index.rsc.ts +9 -36
  80. package/src/index.ts +79 -70
  81. package/src/outlet-context.ts +1 -1
  82. package/src/prerender/store.ts +57 -15
  83. package/src/prerender.ts +138 -77
  84. package/src/response-utils.ts +28 -0
  85. package/src/reverse.ts +27 -2
  86. package/src/route-definition/dsl-helpers.ts +240 -40
  87. package/src/route-definition/helpers-types.ts +67 -19
  88. package/src/route-definition/index.ts +3 -3
  89. package/src/route-definition/redirect.ts +11 -3
  90. package/src/route-definition/resolve-handler-use.ts +155 -0
  91. package/src/route-map-builder.ts +7 -1
  92. package/src/route-types.ts +18 -0
  93. package/src/router/content-negotiation.ts +100 -1
  94. package/src/router/find-match.ts +4 -2
  95. package/src/router/handler-context.ts +129 -26
  96. package/src/router/intercept-resolution.ts +11 -4
  97. package/src/router/lazy-includes.ts +10 -7
  98. package/src/router/loader-resolution.ts +160 -22
  99. package/src/router/logging.ts +5 -2
  100. package/src/router/manifest.ts +31 -16
  101. package/src/router/match-api.ts +128 -193
  102. package/src/router/match-middleware/background-revalidation.ts +30 -2
  103. package/src/router/match-middleware/cache-lookup.ts +94 -17
  104. package/src/router/match-middleware/cache-store.ts +53 -10
  105. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  106. package/src/router/match-middleware/segment-resolution.ts +61 -5
  107. package/src/router/match-result.ts +103 -18
  108. package/src/router/metrics.ts +238 -13
  109. package/src/router/middleware-types.ts +48 -27
  110. package/src/router/middleware.ts +201 -86
  111. package/src/router/navigation-snapshot.ts +182 -0
  112. package/src/router/pattern-matching.ts +77 -11
  113. package/src/router/prerender-match.ts +114 -10
  114. package/src/router/preview-match.ts +30 -102
  115. package/src/router/request-classification.ts +310 -0
  116. package/src/router/revalidation.ts +27 -7
  117. package/src/router/route-snapshot.ts +245 -0
  118. package/src/router/router-context.ts +6 -1
  119. package/src/router/router-interfaces.ts +50 -5
  120. package/src/router/router-options.ts +50 -19
  121. package/src/router/segment-resolution/fresh.ts +215 -19
  122. package/src/router/segment-resolution/helpers.ts +30 -25
  123. package/src/router/segment-resolution/loader-cache.ts +1 -0
  124. package/src/router/segment-resolution/revalidation.ts +454 -301
  125. package/src/router/segment-wrappers.ts +2 -0
  126. package/src/router/trie-matching.ts +30 -6
  127. package/src/router/types.ts +1 -0
  128. package/src/router/url-params.ts +49 -0
  129. package/src/router.ts +89 -17
  130. package/src/rsc/handler.ts +563 -364
  131. package/src/rsc/helpers.ts +69 -41
  132. package/src/rsc/index.ts +0 -20
  133. package/src/rsc/loader-fetch.ts +23 -3
  134. package/src/rsc/manifest-init.ts +5 -1
  135. package/src/rsc/progressive-enhancement.ts +37 -10
  136. package/src/rsc/response-route-handler.ts +14 -1
  137. package/src/rsc/rsc-rendering.ts +47 -44
  138. package/src/rsc/server-action.ts +24 -10
  139. package/src/rsc/ssr-setup.ts +128 -0
  140. package/src/rsc/types.ts +11 -1
  141. package/src/search-params.ts +16 -13
  142. package/src/segment-content-promise.ts +67 -0
  143. package/src/segment-loader-promise.ts +122 -0
  144. package/src/segment-system.tsx +109 -23
  145. package/src/server/context.ts +174 -19
  146. package/src/server/handle-store.ts +19 -0
  147. package/src/server/loader-registry.ts +9 -8
  148. package/src/server/request-context.ts +218 -65
  149. package/src/server.ts +6 -0
  150. package/src/ssr/index.tsx +4 -0
  151. package/src/static-handler.ts +18 -6
  152. package/src/theme/index.ts +4 -13
  153. package/src/types/cache-types.ts +4 -4
  154. package/src/types/handler-context.ts +140 -72
  155. package/src/types/loader-types.ts +41 -15
  156. package/src/types/request-scope.ts +126 -0
  157. package/src/types/route-config.ts +17 -8
  158. package/src/types/route-entry.ts +19 -1
  159. package/src/types/segments.ts +2 -5
  160. package/src/urls/include-helper.ts +24 -14
  161. package/src/urls/path-helper-types.ts +39 -6
  162. package/src/urls/path-helper.ts +48 -13
  163. package/src/urls/pattern-types.ts +12 -0
  164. package/src/urls/response-types.ts +18 -16
  165. package/src/use-loader.tsx +77 -5
  166. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  167. package/src/vite/discovery/discover-routers.ts +7 -4
  168. package/src/vite/discovery/prerender-collection.ts +162 -88
  169. package/src/vite/discovery/state.ts +17 -13
  170. package/src/vite/index.ts +8 -3
  171. package/src/vite/plugin-types.ts +51 -79
  172. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  173. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  174. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  175. package/src/vite/plugins/expose-action-id.ts +1 -3
  176. package/src/vite/plugins/expose-id-utils.ts +12 -0
  177. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  178. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  179. package/src/vite/plugins/performance-tracks.ts +88 -0
  180. package/src/vite/plugins/refresh-cmd.ts +127 -0
  181. package/src/vite/plugins/version-plugin.ts +13 -1
  182. package/src/vite/rango.ts +190 -217
  183. package/src/vite/router-discovery.ts +241 -45
  184. package/src/vite/utils/banner.ts +4 -4
  185. package/src/vite/utils/package-resolution.ts +34 -1
  186. package/src/vite/utils/prerender-utils.ts +97 -5
  187. package/src/vite/utils/shared-utils.ts +3 -2
  188. package/skills/testing/SKILL.md +0 -226
  189. package/src/route-definition/route-function.ts +0 -119
package/AGENTS.md CHANGED
@@ -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
 
@@ -129,13 +161,18 @@ const urlpatterns = urls(({ path }) => [
129
161
  ]);
130
162
  ```
131
163
 
132
- Use `reverse()` as the default way to link to routes:
164
+ Use `ctx.reverse()` from handler context as the default way to link to routes from server code:
133
165
 
134
166
  ```tsx
135
- router.reverse("product", { slug: "widget" }); // "/product/widget"
136
- router.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
167
+ const ProductPage: Handler<"product"> = (ctx) => {
168
+ const url = ctx.reverse("product", { slug: "widget" }); // "/product/widget"
169
+ const searchUrl = ctx.reverse("search", undefined, { q: "rsc" }); // "/search?q=rsc"
170
+ return <Link to={url}>Widget</Link>;
171
+ };
137
172
  ```
138
173
 
174
+ `router.reverse()` (exported from the router module) is the same function without a handler context, useful in scripts or tests. In request code, prefer `ctx.reverse()` — it auto-fills mount params from the current match.
175
+
139
176
  ### Composable URL Modules
140
177
 
141
178
  Local route names compose cleanly with `include(..., { name })`:
@@ -248,7 +285,8 @@ All handler typing styles are supported, but they solve different problems:
248
285
  Example of a scoped local name inside a mounted module:
249
286
 
250
287
  ```tsx
251
- import type { Handler, ScopedRouteMap } from "@rangojs/router";
288
+ import type { Handler } from "@rangojs/router";
289
+ import type { ScopedRouteMap } from "@rangojs/router/__internal";
252
290
 
253
291
  type BlogRoutes = ScopedRouteMap<"blog">;
254
292
 
@@ -446,39 +484,64 @@ const urlpatterns = urls(({ path, loader }) => [
446
484
 
447
485
  ## Navigation & Links
448
486
 
449
- ### Named Routes with `reverse()` (Server Components)
487
+ ### Named Routes with `ctx.reverse()` (Server)
450
488
 
451
- In server components, use `reverse()` to generate URLs by route name:
489
+ In server components and handlers, use `ctx.reverse()` to generate URLs by route name. This is the default — it is typed, auto-fills mount params from the current match, and resolves both local (`.name`) and absolute (`name.sub`) names:
452
490
 
453
491
  ```tsx
454
492
  import { Link } from "@rangojs/router/client";
493
+ import type { Handler } from "@rangojs/router";
494
+
495
+ const BlogPostPage: Handler<"blogPost"> = (ctx) => {
496
+ const backUrl = ctx.reverse("blog");
497
+ return <Link to={backUrl}>Back to blog</Link>;
498
+ };
499
+ ```
500
+
501
+ `reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `ctx.reverse("api.health")`.
502
+
503
+ For scripts, tests, or other code without a handler context, import the router-level `reverse`:
504
+
505
+ ```tsx
455
506
  import { reverse } from "./router";
507
+ reverse("blogPost", { slug: "my-post" });
508
+ ```
509
+
510
+ ### Client Components
511
+
512
+ **`reverse()` is server-only.** It depends on the route manifest and handler context — neither is available in the browser bundle. Client components receive URLs as props, loader data, or server-action return values:
456
513
 
457
- function BlogIndex() {
514
+ ```tsx
515
+ // server
516
+ function BlogIndex(ctx: HandlerContext) {
458
517
  return (
459
- <nav>
460
- <Link to={reverse("home")}>Home</Link>
461
- <Link to={reverse("blogPost", { slug: "my-post" })}>My Post</Link>
462
- <Link to={reverse("about")}>About</Link>
463
- </nav>
518
+ <Nav
519
+ home={ctx.reverse("home")}
520
+ post={ctx.reverse("blogPost", { slug: "my-post" })}
521
+ />
464
522
  );
465
523
  }
466
524
  ```
467
525
 
468
- `reverse()` is type-safe — route names and required params are checked at compile time. Included routes use dotted names: `reverse("api.health")`.
469
-
470
- Handlers also have `ctx.reverse()` directly on the context:
471
-
472
526
  ```tsx
473
- const BlogPostPage: Handler<"blogPost"> = (ctx) => {
474
- const backUrl = ctx.reverse("blog");
475
- return <Link to={backUrl}>Back to blog</Link>;
476
- };
527
+ "use client";
528
+ import { Link } from "@rangojs/router/client";
529
+
530
+ export function Nav({ home, post }: { home: string; post: string }) {
531
+ return (
532
+ <nav>
533
+ <Link to={home}>Home</Link>
534
+ <Link to={post}>My Post</Link>
535
+ </nav>
536
+ );
537
+ }
477
538
  ```
478
539
 
540
+ For client-side navigation to static paths (no named-route lookup), use `href()` — see below. For URLs tied to named routes, always generate on the server and pass the string in.
541
+
479
542
  ### `href()` for Path Validation (Client Components)
480
543
 
481
- In client components, use `href()` for compile-time path validation:
544
+ In client components, use `href()` for compile-time path validation on static path strings:
482
545
 
483
546
  ```tsx
484
547
  "use client";
@@ -488,7 +551,7 @@ function Nav() {
488
551
  return (
489
552
  <nav>
490
553
  <Link to={href("/")}>Home</Link>
491
- <Link to={href("/blog")} prefetch="hybrid">
554
+ <Link to={href("/blog")} prefetch="adaptive">
492
555
  Blog
493
556
  </Link>
494
557
  <Link to={href("/about")}>About</Link>
@@ -683,10 +746,12 @@ export const BlogPost = Prerender(
683
746
 
684
747
  ### Passthrough for Unknown Params
685
748
 
749
+ 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.
750
+
686
751
  ```tsx
687
- import { Prerender } from "@rangojs/router";
752
+ import { Prerender, Passthrough } from "@rangojs/router";
688
753
 
689
- export const ProductPage = Prerender(
754
+ export const ProductPageDef = Prerender(
690
755
  async () => {
691
756
  const featured = await db.getFeaturedProducts();
692
757
  return featured.map((p) => ({ id: p.id }));
@@ -695,16 +760,22 @@ export const ProductPage = Prerender(
695
760
  const product = await db.getProduct(ctx.params.id);
696
761
  return <Product data={product} />;
697
762
  },
698
- { passthrough: true },
699
763
  );
700
- ```
701
764
 
702
- With `passthrough: true`, known params are served from the build-time cache and unknown params fall through to live rendering.
765
+ // In route definition:
766
+ path(
767
+ "/products/:id",
768
+ Passthrough(ProductPageDef, async (ctx) => {
769
+ const product = await ctx.env.DB.getProduct(ctx.params.id);
770
+ return <Product data={product} />;
771
+ }),
772
+ );
773
+ ```
703
774
 
704
- Handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler at runtime:
775
+ Build handlers can also skip individual param sets with `ctx.passthrough()`, deferring them to the live handler:
705
776
 
706
777
  ```tsx
707
- export const ProductPage = Prerender(
778
+ export const ProductPageDef = Prerender(
708
779
  async () => {
709
780
  const all = await db.getAllProducts();
710
781
  return all.map((p) => ({ id: p.id }));
@@ -714,10 +785,55 @@ export const ProductPage = Prerender(
714
785
  if (!product.published) return ctx.passthrough();
715
786
  return <Product data={product} />;
716
787
  },
717
- { passthrough: true },
718
788
  );
719
789
  ```
720
790
 
791
+ ### Build-Time Environment Bindings
792
+
793
+ Prerender handlers can access platform bindings (KV, D1, R2) at build time when `buildEnv` is configured in the Vite plugin:
794
+
795
+ ```ts
796
+ // vite.config.ts
797
+ import { rango } from "@rangojs/router/vite";
798
+
799
+ rango({ preset: "cloudflare", buildEnv: "auto" });
800
+ ```
801
+
802
+ With `buildEnv: "auto"`, the plugin calls `wrangler.getPlatformProxy()` to provide local bindings. Handlers then access `ctx.env` during build:
803
+
804
+ ```tsx
805
+ export const BlogPosts = Prerender<{ slug: string }>(
806
+ async (ctx) => {
807
+ const rows = await ctx.env.DB.prepare("SELECT slug FROM posts").all();
808
+ return rows.map((r) => ({ slug: r.slug }));
809
+ },
810
+ async (ctx) => {
811
+ const post = await ctx.env.DB.prepare("SELECT * FROM posts WHERE slug = ?")
812
+ .bind(ctx.params.slug)
813
+ .first();
814
+ return <BlogPost post={post} />;
815
+ },
816
+ );
817
+ ```
818
+
819
+ `buildEnv` also accepts a factory function or plain object:
820
+
821
+ ```ts
822
+ // Custom factory
823
+ rango({
824
+ buildEnv: async (ctx) => {
825
+ const { getPlatformProxy } = await import("wrangler");
826
+ const proxy = await getPlatformProxy();
827
+ return { env: proxy.env, dispose: proxy.dispose };
828
+ },
829
+ });
830
+
831
+ // Plain object (Node.js)
832
+ rango({ buildEnv: { DATABASE_URL: process.env.DATABASE_URL } });
833
+ ```
834
+
835
+ 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.
836
+
721
837
  ## Theme
722
838
 
723
839
  ### Router Configuration
@@ -842,16 +958,22 @@ module, use `scopedReverse<typeof localPatterns>(ctx.reverse)` or
842
958
 
843
959
  ## Subpath Exports
844
960
 
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 |
961
+ | Export | Description |
962
+ | ------------------------ | -------------------------------------------------------------------------------------------------------- |
963
+ | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
964
+ | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
965
+ | `@rangojs/router/cache` | Cache: `CFCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
966
+ | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
967
+ | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts` |
968
+ | `@rangojs/router/vite` | Vite plugin: `rango()` |
969
+ | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
970
+ | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
971
+ | `@rangojs/router/server` | Internal build/runtime utilities for advanced integrations |
972
+ | `@rangojs/router/build` | Build utilities |
973
+
974
+ The root entrypoint is not a generic client/runtime barrel. If you need hooks
975
+ or components, import from `@rangojs/router/client`; if you need cache or host
976
+ APIs, use their dedicated subpaths.
855
977
 
856
978
  ## Examples
857
979
 
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;
@@ -601,7 +604,7 @@ function countPublicRouteEntries(source) {
601
604
  return count;
602
605
  }
603
606
  function isRoutableSourceFile(name) {
604
- return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.");
607
+ return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
605
608
  }
606
609
  function findRouterFilesRecursive(dir, filter, results) {
607
610
  let entries;
@@ -618,7 +621,8 @@ function findRouterFilesRecursive(dir, filter, results) {
618
621
  for (const entry of entries) {
619
622
  const fullPath = join2(dir, entry.name);
620
623
  if (entry.isDirectory()) {
621
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
624
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
625
+ continue;
622
626
  childDirs.push(fullPath);
623
627
  continue;
624
628
  }
@@ -670,7 +674,7 @@ Router root: ${conflict.ancestor}
670
674
  Nested router: ${conflict.nested}
671
675
  Move the nested router into a sibling directory or configure it as a separate app root.`;
672
676
  }
673
- function extractUrlsVariableFromRouter(code) {
677
+ function extractUrlsFromRouter(code) {
674
678
  const sourceFile = ts5.createSourceFile(
675
679
  "router.tsx",
676
680
  code,
@@ -684,24 +688,70 @@ function extractUrlsVariableFromRouter(code) {
684
688
  const callee = node.expression;
685
689
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
686
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
+ }
687
703
  function visit(node) {
688
704
  if (result) return;
689
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
690
- let inner = node.expression.expression;
691
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
692
- inner = inner.expression.expression;
693
- }
694
- if (isCreateRouterCall(inner)) {
695
- result = node.arguments[0].text;
696
- 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) };
697
711
  }
712
+ return;
698
713
  }
699
714
  if (isCreateRouterCall(node)) {
700
715
  const callExpr = node;
701
- 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) {
702
752
  if (ts5.isObjectLiteralExpression(arg)) {
703
753
  for (const prop of arg.properties) {
704
- 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)) {
705
755
  result = prop.initializer.text;
706
756
  return;
707
757
  }
@@ -714,6 +764,19 @@ function extractUrlsVariableFromRouter(code) {
714
764
  visit(sourceFile);
715
765
  return result;
716
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
+ }
717
780
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
718
781
  let routerSource;
719
782
  try {
@@ -721,19 +784,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
721
784
  } catch {
722
785
  return { routes: {}, searchSchemas: {} };
723
786
  }
724
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
725
- if (!urlsVarName) {
787
+ const extraction = extractUrlsFromRouter(routerSource);
788
+ if (!extraction) {
726
789
  return { routes: {}, searchSchemas: {} };
727
790
  }
728
- const imported = resolveImportedVariable(routerSource, urlsVarName);
729
- if (imported) {
730
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
731
- if (!targetFile) {
732
- 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);
733
815
  }
734
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
735
816
  }
736
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
817
+ if (basename2) {
818
+ result = applyBasenameToRoutes(result, basename2);
819
+ }
820
+ return result;
737
821
  }
738
822
  function detectUnresolvableIncludes(routerFilePath) {
739
823
  const realPath = resolve2(routerFilePath);
@@ -743,9 +827,20 @@ function detectUnresolvableIncludes(routerFilePath) {
743
827
  } catch {
744
828
  return [];
745
829
  }
746
- const urlsVarName = extractUrlsVariableFromRouter(source);
747
- if (!urlsVarName) return [];
748
- 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);
749
844
  let targetFile;
750
845
  let exportedName;
751
846
  if (imported) {
@@ -765,9 +860,8 @@ function detectUnresolvableIncludes(routerFilePath) {
765
860
  exportedName = imported.exportedName;
766
861
  } else {
767
862
  targetFile = realPath;
768
- exportedName = urlsVarName;
863
+ exportedName = extraction.name;
769
864
  }
770
- const diagnostics = [];
771
865
  buildCombinedRouteMapWithSearch(
772
866
  targetFile,
773
867
  exportedName,
@@ -815,25 +909,15 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
815
909
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
816
910
  }
817
911
  for (const routerFilePath of routerFilePaths) {
818
- let routerSource;
819
- try {
820
- routerSource = readFileSync3(routerFilePath, "utf-8");
821
- } catch {
822
- continue;
823
- }
824
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
825
- if (!urlsVarName) continue;
826
- let result;
827
- const imported = resolveImportedVariable(routerSource, urlsVarName);
828
- if (imported) {
829
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
830
- if (!targetFile) continue;
831
- result = buildCombinedRouteMapWithSearch(
832
- targetFile,
833
- imported.exportedName
834
- );
835
- } else {
836
- 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;
837
921
  }
838
922
  const routerBasename = pathBasename(routerFilePath).replace(
839
923
  /\.(tsx?|jsx?)$/,
@@ -1062,8 +1146,9 @@ function createVersionPlugin() {
1062
1146
  let isDev = false;
1063
1147
  let server = null;
1064
1148
  const clientModuleSignatures = /* @__PURE__ */ new Map();
1149
+ let versionCounter = 0;
1065
1150
  const bumpVersion = (reason) => {
1066
- currentVersion = Date.now().toString(16);
1151
+ currentVersion = Date.now().toString(16) + String(++versionCounter);
1067
1152
  console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
1068
1153
  const rscEnv = server?.environments?.rsc;
1069
1154
  const versionMod = rscEnv?.moduleGraph?.getModuleById(
@@ -1119,6 +1204,9 @@ function createVersionPlugin() {
1119
1204
  if (!isDev) return;
1120
1205
  const isRscModule = this.environment?.name === "rsc";
1121
1206
  if (!isRscModule) return;
1207
+ if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
1208
+ return;
1209
+ }
1122
1210
  if (isCodeModule(ctx.file)) {
1123
1211
  const filePath = normalizeModuleId(ctx.file);
1124
1212
  const previousSignature = clientModuleSignatures.get(filePath);