@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/vite/rango.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import type { PluginOption } from "vite";
2
2
  import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
3
4
  import { exposeActionId } from "./plugins/expose-action-id.js";
4
5
  import {
5
6
  exposeInternalIds,
6
7
  exposeRouterId,
7
8
  } from "./plugins/expose-internal-ids.js";
8
9
  import { useCacheTransform } from "./plugins/use-cache-transform.js";
10
+ import { clientRefDedup } from "./plugins/client-ref-dedup.js";
9
11
  import { VIRTUAL_IDS } from "./plugins/virtual-entries.js";
10
12
  import {
11
13
  getExcludeDeps,
@@ -70,8 +72,10 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
70
72
  // Track RSC entry path for version injection
71
73
  let rscEntryPath: string | null = null;
72
74
 
73
- // Resolved router path (node preset only, may be auto-discovered)
74
- let routerPath: string | undefined;
75
+ // Mutable ref for router path (node preset only).
76
+ // Set immediately when user-specified, or populated by the auto-discover
77
+ // config() hook using Vite's resolved root.
78
+ const routerRef: { path: string | undefined } = { path: undefined };
75
79
 
76
80
  // Build-time prerendering is enabled for both presets.
77
81
  // Collection runs in-process via the RSC dev environment runner during discoverRouters().
@@ -193,43 +197,54 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
193
197
  // since SSR runs in workerd alongside RSC
194
198
  plugins.push(
195
199
  rsc({
196
- get entries() {
197
- return finalEntries;
198
- },
200
+ entries: finalEntries,
199
201
  serverHandler: false,
200
202
  }) as PluginOption,
201
203
  );
204
+
205
+ // Deduplicate client references from third-party packages in dev mode.
206
+ // Prevents module duplication when server components import "use client"
207
+ // packages that are also imported directly by client components.
208
+ plugins.push(clientRefDedup());
202
209
  } else {
203
210
  // Node preset: full RSC plugin integration
204
211
  const nodeOptions = resolvedOptions as RangoNodeOptions;
205
- routerPath = nodeOptions.router;
206
212
 
207
- // Auto-discover router when not specified
208
- if (!routerPath) {
209
- const earlyFilter = createScanFilter(process.cwd(), {
210
- include: resolvedOptions.include,
211
- exclude: resolvedOptions.exclude,
213
+ routerRef.path = nodeOptions.router;
214
+
215
+ // Auto-discover router using Vite's resolved root (not process.cwd())
216
+ if (!routerRef.path) {
217
+ plugins.push({
218
+ name: "@rangojs/router:auto-discover",
219
+ config(userConfig) {
220
+ if (routerRef.path) return;
221
+ const root = userConfig.root
222
+ ? resolve(process.cwd(), userConfig.root)
223
+ : process.cwd();
224
+ const filter = createScanFilter(root, {
225
+ include: resolvedOptions.include,
226
+ exclude: resolvedOptions.exclude,
227
+ });
228
+ const candidates = findRouterFiles(root, filter);
229
+ if (candidates.length === 1) {
230
+ const abs = candidates[0];
231
+ routerRef.path = (
232
+ abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs
233
+ ).replaceAll("\\", "/");
234
+ } else if (candidates.length > 1) {
235
+ const list = candidates
236
+ .map(
237
+ (f) =>
238
+ " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f),
239
+ )
240
+ .join("\n");
241
+ throw new Error(
242
+ `[rsc-router] Multiple routers found. Specify \`router\` to choose one:\n${list}`,
243
+ );
244
+ }
245
+ // 0 found: routerRef.path stays undefined, warn at startup via discovery plugin
246
+ },
212
247
  });
213
- const candidates = findRouterFiles(process.cwd(), earlyFilter);
214
- if (candidates.length === 1) {
215
- // Convert absolute path to relative ./path
216
- const abs = candidates[0];
217
- const rel = abs.startsWith(process.cwd())
218
- ? "./" + abs.slice(process.cwd().length + 1)
219
- : abs;
220
- routerPath = rel;
221
- } else if (candidates.length > 1) {
222
- const cwd = process.cwd();
223
- const list = candidates
224
- .map(
225
- (f) => " - " + (f.startsWith(cwd) ? f.slice(cwd.length + 1) : f),
226
- )
227
- .join("\n");
228
- throw new Error(
229
- `[rsc-router] Multiple routers found. Specify \`router\` to choose one:\n${list}`,
230
- );
231
- }
232
- // 0 found: routerPath stays undefined, warn at startup via discovery plugin
233
248
  }
234
249
 
235
250
  const rscOption = nodeOptions.rsc ?? true;
@@ -373,8 +388,8 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
373
388
  },
374
389
  });
375
390
 
376
- // Add virtual entries plugin
377
- plugins.push(createVirtualEntriesPlugin(finalEntries, routerPath));
391
+ // Add virtual entries plugin (RSC entry generated lazily from routerRef)
392
+ plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
378
393
 
379
394
  // Add the RSC plugin directly
380
395
  // Cast to PluginOption to handle type differences between bundled vite types
@@ -384,6 +399,11 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
384
399
  }) as PluginOption,
