@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
@@ -6,9 +6,9 @@
6
6
  */
7
7
 
8
8
  import { resolve } from "node:path";
9
- import { createHash } from "node:crypto";
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
11
10
  import { evictHandlerCode } from "../utils/bundle-analysis.js";
11
+ import { copyStagedBuildAssets } from "../utils/prerender-utils.js";
12
12
  import type { DiscoveryState } from "./state.js";
13
13
 
14
14
  /**
@@ -17,11 +17,11 @@ import type { DiscoveryState } from "./state.js";
17
17
  */
18
18
  export function postprocessBundle(state: DiscoveryState): void {
19
19
  const hasPrerenderData =
20
- state.prerenderCollectedData &&
21
- Object.keys(state.prerenderCollectedData).length > 0;
20
+ state.prerenderManifestEntries &&
21
+ Object.keys(state.prerenderManifestEntries).length > 0;
22
22
  const hasStaticData =
23
- state.staticCollectedData &&
24
- Object.keys(state.staticCollectedData).length > 0;
23
+ state.staticManifestEntries &&
24
+ Object.keys(state.staticManifestEntries).length > 0;
25
25
  if (!hasPrerenderData && !hasStaticData) return;
26
26
 
27
27
  // Find RSC entry (recorded in generateBundle, fallback to dist/rsc/index.js)
@@ -88,36 +88,30 @@ export function postprocessBundle(state: DiscoveryState): void {
88
88
  state.staticHandlerChunkInfo = null;
89
89
 
90
90
  // 2. Write prerender data as separate importable asset modules
91
- // and inject a manifest import into the RSC entry.
91
+ // and inject a lazy manifest loader into the RSC entry.
92
92
  if (hasPrerenderData && existsSync(rscEntryPath)) {
93
93
  const rscCode = readFileSync(rscEntryPath, "utf-8");
94
- if (!rscCode.includes("__PRERENDER_MANIFEST")) {
94
+ // Check for the specific injection marker to avoid double-injection.
95
+ if (!rscCode.includes("__prerender-manifest.js")) {
95
96
  try {
96
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
97
- mkdirSync(assetsDir, { recursive: true });
98
-
99
- const manifestEntries: string[] = [];
100
- let totalBytes = 0;
97
+ let totalBytes = copyStagedBuildAssets(
98
+ state.projectRoot,
99
+ Object.values(state.prerenderManifestEntries!),
100
+ );
101
101
 
102
- for (const [key, entry] of Object.entries(
103
- state.prerenderCollectedData!,
102
+ const manifestMap: Record<string, string> = {};
103
+ for (const [key, assetFileName] of Object.entries(
104
+ state.prerenderManifestEntries!,
104
105
  )) {
105
- const entryJson = JSON.stringify(entry);
106
- const contentHash = createHash("sha256")
107
- .update(entryJson)
108
- .digest("hex")
109
- .slice(0, 8);
110
- const assetFileName = `__pr-${contentHash}.js`;
111
- const assetPath = resolve(assetsDir, assetFileName);
112
- const assetCode = `export default ${entryJson};\n`;
113
- writeFileSync(assetPath, assetCode);
114
- totalBytes += Buffer.byteLength(assetCode);
115
- manifestEntries.push(
116
- `${JSON.stringify(key)}:()=>import("./assets/${assetFileName}")`,
117
- );
106
+ manifestMap[key] = `./assets/${assetFileName}`;
118
107
  }
119
108
 
120
- const manifestCode = `const m={${manifestEntries.join(",")}};export default m;\n`;
109
+ const manifestCode = [
110
+ `const m=JSON.parse('${JSON.stringify(manifestMap).replace(/'/g, "\\'")}');`,
111
+ `export function loadPrerenderAsset(s){return import(s)}`,
112
+ `export default m;`,
113
+ "",
114
+ ].join("\n");
121
115
  const manifestPath = resolve(
122
116
  state.projectRoot,
123
117
  "dist/rsc/__prerender-manifest.js",
@@ -125,12 +119,12 @@ export function postprocessBundle(state: DiscoveryState): void {
125
119
  writeFileSync(manifestPath, manifestCode);
126
120
  totalBytes += Buffer.byteLength(manifestCode);
127
121
 
128
- const injection = `import __pm from "./__prerender-manifest.js";\nglobalThis.__PRERENDER_MANIFEST = __pm;\n`;
122
+ const injection = `globalThis.__loadPrerenderManifestModule = () => import("./__prerender-manifest.js");\n`;
129
123
  writeFileSync(rscEntryPath, injection + rscCode);
130
124
 
131
125
  const totalKB = (totalBytes / 1024).toFixed(1);
132
126
  console.log(
133
- `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderCollectedData!).length} entries)`,
127
+ `[rsc-router] Wrote prerender assets (${totalKB} KB total, ${Object.keys(state.prerenderManifestEntries!).length} entries)`,
134
128
  );
135
129
  } catch (err: any) {
136
130
  throw new Error(
@@ -146,29 +140,15 @@ export function postprocessBundle(state: DiscoveryState): void {
146
140
  const rscCode = readFileSync(rscEntryPath, "utf-8");
147
141
  if (!rscCode.includes("__STATIC_MANIFEST")) {
148
142
  try {
149
- const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
150
- mkdirSync(assetsDir, { recursive: true });
151
-
152
143
  const manifestEntries: string[] = [];
153
- let totalBytes = 0;
144
+ let totalBytes = copyStagedBuildAssets(
145
+ state.projectRoot,
146
+ Object.values(state.staticManifestEntries!),
147
+ );
154
148
 
155
- for (const [handlerId, { encoded, handles }] of Object.entries(
156
- state.staticCollectedData!,
149
+ for (const [handlerId, assetFileName] of Object.entries(
150
+ state.staticManifestEntries!,
157
151
  )) {
158
- const contentHash = createHash("sha256")
159
- .update(encoded)
160
- .digest("hex")
161
- .slice(0, 8);
162
- const assetFileName = `__st-${contentHash}.js`;
163
- const assetPath = resolve(assetsDir, assetFileName);
164
- // Store both the Flight payload and handle data
165
- const hasHandles = Object.keys(handles).length > 0;
166
- const exportValue = hasHandles
167
- ? JSON.stringify({ encoded, handles })
168
- : JSON.stringify(encoded);
169
- const assetCode = `export default ${exportValue};\n`;
170
- writeFileSync(assetPath, assetCode);
171
- totalBytes += Buffer.byteLength(assetCode);
172
152
  manifestEntries.push(
173
153
  `${JSON.stringify(handlerId)}:()=>import("./assets/${assetFileName}")`,
174
154
  );
@@ -192,7 +172,7 @@ export function postprocessBundle(state: DiscoveryState): void {
192
172
 
193
173
  const totalKB = (totalBytes / 1024).toFixed(1);
194
174
  console.log(
195
- `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticCollectedData!).length} entries)`,
175
+ `[rsc-router] Wrote static assets (${totalKB} KB total, ${Object.keys(state.staticManifestEntries!).length} entries)`,
196
176
  );
197
177
  } catch (err: any) {
198
178
  throw new Error(
@@ -6,7 +6,11 @@
6
6
  * router, and builds route tries for O(path_length) matching.
7
7
  */
8
8
 
9
- import { buildCombinedRouteMapForRouterFile } from "../../build/generate-route-types.js";
9
+ import {
10
+ buildCombinedRouteMapForRouterFile,
11
+ formatNestedRouterConflictError,
12
+ findNestedRouterConflict,
13
+ } from "../../build/generate-route-types.js";
10
14
  import {
11
15
  flattenLeafEntries,
12
16
  buildRouteToStaticPrefix,
@@ -44,9 +48,8 @@ export async function discoverRouters(
44
48
  // No RSC routers found directly. Check for host routers with lazy handlers
45
49
  // that need to be resolved to trigger sub-app createRouter() calls.
46
50
  try {
47
- const hostMod = await rscEnv.runner.import("@rangojs/router/host");
48
51
  const hostRegistry: Map<string, any> | undefined =
49
- hostMod.HostRouterRegistry;
52
+ serverMod.HostRouterRegistry;
50
53
 
51
54
  if (hostRegistry && hostRegistry.size > 0) {
52
55
  console.log(
@@ -85,7 +88,7 @@ export async function discoverRouters(
85
88
  }
86
89
  }
87
90
  } catch {
88
- // @rangojs/router/host not available or import failed, skip
91
+ // Host-router discovery is best-effort; skip if unavailable
89
92
  }
90
93
 
91
94
  // If still no routers after host router resolution, fail
@@ -98,14 +101,28 @@ export async function discoverRouters(
98
101
 
99
102
  // Import build utilities for manifest generation
100
103
  const buildMod = await rscEnv.runner.import("@rangojs/router/build");
101
- const generateManifest = buildMod.generateManifest;
102
-
103
- state.mergedRouteManifest = {};
104
- state.mergedPrecomputedEntries = [];
105
- state.perRouterManifests = [];
106
- state.perRouterManifestDataMap = new Map();
107
- state.perRouterPrecomputedMap = new Map();
108
- state.perRouterTrieMap = new Map();
104
+ const generateManifestFull = buildMod.generateManifestFull;
105
+
106
+ const nestedRouterConflict = findNestedRouterConflict(
107
+ [...registry.values()]
108
+ .map((router) => router.__sourceFile)
109
+ .filter(
110
+ (sourceFile): sourceFile is string => typeof sourceFile === "string",
111
+ ),
112
+ );
113
+ if (nestedRouterConflict) {
114
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
115
+ }
116
+
117
+ // Build into local variables first. Only commit to state after the
118
+ // full pass succeeds, so a failed re-discovery preserves the last
119
+ // known-good state instead of leaving it partially wiped.
120
+ const newMergedRouteManifest: Record<string, string> = {};
121
+ const newMergedPrecomputedEntries: PrecomputedEntry[] = [];
122
+ const newPerRouterManifests: typeof state.perRouterManifests = [];
123
+ const newPerRouterManifestDataMap = new Map<string, any>();
124
+ const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
125
+ const newPerRouterTrieMap = new Map<string, any>();
109
126
  let mergedRouteAncestry: Record<string, string[]> = {};
110
127
  let mergedRouteTrailingSlash: Record<string, string> = {};
111
128
 
@@ -114,11 +131,11 @@ export async function discoverRouters(
114
131
  const allManifests: Array<{ id: string; manifest: any }> = [];
115
132
 
116
133
  for (const [id, router] of registry) {
117
- if (!router.urlpatterns || !generateManifest) {
134
+ if (!router.urlpatterns || !generateManifestFull) {
118
135
  continue;
119
136
  }
120
137
 
121
- const manifest = generateManifest(router.urlpatterns, routerMountIndex);
138
+ const manifest = generateManifestFull(router.urlpatterns, routerMountIndex);
122
139
  routerMountIndex++;
123
140
  allManifests.push({ id, manifest });
124
141
  const routeCount = Object.keys(manifest.routeManifest).length;
@@ -128,7 +145,7 @@ export async function discoverRouters(
128
145
  const dynamicRoutes = routeCount - staticRoutes;
129
146
 
130
147
  // Merge into the combined manifest
131
- Object.assign(state.mergedRouteManifest, manifest.routeManifest);
148
+ Object.assign(newMergedRouteManifest, manifest.routeManifest);
132
149
 
133
150
  // Compute factory-only prefixes: dot-prefixed groups in the runtime
134
151
  // manifest that the static parser cannot see. These are routes created
@@ -152,7 +169,7 @@ export async function discoverRouters(
152
169
  if (factoryOnlyPrefixes.size === 0) factoryOnlyPrefixes = undefined;
153
170
  }
154
171
 
155
- state.perRouterManifests.push({
172
+ newPerRouterManifests.push({
156
173
  id,
157
174
  routeManifest: manifest.routeManifest,
158
175
  routeSearchSchemas: manifest.routeSearchSchemas,
@@ -175,18 +192,18 @@ export async function discoverRouters(
175
192
  flattenLeafEntries(
176
193
  manifest.prefixTree,
177
194
  manifest.routeManifest,
178
- state.mergedPrecomputedEntries,
195
+ newMergedPrecomputedEntries,
179
196
  );
180
197
 
181
198
  // Store per-router manifest and precomputed entries for isolated virtual modules.
182
- state.perRouterManifestDataMap.set(id, manifest.routeManifest);
199
+ newPerRouterManifestDataMap.set(id, manifest.routeManifest);
183
200
  const routerPrecomputed: PrecomputedEntry[] = [];
184
201
  flattenLeafEntries(
185
202
  manifest.prefixTree,
186
203
  manifest.routeManifest,
187
204
  routerPrecomputed,
188
205
  );
189
- state.perRouterPrecomputedMap.set(id, routerPrecomputed);
206
+ newPerRouterPrecomputedMap.set(id, routerPrecomputed);
190
207
 
191
208
  console.log(
192
209
  `[rsc-router] Router "${id}" -> ${routeCount} routes ` +
@@ -214,10 +231,8 @@ export async function discoverRouters(
214
231
  }
215
232
 
216
233
  // Build route trie from merged manifest + ancestry
217
- if (
218
- state.mergedRouteManifest &&
219
- Object.keys(state.mergedRouteManifest).length > 0
220
- ) {
234
+ let newMergedRouteTrie: any = null;
235
+ if (Object.keys(newMergedRouteManifest).length > 0) {
221
236
  const buildRouteTrie = buildMod.buildRouteTrie;
222
237
  if (buildRouteTrie && mergedRouteAncestry) {
223
238
  // Build routeToStaticPrefix from saved manifests
@@ -252,8 +267,8 @@ export async function discoverRouters(
252
267
  }
253
268
  }
254
269
 
255
- state.mergedRouteTrie = buildRouteTrie(
256
- state.mergedRouteManifest,
270
+ newMergedRouteTrie = buildRouteTrie(
271
+ newMergedRouteManifest,
257
272
  mergedRouteAncestry,
258
273
  routeToStaticPrefix,
259
274
  Object.keys(mergedRouteTrailingSlash).length > 0
@@ -305,11 +320,22 @@ export async function discoverRouters(
305
320
  ? manifest.responseTypeRoutes
306
321
  : undefined,
307
322
  );
308
- state.perRouterTrieMap.set(id, perRouterTrie);
323
+ newPerRouterTrieMap.set(id, perRouterTrie);
309
324
  }
310
325
  }
311
326
  }
312
327
 
328
+ // Commit all local state to the shared discovery state atomically.
329
+ // This ensures a failed re-discovery (e.g. from a transient module
330
+ // evaluation error) preserves the last known-good state.
331
+ state.mergedRouteManifest = newMergedRouteManifest;
332
+ state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
333
+ state.perRouterManifests = newPerRouterManifests;
334
+ state.perRouterManifestDataMap = newPerRouterManifestDataMap;
335
+ state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
336
+ state.perRouterTrieMap = newPerRouterTrieMap;
337
+ state.mergedRouteTrie = newMergedRouteTrie;
338
+
313
339
  // Expand prerender routes and render static handlers (build mode only)
314
340
  await expandPrerenderRoutes(state, rscEnv, registry, allManifests);
315
341
  await renderStaticHandlers(state, rscEnv, registry);
@@ -9,16 +9,18 @@
9
9
  import { contextSet } from "../../context-var.js";
10
10
  import {
11
11
  encodePathParam,
12
- escapeRegExp,
12
+ substituteRouteParams,
13
13
  runWithConcurrency,
14
14
  groupByConcurrency,
15
15
  notifyOnError,
16
+ stageBuildAssetModule,
16
17
  } from "../utils/prerender-utils.js";
17
18
  import type { DiscoveryState } from "./state.js";
18
19
 
19
20
  /**
20
21
  * Expand prerender routes into concrete URLs and render them via the
21
- * RSC runner. Stores collected data in state.prerenderCollectedData.
22
+ * RSC runner. Stages asset modules and stores key-to-file entries in
23
+ * state.prerenderManifestEntries.
22
24
  */
23
25
  export async function expandPrerenderRoutes(
24
26
  state: DiscoveryState,
@@ -33,6 +35,7 @@ export async function expandPrerenderRoutes(
33
35
  routeName: string;
34
36
  concurrency: number;
35
37
  buildVars?: Record<string, any>;
38
+ isPassthroughRoute?: boolean;
36
39
  };
37
40
  const entries: PrerenderEntry[] = [];
38
41
 
@@ -44,19 +47,8 @@ export async function expandPrerenderRoutes(
44
47
  const getParamsReverse = (name: string, params?: Record<string, string>) => {
45
48
  const pattern = allRoutes[name];
46
49
  if (!pattern) throw new Error(`Unknown route: "${name}"`);
47
- let result = pattern;
48
- if (params) {
49
- for (const [key, value] of Object.entries(params)) {
50
- // Strip constraint syntax: :param(a|b) -> value
51
- const escaped = escapeRegExp(key);
52
- result = result.replace(
53
- new RegExp(`:${escaped}(\\([^)]*\\))?`),
54
- encodeURIComponent(value),
55
- );
56
- result = result.replace(`*${key}`, encodeURIComponent(value));
57
- }
58
- }
59
- return result;
50
+ if (!params) return pattern;
51
+ return substituteRouteParams(pattern, params);
60
52
  };
61
53
 
62
54
  for (const { manifest } of allManifests) {
@@ -65,6 +57,8 @@ export async function expandPrerenderRoutes(
65
57
  for (const routeName of manifest.prerenderRoutes) {
66
58
  const pattern = manifest.routeManifest[routeName];
67
59
  if (!pattern) continue;
60
+ const def = defs[routeName];
61
+ const isPassthroughRoute = !!def?.options?.passthrough;
68
62
  const hasDynamic = pattern.includes(":") || pattern.includes("*");
69
63
  if (!hasDynamic) {
70
64
  // Static route: use pattern directly (strip trailing slash for URL)
@@ -72,10 +66,10 @@ export async function expandPrerenderRoutes(
72
66
  urlPath: pattern.replace(/\/$/, "") || "/",
73
67
  routeName,
74
68
  concurrency: 1,
69
+ isPassthroughRoute,
75
70
  });
76
71
  } else {
77
72
  // Dynamic route: call getParams() to enumerate param combinations
78
- const def = defs[routeName];
79
73
  if (def?.getParams) {
80
74
  try {
81
75
  const buildVars: Record<string, any> = {};
@@ -92,19 +86,11 @@ export async function expandPrerenderRoutes(
92
86
  Object.keys(buildVars).length > 0 ||
93
87
  Object.getOwnPropertySymbols(buildVars).length > 0;
94
88
  for (const params of paramsList) {
95
- let url = pattern;
96
- for (const [key, value] of Object.entries(
89
+ let url = substituteRouteParams(
90
+ pattern,
97
91
  params as Record<string, string>,
98
- )) {
99
- const encoded = encodePathParam(value);
100
- // Strip constraint syntax: :param(a|b) -> value
101
- const escaped = escapeRegExp(key);
102
- url = url.replace(
103
- new RegExp(`:${escaped}(\\([^)]*\\))?`),
104
- encoded,
105
- );
106
- url = url.replace(`*${key}`, encoded);
107
- }
92
+ encodePathParam,
93
+ );
108
94
  // Anonymous wildcard fallback: use conventional keys if provided
109
95
  if (url.includes("*")) {
110
96
  const wildcardValue =
@@ -119,6 +105,7 @@ export async function expandPrerenderRoutes(
119
105
  routeName,
120
106
  concurrency,
121
107
  ...(hasBuildVars ? { buildVars } : {}),
108
+ isPassthroughRoute,
122
109
  });
123
110
  }
124
111
  } catch (err: any) {
@@ -165,7 +152,7 @@ export async function expandPrerenderRoutes(
165
152
 
166
153
  const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
167
154
 
168
- const collectedData: Record<string, any> = {};
155
+ const manifestEntries: Record<string, string> = {};
169
156
  let doneCount = 0;
170
157
  let skipCount = 0;
171
158
  const startTotal = performance.now();
@@ -187,21 +174,45 @@ export async function expandPrerenderRoutes(
187
174
  entry.urlPath,
188
175
  {},
189
176
  entry.buildVars,
177
+ entry.isPassthroughRoute,
190
178
  );
191
179
  if (!result) continue;
180
+
181
+ // Handler returned ctx.passthrough() — skip manifest entry
182
+ if (result.passthrough) {
183
+ const elapsed = (performance.now() - startUrl).toFixed(0);
184
+ console.log(
185
+ `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
186
+ );
187
+ doneCount++;
188
+ break;
189
+ }
190
+
192
191
  const paramHash = hashParams(result.params || {});
193
- collectedData[`${result.routeName}/${paramHash}`] = {
192
+ const mainKey = `${result.routeName}/${paramHash}`;
193
+ const mainValue = JSON.stringify({
194
194
  segments: result.segments,
195
195
  handles: result.handles,
196
- };
196
+ });
197
+ manifestEntries[mainKey] = stageBuildAssetModule(
198
+ state.projectRoot,
199
+ "__pr",
200
+ mainValue,
201
+ );
197
202
  if (result.interceptSegments?.length) {
198
- collectedData[`${result.routeName}/${paramHash}/i`] = {
203
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
204
+ const interceptValue = JSON.stringify({
199
205
  segments: [...result.segments, ...result.interceptSegments],
200
206
  handles: {
201
207
  ...result.handles,
202
208
  ...(result.interceptHandles || {}),
203
209
  },
204
- };
210
+ });
211
+ manifestEntries[interceptKey] = stageBuildAssetModule(
212
+ state.projectRoot,
213
+ "__pr",
214
+ interceptValue,
215
+ );
205
216
  }
206
217
  const elapsed = (performance.now() - startUrl).toFixed(0);
207
218
  console.log(
@@ -247,7 +258,7 @@ export async function expandPrerenderRoutes(
247
258
 
248
259
  const totalElapsed = (performance.now() - startTotal).toFixed(0);
249
260
  if (doneCount > 0) {
250
- state.prerenderCollectedData = collectedData;
261
+ state.prerenderManifestEntries = manifestEntries;
251
262
  }
252
263
  const parts = [`${doneCount} done`];
253
264
  if (skipCount > 0) parts.push(`${skipCount} skipped`);
@@ -259,7 +270,8 @@ export async function expandPrerenderRoutes(
259
270
  /**
260
271
  * Render Static handlers at build time. Each Static handler is called
261
272
  * with a synthetic BuildContext and its output is RSC-serialized.
262
- * Stores collected data in state.staticCollectedData.
273
+ * Stages asset modules and stores handlerId-to-file entries in
274
+ * state.staticManifestEntries.
263
275
  */
264
276
  export async function renderStaticHandlers(
265
277
  state: DiscoveryState,
@@ -273,10 +285,7 @@ export async function renderStaticHandlers(
273
285
  )
274
286
  return;
275
287
 
276
- const collected: Record<
277
- string,
278
- { encoded: string; handles: Record<string, unknown[]> }
279
- > = {};
288
+ const manifestEntries: Record<string, string> = {};
280
289
  let staticDone = 0;
281
290
  let staticSkip = 0;
282
291
  let totalStaticCount = 0;
@@ -319,7 +328,15 @@ export async function renderStaticHandlers(
319
328
  (def as any).$$routePrefix,
320
329
  );
321
330
  if (result) {
322
- collected[def.$$id] = result;
331
+ const hasHandles = Object.keys(result.handles).length > 0;
332
+ const exportValue = hasHandles
333
+ ? JSON.stringify(result)
334
+ : JSON.stringify(result.encoded);
335
+ manifestEntries[def.$$id] = stageBuildAssetModule(
336
+ state.projectRoot,
337
+ "__st",
338
+ exportValue,
339
+ );
323
340
  const elapsed = (performance.now() - startHandler).toFixed(0);
324
341
  console.log(
325
342
  `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
@@ -358,7 +375,7 @@ export async function renderStaticHandlers(
358
375
 
359
376
  const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
360
377
  if (staticDone > 0) {
361
- state.staticCollectedData = collected;
378
+ state.staticManifestEntries = manifestEntries;
362
379
  }
363
380
  const staticParts = [`${staticDone} done`];
364
381
  if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
@@ -15,20 +15,20 @@ import {
15
15
  } from "../../build/generate-route-types.js";
16
16
  import type { DiscoveryState } from "./state.js";
17
17
  import { markSelfGenWrite } from "./self-gen-tracking.js";
18
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
18
19
 
19
20
  /**
20
21
  * Filter out auto-generated route names from a manifest.
21
- * Routes with names starting with "$" are internal (auto-generated from
22
- * patterns for unnamed routes) and should not appear in the typed gen file.
23
- * This keeps the runtime writer's output consistent with the static parser,
24
- * which never produces these names.
22
+ * Unnamed routes get "$path_"-prefixed names at runtime (see path-helper.ts).
23
+ * These should not appear in the typed gen file. User-defined names
24
+ * containing "$" (e.g. "$admin") are valid and preserved.
25
25
  */
26
26
  function filterUserNamedRoutes(
27
27
  manifest: Record<string, string>,
28
28
  ): Record<string, string> {
29
29
  const filtered: Record<string, string> = {};
30
30
  for (const [name, pattern] of Object.entries(manifest)) {
31
- if (!name.startsWith("$")) {
31
+ if (!isAutoGeneratedRouteName(name)) {
32
32
  filtered[name] = pattern;
33
33
  }
34
34
  }
@@ -222,8 +222,8 @@ export function supplementGenFilesWithRuntimeRoutes(
222
222
  };
223
223
 
224
224
  for (const [name, pattern] of Object.entries(routeManifest)) {
225
- // Skip auto-generated names
226
- if (name.startsWith("$")) continue;
225
+ // Skip internal runtime-only names from unnamed routes/includes.
226
+ if (isAutoGeneratedRouteName(name)) continue;
227
227
  const dotIdx = name.indexOf(".");
228
228
  if (dotIdx <= 0) continue;
229
229
  const prefix = name.substring(0, dotIdx + 1);
@@ -15,6 +15,9 @@ export interface PluginOptions {
15
15
  staticRouteTypesGeneration?: boolean;
16
16
  include?: string[];
17
17
  exclude?: string[];
18
+ // Mutable ref for deferred auto-discovery (node preset).
19
+ // The auto-discover config() hook populates this before configResolved.
20
+ routerPathRef?: { path?: string };
18
21
  }
19
22
 
20
23
  export interface PrecomputedEntry {
@@ -53,11 +56,8 @@ export interface DiscoveryState {
53
56
  perRouterPrecomputedMap: Map<string, PrecomputedEntry[]>;
54
57
  perRouterManifestDataMap: Map<string, Record<string, string>>;
55
58
 
56
- prerenderCollectedData: Record<string, any> | null;
57
- staticCollectedData: Record<
58
- string,
59
- { encoded: string; handles: Record<string, unknown[]> }
60
- > | null;
59
+ prerenderManifestEntries: Record<string, string> | null;
60
+ staticManifestEntries: Record<string, string> | null;
61
61
  handlerChunkInfo: ChunkInfo | null;
62
62
  staticHandlerChunkInfo: ChunkInfo | null;
63
63
  rscEntryFileName: string | null;
@@ -93,8 +93,8 @@ export function createDiscoveryState(
93
93
  perRouterPrecomputedMap: new Map(),
94
94
  perRouterManifestDataMap: new Map(),
95
95
 
96
- prerenderCollectedData: null,
97
- staticCollectedData: null,
96
+ prerenderManifestEntries: null,
97
+ staticManifestEntries: null,
98
98
  handlerChunkInfo: null,
99
99
  staticHandlerChunkInfo: null,
100
100
  rscEntryFileName: null,
@@ -42,7 +42,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
42
42
  const genPath = join(
43
43
  routerDir,
44
44
  `${routerBasename}.named-routes.gen.js`,
45
- );
45
+ ).replaceAll("\\", "/");
46
46
  const varName = `_r${varIdx++}`;
47
47
  genFileImports.push(
48
48
  `import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`,
@@ -176,7 +176,10 @@ export function generatePerRouterModule(
176
176
  /\.(tsx?|jsx?)$/,
177
177
  "",
178
178
  );
179
- const genPath = join(routerDir, `${routerBasename}.named-routes.gen.js`);
179
+ const genPath = join(
180
+ routerDir,
181
+ `${routerBasename}.named-routes.gen.js`,
182
+ ).replaceAll("\\", "/");
180
183
  lines.push(`import { NamedRoutes as _r } from ${JSON.stringify(genPath)};`);
181
184
  lines.push(
182
185
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,