385
400
  );
386
401
  }
402
+
403
+ // Deduplicate client references from third-party packages in dev mode.
404
+ // Prevents module duplication when server components import "use client"
405
+ // packages that are also imported directly by client components.
406
+ plugins.push(clientRefDedup());
387
407
  }
388
408
 
389
409
  // Fix HMR for "use client" components.
@@ -451,10 +471,13 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
451
471
  // Add version virtual module plugin for cache invalidation
452
472
  plugins.push(createVersionPlugin());
453
473
 
454
- // Entry path for discovery and version injection.
455
- // Node preset: uses the (possibly auto-discovered) router path.
474
+ // Entry path for discovery: user-specified value (if any) or undefined.
475
+ // Auto-discovered path is passed separately via routerRef.
456
476
  // Cloudflare preset: deferred to configResolved (read from resolved Vite env config).
457
- const discoveryEntryPath = preset !== "cloudflare" ? routerPath : undefined;
477
+ const discoveryEntryPath =
478
+ preset !== "cloudflare" ? routerRef.path : undefined;
479
+ // Ref for deferred auto-discovery (node preset only, undefined for cloudflare)
480
+ const discoveryRouterRef = preset !== "cloudflare" ? routerRef : undefined;
458
481
 
459
482
  // Version injector: auto-injects VERSION and routes-manifest into custom entry.rsc files.
460
483
  // Only applies when there's an explicit rscEntryPath or for cloudflare preset (resolved
@@ -472,8 +495,10 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
472
495
 
473
496
  // Router discovery plugin for build-time manifest generation.
474
497
  // For cloudflare, the entry is resolved lazily in configResolved from the RSC environment.
498
+ // For node, discoveryRouterRef provides the auto-discovered path when not user-specified.
475
499
  plugins.push(
476
500
  createRouterDiscoveryPlugin(discoveryEntryPath, {
501
+ routerPathRef: discoveryRouterRef,
477
502
  enableBuildPrerender: prerenderEnabled,
478
503
  staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
479
504
  include: resolvedOptions.include,
@@ -11,6 +11,8 @@ import { createServer as createViteServer } from "vite";
11
11
  import { resolve } from "node:path";
12
12
  import { readFileSync } from "node:fs";
13
13
  import {
14
+ formatNestedRouterConflictError,
15
+ findNestedRouterConflict,
14
16
  findRouterFiles,
15
17
  createScanFilter,
16
18
  } from "../build/generate-route-types.js";
@@ -25,6 +27,7 @@ import { extractHandlerExportsFromChunk } from "./utils/bundle-analysis.js";
25
27
  import {
26
28
  createDiscoveryState,
27
29
  VIRTUAL_ROUTES_MANIFEST_ID,
30
+ type DiscoveryState,
28
31
  type PluginOptions,
29
32
  } from "./discovery/state.js";
30
33
  import { consumeSelfGenWrite } from "./discovery/self-gen-tracking.js";
@@ -39,9 +42,59 @@ import {
39
42
  generatePerRouterModule,
40
43
  } from "./discovery/virtual-module-codegen.js";
41
44
  import { postprocessBundle } from "./discovery/bundle-postprocess.js";
45
+ import { resetStagedBuildAssets } from "./utils/prerender-utils.js";
42
46
 
43
47
  export { VIRTUAL_ROUTES_MANIFEST_ID };
44
48
 
49
+ // ============================================================================
50
+ // Temp Server Factory
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Create a minimal Vite server for router discovery.
55
+ *
56
+ * Both dev-mode prerender and build-mode discovery need a temp RSC server
57
+ * to import user router files via module runner. This factory centralizes
58
+ * the shared config and the mode-specific differences:
59
+ * - Dev: path-based IDs (no forceBuild), separate cacheDir
60
+ * - Build: hashed IDs (forceBuild), hashClientRefs for production bundles
61
+ *
62
+ * Returns the ViteDevServer instance. Callers access .environments.rsc as needed.
63
+ */
64
+ async function createTempRscServer(
65
+ state: DiscoveryState,
66
+ options: { forceBuild?: boolean; cacheDir?: string } = {},
67
+ ) {
68
+ const { default: rsc } = await import("@vitejs/plugin-rsc");
69
+ return createViteServer({
70
+ root: state.projectRoot,
71
+ configFile: false,
72
+ server: { middlewareMode: true },
73
+ appType: "custom",
74
+ logLevel: "silent",
75
+ resolve: { alias: state.userResolveAlias },
76
+ esbuild: { jsx: "automatic", jsxImportSource: "react" },
77
+ ...(options.cacheDir && { cacheDir: options.cacheDir }),
78
+ plugins: [
79
+ rsc({
80
+ entries: {
81
+ client: "virtual:entry-client",
82
+ ssr: "virtual:entry-ssr",
83
+ rsc: state.resolvedEntryPath!,
84
+ },
85
+ }),
86
+ // hashClientRefs only in build mode — production bundles need hashed refs
87
+ ...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
88
+ createVersionPlugin(),
89
+ createVirtualStubPlugin(),
90
+ // Dev prerender must use dev-mode IDs (path-based) to match the workerd
91
+ // runtime. forceBuild produces hashed IDs for production bundle consistency.
92
+ exposeInternalIds(options.forceBuild ? { forceBuild: true } : undefined),
93
+ exposeRouterId(),
94
+ ],
95
+ });
96
+ }
97
+
45
98
  /**
46
99
  * Plugin that discovers router instances at dev/build time via the RSC environment.
47
100
  *
@@ -97,6 +150,12 @@ export function createRouterDiscoveryPlugin(
97
150
  s.isBuildMode = config.command === "build";
98
151
  // Capture user's resolve aliases for the temp server
99
152
  s.userResolveAlias = config.resolve.alias;
153
+ // Node preset: pick up auto-discovered router path from the config() hook.
154
+ // The auto-discover plugin runs in config() using Vite's resolved root,
155
+ // populating the mutable ref before configResolved fires.
156
+ if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
157
+ s.resolvedEntryPath = opts.routerPathRef.path;
158
+ }
100
159
  // Cloudflare preset: read entry from resolved environment config.
101
160
  // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
102
161
  // and sets optimizeDeps.entries on the RSC environment.
@@ -179,32 +238,8 @@ export function createRouterDiscoveryPlugin(
179
238
  return (prerenderTempServer.environments as any)?.rsc ?? null;
180
239
  }
181
240
  try {
182
- const { default: rsc } = await import("@vitejs/plugin-rsc");
183
- prerenderTempServer = await createViteServer({
184
- root: s.projectRoot,
185
- configFile: false,
186
- server: { middlewareMode: true },
187
- appType: "custom",
188
- logLevel: "silent",
241
+ prerenderTempServer = await createTempRscServer(s, {
189
242
  cacheDir: "node_modules/.vite_prerender",
190
- resolve: { alias: s.userResolveAlias },
191
- esbuild: { jsx: "automatic", jsxImportSource: "react" },
192
- plugins: [
193
- rsc({
194
- entries: {
195
- client: "virtual:entry-client",
196
- ssr: "virtual:entry-ssr",
197
- rsc: s.resolvedEntryPath!,
198
- },
199
- }),
200
- createVersionPlugin(),
201
- createVirtualStubPlugin(),
202
- // Dev prerender must use dev-mode IDs (path-based) to match the
203
- // workerd runtime. forceBuild would produce hashed IDs causing
204
- // handle data key mismatches when replayed into the runtime store.
205
- exposeInternalIds(),
206
- exposeRouterId(),
207
- ],
208
243
  });
209
244
 
210
245
  const tempRscEnv = (prerenderTempServer.environments as any)?.rsc;
@@ -260,10 +295,7 @@ export function createRouterDiscoveryPlugin(
260
295
  serverMod.setManifestReadyPromise(discoveryPromise);
261
296
  }
262
297
 
263
- const serverModAfterDiscovery = await discoverRouters(s, rscEnv);
264
-
265
- // Save registry for the /__rsc_prerender endpoint (avoids creating a temp server)
266
- mainRegistry = serverModAfterDiscovery?.RouterRegistry ?? null;
298
+ await discoverRouters(s, rscEnv);
267
299
 
268
300
  // Store server origin for dev prerender endpoint (virtual module injection)
269
301
  s.devServerOrigin = getDevServerOrigin();
@@ -275,37 +307,8 @@ export function createRouterDiscoveryPlugin(
275
307
  // won't cause unnecessary HMR triggers.
276
308
  writeRouteTypesFiles(s);
277
309
 
278
- // Populate the route map in the RSC env
279
- if (s.mergedRouteManifest && serverMod?.setCachedManifest) {
280
- serverMod.setCachedManifest(s.mergedRouteManifest);
281
- }
282
- if (
283
- s.mergedPrecomputedEntries &&
284
- s.mergedPrecomputedEntries.length > 0 &&
285
- serverMod?.setPrecomputedEntries
286
- ) {
287
- serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
288
- }
289
- if (s.mergedRouteTrie && serverMod?.setRouteTrie) {
290
- serverMod.setRouteTrie(s.mergedRouteTrie);
291
- }
292
- // Populate per-router isolated data eagerly in dev (HMR).
293
- // In production builds, per-router data is loaded lazily via import().
294
- if (serverMod?.setRouterManifest) {
295
- for (const [routerId, manifest] of s.perRouterManifestDataMap) {
296
- serverMod.setRouterManifest(routerId, manifest);
297
- }
298
- }
299
- if (serverMod?.setRouterTrie) {
300
- for (const [routerId, trie] of s.perRouterTrieMap) {
301
- serverMod.setRouterTrie(routerId, trie);
302
- }
303
- }
304
- if (serverMod?.setRouterPrecomputedEntries) {
305
- for (const [routerId, entries] of s.perRouterPrecomputedMap) {
306
- serverMod.setRouterPrecomputedEntries(routerId, entries);
307
- }
308
- }
310
+ // Populate the route map and per-router data in the RSC env
311
+ await propagateDiscoveryState(rscEnv);
309
312
  } catch (err: any) {
310
313
  console.warn(
311
314
  `[rsc-router] Router discovery failed: ${err.message}\n${err.stack}`,
@@ -333,6 +336,49 @@ export function createRouterDiscoveryPlugin(
333
336
  // Registry from the main server's RSC environment (populated by discoverRouters)
334
337
  let mainRegistry: Map<string, any> | null = null;
335
338
 
339
+ // Push discovery state (manifest, trie, precomputed entries) to the
340
+ // server module so runtime request handling uses the current routes.
341
+ // Shared by initial discovery and HMR-triggered re-discovery.
342
+ const propagateDiscoveryState = async (rscEnv: any) => {
343
+ const serverMod = await rscEnv.runner.import("@rangojs/router/server");
344
+ if (!serverMod) return;
345
+ // Clear stale per-router and global route data before repopulating.
346
+ // Without this, removed routers/routes survive in the per-router maps
347
+ // and shrunk precomputed entries or tries are never purged.
348
+ if (serverMod.clearAllRouterData) {
349
+ serverMod.clearAllRouterData();
350
+ }
351
+ mainRegistry = serverMod.RouterRegistry ?? null;
352
+ if (s.mergedRouteManifest && serverMod.setCachedManifest) {
353
+ serverMod.setCachedManifest(s.mergedRouteManifest);
354
+ }
355
+ if (
356
+ s.mergedPrecomputedEntries &&
357
+ s.mergedPrecomputedEntries.length > 0 &&
358
+ serverMod.setPrecomputedEntries
359
+ ) {
360
+ serverMod.setPrecomputedEntries(s.mergedPrecomputedEntries);
361
+ }
362
+ if (s.mergedRouteTrie && serverMod.setRouteTrie) {
363
+ serverMod.setRouteTrie(s.mergedRouteTrie);
364
+ }
365
+ if (serverMod.setRouterManifest) {
366
+ for (const [routerId, manifest] of s.perRouterManifestDataMap) {
367
+ serverMod.setRouterManifest(routerId, manifest);
368
+ }
369
+ }
370
+ if (serverMod.setRouterTrie) {
371
+ for (const [routerId, trie] of s.perRouterTrieMap) {
372
+ serverMod.setRouterTrie(routerId, trie);
373
+ }
374
+ }
375
+ if (serverMod.setRouterPrecomputedEntries) {
376
+ for (const [routerId, entries] of s.perRouterPrecomputedMap) {
377
+ serverMod.setRouterPrecomputedEntries(routerId, entries);
378
+ }
379
+ }
380
+ };
381
+
336
382
  server.middlewares.use("/__rsc_prerender", async (req: any, res: any) => {
337
383
  if (s.discoveryDone) await s.discoveryDone;
338
384
 
@@ -364,12 +410,24 @@ export function createRouterDiscoveryPlugin(
364
410
  }
365
411
 
366
412
  const wantIntercept = url.searchParams.get("intercept") === "1";
413
+ const wantRouteName = url.searchParams.get("routeName");
414
+ const wantPassthrough = url.searchParams.get("passthrough") === "1";
367
415
 
368
416
  for (const [, routerInstance] of registry) {
369
417
  if (!routerInstance.matchForPrerender) continue;
370
418
  try {
371
- const result = await routerInstance.matchForPrerender(pathname, {});
419
+ const result = await routerInstance.matchForPrerender(
420
+ pathname,
421
+ {},
422
+ undefined,
423
+ wantPassthrough,
424
+ );
372
425
  if (!result) continue;
426
+ if (result.passthrough) continue;
427
+ // When routeName is specified, only accept a match for that route.
428
+ // This prevents returning the wrong entry when multiple routers
429
+ // have prerenderable routes sharing the same pathname.
430
+ if (wantRouteName && result.routeName !== wantRouteName) continue;
373
431
  res.setHeader("content-type", "application/json");
374
432
  let payload: Record<string, unknown>;
375
433
  if (wantIntercept && result.interceptSegments?.length) {
@@ -417,6 +475,13 @@ export function createRouterDiscoveryPlugin(
417
475
  ): boolean => {
418
476
  if (!isGeneratedRouteFile(filePath)) return false;
419
477
  if (consumeSelfGenWrite(s, filePath)) return true;
478
+ // In Cloudflare dev (no module runner), perRouterManifests is never
479
+ // refreshed after HMR so regenerateGeneratedRouteFiles() would use
480
+ // stale data and revert user edits. Source files own route state;
481
+ // gen files are derived output. Skip regeneration and let the next
482
+ // source-file change rebuild them from the static parser.
483
+ const hasRunner = !!(server.environments as any)?.rsc?.runner;
484
+ if (!hasRunner) return true;
420
485
  regenerateGeneratedRouteFiles();
421
486
  return true;
422
487
  };
@@ -427,6 +492,26 @@ export function createRouterDiscoveryPlugin(
427
492
  // only the expensive regeneration is debounced.
428
493
  let routeChangeTimer: ReturnType<typeof setTimeout> | undefined;
429
494
 
495
+ // Re-run runtime discovery so factory-generated routes that the
496
+ // static parser cannot see are refreshed after source changes.
497
+ let runtimeRediscoveryInProgress = false;
498
+ const refreshRuntimeDiscovery = async () => {
499
+ const rscEnv = (server.environments as any)?.rsc;
500
+ if (!rscEnv?.runner || runtimeRediscoveryInProgress) return;
501
+ runtimeRediscoveryInProgress = true;
502
+ try {
503
+ await discoverRouters(s, rscEnv);
504
+ writeRouteTypesFiles(s);
505
+ await propagateDiscoveryState(rscEnv);
506
+ } catch (err: any) {
507
+ console.warn(
508
+ `[rsc-router] Runtime re-discovery failed: ${err.message}`,
509
+ );
510
+ } finally {
511
+ runtimeRediscoveryInProgress = false;
512
+ }
513
+ };
514
+
430
515
  const scheduleRouteRegeneration = () => {
431
516
  clearTimeout(routeChangeTimer);
432
517
  routeChangeTimer = setTimeout(() => {
@@ -441,6 +526,15 @@ export function createRouterDiscoveryPlugin(
441
526
  `[rsc-router] Route regeneration error: ${err.message}`,
442
527
  );
443
528
  }
529
+ // Async: re-run runtime discovery to refresh factory-generated
530
+ // routes that the static parser cannot resolve.
531
+ if (s.perRouterManifests.length > 0) {
532
+ refreshRuntimeDiscovery().catch((err: any) => {
533
+ console.warn(
534
+ `[rsc-router] Runtime re-discovery error: ${err.message}`,
535
+ );
536
+ });
537
+ }
444
538
  }, 100);
445
539
  };
446
540
 
@@ -468,6 +562,16 @@ export function createRouterDiscoveryPlugin(
468
562
  if (!hasUrls && !hasCreateRouter) return;
469
563
  // Invalidate cache when a router file changes (new router added/removed)
470
564
  if (hasCreateRouter) {
565
+ const nestedRouterConflict = findNestedRouterConflict([
566
+ ...(s.cachedRouterFiles ?? []),
567
+ resolve(filePath),
568
+ ]);
569
+ if (nestedRouterConflict) {
570
+ server.config.logger.error(
571
+ formatNestedRouterConflictError(nestedRouterConflict),
572
+ );
573
+ return;
574
+ }
471
575
  s.cachedRouterFiles = undefined;
472
576
  }
473
577
  scheduleRouteRegeneration();
@@ -483,8 +587,12 @@ export function createRouterDiscoveryPlugin(
483
587
  server.watcher.on("change", handleRouteFileChange);
484
588
 
485
589
  // Regenerate gen files when they are deleted (e.g. manual cleanup).
590
+ // Same no-runner guard as change/add: stale perRouterManifests would
591
+ // reintroduce reverted content.
486
592
  server.watcher.on("unlink", (filePath) => {
487
593
  if (!isGeneratedRouteFile(filePath)) return;
594
+ const hasRunner = !!(server.environments as any)?.rsc?.runner;
595
+ if (!hasRunner) return;
488
596
  regenerateGeneratedRouteFiles();
489
597
  });
490
598
  }
@@ -497,6 +605,9 @@ export function createRouterDiscoveryPlugin(
497
605
  if (!s.isBuildMode) return;
498
606
  // Only run once across environment builds
499
607
  if (s.mergedRouteManifest !== null) return;
608
+ resetStagedBuildAssets(s.projectRoot);
609
+ s.prerenderManifestEntries = null;
610
+ s.staticManifestEntries = null;
500
611
 
501
612
  let tempServer: any = null;
502
613
  // Signal to user-space code (e.g. reverse.ts) that build-time discovery
@@ -505,45 +616,7 @@ export function createRouterDiscoveryPlugin(
505
616
  // between the vite plugin and user code loaded via runner.import().
506
617
  (globalThis as any).__rscRouterDiscoveryActive = true;
507
618
  try {
508
- // Create a minimal Vite server with just the RSC plugin.
509
- // We bypass the user's config file because:
510
- // - Custom environments (e.g., CloudflareDevEnvironment) may not expose
511
- // a module runner compatible with runner.import()
512
- // - The temp server only needs RSC conditions to import the router
513
- const { default: rsc } = await import("@vitejs/plugin-rsc");
514
- tempServer = await createViteServer({
515
- root: s.projectRoot,
516
- configFile: false,
517
- server: { middlewareMode: true },
518
- appType: "custom",
519
- logLevel: "silent",
520
- // Use the resolved aliases from the real config (includes user's path aliases
521
- // like @/ -> src/ AND package aliases from rsc-router)
522
- resolve: { alias: s.userResolveAlias },
523
- // Enable automatic JSX runtime so .tsx files don't need `import React`.
524
- // Without this, esbuild defaults to classic mode (React.createElement)
525
- // which fails when lazy host-router handlers load sub-app modules with JSX.
526
- esbuild: { jsx: "automatic", jsxImportSource: "react" },
527
- plugins: [
528
- rsc({
529
- entries: {
530
- client: "virtual:entry-client",
531
- ssr: "virtual:entry-ssr",
532
- rsc: s.resolvedEntryPath!,
533
- },
534
- }),
535
- hashClientRefs(s.projectRoot),
536
- createVersionPlugin(),
537
- // Stub virtual modules that the RSC entry may import
538
- // (e.g., virtual:rsc-router/routes-manifest, virtual:rsc-router/loader-manifest)
539
- createVirtualStubPlugin(),
540
- // Inject handle + router IDs so prerender-collected handle data uses
541
- // the same hashed keys as the production client/SSR bundles, and
542
- // build-time router IDs match runtime IDs across environments.
543
- exposeInternalIds({ forceBuild: true }),
544
- exposeRouterId(),
545
- ],
546
- });
619
+ tempServer = await createTempRscServer(s, { forceBuild: true });
547
620
 
548
621
  const rscEnv = (tempServer.environments as any)?.rsc;
549
622
  if (!rscEnv?.runner) {
@@ -1,3 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ rmSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { resolve } from "node:path";
11
+
1
12
  /**
2
13
  * Escape special RegExp characters in a string for safe interpolation
3
14
  * into new RegExp() patterns.
@@ -17,6 +28,27 @@ export function encodePathParam(value: unknown): string {
17
28
  .join("/");
18
29
  }
19
30
 
31
+ /**
32
+ * Substitute route params into a pattern, stripping constraint and optional
33
+ * syntax (:param(a|b)? -> value). Also handles wildcard params (*key).
34
+ */
35
+ export function substituteRouteParams(
36
+ pattern: string,
37
+ params: Record<string, string>,
38
+ encode: (value: string) => string = encodeURIComponent,
39
+ ): string {
40
+ let result = pattern;
41
+ for (const [key, value] of Object.entries(params)) {
42
+ const escaped = escapeRegExp(key);
43
+ result = result.replace(
44
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
45
+ encode(value),
46
+ );
47
+ result = result.replace(`*${key}`, encode(value));
48
+ }
49
+ return result;
50
+ }
51
+
20
52
  /**
21
53
  * Run an async function over items with bounded concurrency.
22
54
  * Errors propagate immediately and abort remaining work.
@@ -106,3 +138,52 @@ export function notifyOnError(
106
138
  break; // Only notify the first router with onError
107
139
  }
108
140
  }
141
+
142
+ function getStagedAssetDir(projectRoot: string): string {
143
+ return resolve(projectRoot, "node_modules/.rangojs-router-build/rsc-assets");
144
+ }
145
+
146
+ export function resetStagedBuildAssets(projectRoot: string): void {
147
+ rmSync(getStagedAssetDir(projectRoot), { recursive: true, force: true });
148
+ }
149
+
150
+ export function stageBuildAssetModule(
151
+ projectRoot: string,
152
+ prefix: "__pr" | "__st",
153
+ exportValue: string,
154
+ ): string {
155
+ const stagedDir = getStagedAssetDir(projectRoot);
156
+ mkdirSync(stagedDir, { recursive: true });
157
+
158
+ const contentHash = createHash("sha256")
159
+ .update(exportValue)
160
+ .digest("hex")
161
+ .slice(0, 8);
162
+ const fileName = `${prefix}-${contentHash}.js`;
163
+ const filePath = resolve(stagedDir, fileName);
164
+
165
+ if (!existsSync(filePath)) {
166
+ writeFileSync(filePath, `export default ${exportValue};\n`);
167
+ }
168
+
169
+ return fileName;
170
+ }
171
+
172
+ export function copyStagedBuildAssets(
173
+ projectRoot: string,
174
+ fileNames: Iterable<string>,
175
+ ): number {
176
+ const stagedDir = getStagedAssetDir(projectRoot);
177
+ const distAssetsDir = resolve(projectRoot, "dist/rsc/assets");
178
+ mkdirSync(distAssetsDir, { recursive: true });
179
+
180
+ let totalBytes = 0;
181
+ for (const fileName of new Set(fileNames)) {
182
+ const stagedPath = resolve(stagedDir, fileName);
183
+ const distPath = resolve(distAssetsDir, fileName);
184
+ copyFileSync(stagedPath, distPath);
185
+ totalBytes += statSync(stagedPath).size;
186
+ }
187
+
188
+ return totalBytes;
189
+ }