@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Prerender Collection
3
+ *
4
+ * Expands prerender routes into concrete URLs and renders them at build
5
+ * time. Also handles Static handler rendering for segment-level static
6
+ * generation.
7
+ */
8
+
9
+ import { contextSet } from "../../context-var.js";
10
+ import {
11
+ encodePathParam,
12
+ substituteRouteParams,
13
+ runWithConcurrency,
14
+ groupByConcurrency,
15
+ notifyOnError,
16
+ stageBuildAssetModule,
17
+ } from "../utils/prerender-utils.js";
18
+ import type { DiscoveryState } from "./state.js";
19
+
20
+ /**
21
+ * Expand prerender routes into concrete URLs and render them via the
22
+ * RSC runner. Stages asset modules and stores key-to-file entries in
23
+ * state.prerenderManifestEntries.
24
+ */
25
+ export async function expandPrerenderRoutes(
26
+ state: DiscoveryState,
27
+ rscEnv: any,
28
+ registry: Map<string, any>,
29
+ allManifests: Array<{ id: string; manifest: any }>,
30
+ ): Promise<void> {
31
+ if (!state.opts?.enableBuildPrerender || !state.isBuildMode) return;
32
+
33
+ type PrerenderEntry = {
34
+ urlPath: string;
35
+ routeName: string;
36
+ concurrency: number;
37
+ buildVars?: Record<string, any>;
38
+ isPassthroughRoute?: boolean;
39
+ };
40
+ const entries: PrerenderEntry[] = [];
41
+
42
+ // Build a merged route map for getParams context reverse()
43
+ const allRoutes: Record<string, string> = {};
44
+ for (const { manifest: m } of allManifests) {
45
+ if (m.routeManifest) Object.assign(allRoutes, m.routeManifest);
46
+ }
47
+ const getParamsReverse = (name: string, params?: Record<string, string>) => {
48
+ const pattern = allRoutes[name];
49
+ if (!pattern) throw new Error(`Unknown route: "${name}"`);
50
+ if (!params) return pattern;
51
+ return substituteRouteParams(pattern, params);
52
+ };
53
+
54
+ let resolvedRoutes = 0;
55
+ let totalDynamic = 0;
56
+
57
+ // Count dynamic routes upfront for progress reporting
58
+ for (const { manifest } of allManifests) {
59
+ if (!manifest.prerenderRoutes) continue;
60
+ for (const routeName of manifest.prerenderRoutes) {
61
+ const pattern = manifest.routeManifest[routeName];
62
+ if (pattern && (pattern.includes(":") || pattern.includes("*"))) {
63
+ totalDynamic++;
64
+ }
65
+ }
66
+ }
67
+
68
+ // Periodic progress log so long getParams() calls don't look stalled
69
+ const paramsStart = performance.now();
70
+ const progressInterval =
71
+ totalDynamic > 0
72
+ ? setInterval(() => {
73
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
74
+ console.log(
75
+ `[rsc-router] Resolving prerender params... ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
76
+ );
77
+ }, 5000)
78
+ : undefined;
79
+
80
+ try {
81
+ for (const { manifest } of allManifests) {
82
+ if (!manifest.prerenderRoutes) continue;
83
+ const defs = manifest._prerenderDefs || {};
84
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
85
+ for (const routeName of manifest.prerenderRoutes) {
86
+ const pattern = manifest.routeManifest[routeName];
87
+ if (!pattern) continue;
88
+ const def = defs[routeName];
89
+ const isPassthroughRoute = passthroughSet.has(routeName);
90
+ const hasDynamic = pattern.includes(":") || pattern.includes("*");
91
+ if (!hasDynamic) {
92
+ // Static route: use pattern directly (strip trailing slash for URL)
93
+ entries.push({
94
+ urlPath: pattern.replace(/\/$/, "") || "/",
95
+ routeName,
96
+ concurrency: 1,
97
+ isPassthroughRoute,
98
+ });
99
+ } else {
100
+ // Dynamic route: call getParams() to enumerate param combinations
101
+ if (def?.getParams) {
102
+ try {
103
+ const buildVars: Record<string, any> = {};
104
+ const buildEnv = state.resolvedBuildEnv;
105
+ const getParamsCtx = {
106
+ build: true as const,
107
+ dev: !state.isBuildMode,
108
+ set: ((keyOrVar: any, value: any) => {
109
+ contextSet(buildVars, keyOrVar, value);
110
+ }) as any,
111
+ reverse: getParamsReverse,
112
+ get env() {
113
+ if (buildEnv !== undefined) return buildEnv;
114
+ throw new Error(
115
+ "[rsc-router] ctx.env is not available during build-time getParams(). " +
116
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
117
+ );
118
+ },
119
+ };
120
+ const paramsList = await def.getParams(getParamsCtx);
121
+ const concurrency = def.options?.concurrency ?? 1;
122
+ const hasBuildVars =
123
+ Object.keys(buildVars).length > 0 ||
124
+ Object.getOwnPropertySymbols(buildVars).length > 0;
125
+ for (const params of paramsList) {
126
+ let url = substituteRouteParams(
127
+ pattern,
128
+ params as Record<string, string>,
129
+ encodePathParam,
130
+ );
131
+ // Anonymous wildcard fallback: use conventional keys if provided
132
+ if (url.includes("*")) {
133
+ const wildcardValue =
134
+ (params as Record<string, string>)["*"] ??
135
+ (params as Record<string, string>).splat;
136
+ if (wildcardValue !== undefined) {
137
+ url = url.replace(
138
+ /\*[^/]*$/,
139
+ encodePathParam(wildcardValue),
140
+ );
141
+ }
142
+ }
143
+ entries.push({
144
+ urlPath: url.replace(/\/$/, "") || "/",
145
+ routeName,
146
+ concurrency,
147
+ ...(hasBuildVars ? { buildVars } : {}),
148
+ isPassthroughRoute,
149
+ });
150
+ }
151
+ resolvedRoutes++;
152
+ } catch (err: any) {
153
+ resolvedRoutes++;
154
+ // Skip in getParams() skips the entire route
155
+ if (err.name === "Skip") {
156
+ console.log(
157
+ `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
158
+ );
159
+ notifyOnError(
160
+ registry,
161
+ err,
162
+ "prerender",
163
+ routeName,
164
+ undefined,
165
+ true,
166
+ );
167
+ continue;
168
+ }
169
+ // Regular error: fail the build
170
+ console.error(
171
+ `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
172
+ );
173
+ notifyOnError(registry, err, "prerender", routeName);
174
+ throw err;
175
+ }
176
+ } else {
177
+ console.warn(
178
+ `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
179
+ );
180
+ }
181
+ }
182
+ }
183
+ }
184
+ } finally {
185
+ if (progressInterval) {
186
+ clearInterval(progressInterval);
187
+ const elapsed = ((performance.now() - paramsStart) / 1000).toFixed(1);
188
+ console.log(
189
+ `[rsc-router] Resolved prerender params: ${resolvedRoutes}/${totalDynamic} routes (${elapsed}s)`,
190
+ );
191
+ }
192
+ }
193
+
194
+ if (entries.length === 0) return;
195
+
196
+ // Determine the max concurrency for the log header
197
+ const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
198
+ const concurrencyNote =
199
+ maxConcurrency > 1 ? ` (concurrency: ${maxConcurrency})` : "";
200
+ console.log(
201
+ `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
202
+ );
203
+
204
+ const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
205
+
206
+ const manifestEntries: Record<string, string> = {};
207
+ let doneCount = 0;
208
+ let skipCount = 0;
209
+ const startTotal = performance.now();
210
+
211
+ // Group entries by concurrency for batched rendering.
212
+ // Within each group, all entries share the same concurrency limit.
213
+ const groups = groupByConcurrency(entries);
214
+
215
+ for (const group of groups) {
216
+ await runWithConcurrency(
217
+ group.entries,
218
+ group.concurrency,
219
+ async (entry) => {
220
+ const startUrl = performance.now();
221
+ for (const [, routerInstance] of registry) {
222
+ if (!routerInstance.matchForPrerender) continue;
223
+ try {
224
+ const result = await routerInstance.matchForPrerender(
225
+ entry.urlPath,
226
+ {},
227
+ entry.buildVars,
228
+ entry.isPassthroughRoute,
229
+ state.resolvedBuildEnv,
230
+ );
231
+ if (!result) continue;
232
+
233
+ // Handler returned ctx.passthrough() — skip manifest entry
234
+ if (result.passthrough) {
235
+ const elapsed = (performance.now() - startUrl).toFixed(0);
236
+ console.log(
237
+ `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
238
+ );
239
+ doneCount++;
240
+ break;
241
+ }
242
+
243
+ const paramHash = hashParams(result.params || {});
244
+ const mainKey = `${result.routeName}/${paramHash}`;
245
+ const mainValue = JSON.stringify({
246
+ segments: result.segments,
247
+ handles: result.handles,
248
+ });
249
+ manifestEntries[mainKey] = stageBuildAssetModule(
250
+ state.projectRoot,
251
+ "__pr",
252
+ mainValue,
253
+ );
254
+ if (result.interceptSegments?.length) {
255
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
256
+ const interceptValue = JSON.stringify({
257
+ segments: [...result.segments, ...result.interceptSegments],
258
+ handles: {
259
+ ...result.handles,
260
+ ...(result.interceptHandles || {}),
261
+ },
262
+ });
263
+ manifestEntries[interceptKey] = stageBuildAssetModule(
264
+ state.projectRoot,
265
+ "__pr",
266
+ interceptValue,
267
+ );
268
+ }
269
+ const elapsed = (performance.now() - startUrl).toFixed(0);
270
+ console.log(
271
+ `[rsc-router] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
272
+ );
273
+ doneCount++;
274
+ break;
275
+ } catch (err: any) {
276
+ if (err.name === "Skip") {
277
+ const elapsed = (performance.now() - startUrl).toFixed(0);
278
+ console.log(
279
+ `[rsc-router] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
280
+ );
281
+ skipCount++;
282
+ notifyOnError(
283
+ registry,
284
+ err,
285
+ "prerender",
286
+ entry.routeName,
287
+ entry.urlPath,
288
+ true,
289
+ );
290
+ break;
291
+ }
292
+ // Regular error: log, notify, and fail the build
293
+ const elapsed = (performance.now() - startUrl).toFixed(0);
294
+ console.error(
295
+ `[rsc-router] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
296
+ );
297
+ notifyOnError(
298
+ registry,
299
+ err,
300
+ "prerender",
301
+ entry.routeName,
302
+ entry.urlPath,
303
+ );
304
+ throw err;
305
+ }
306
+ }
307
+ },
308
+ );
309
+ }
310
+
311
+ const totalElapsed = (performance.now() - startTotal).toFixed(0);
312
+ if (doneCount > 0) {
313
+ state.prerenderManifestEntries = manifestEntries;
314
+ }
315
+ const parts = [`${doneCount} done`];
316
+ if (skipCount > 0) parts.push(`${skipCount} skipped`);
317
+ console.log(
318
+ `[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
319
+ );
320
+ }
321
+
322
+ /**
323
+ * Render Static handlers at build time. Each Static handler is called
324
+ * with a synthetic BuildContext and its output is RSC-serialized.
325
+ * Stages asset modules and stores handlerId-to-file entries in
326
+ * state.staticManifestEntries.
327
+ */
328
+ export async function renderStaticHandlers(
329
+ state: DiscoveryState,
330
+ rscEnv: any,
331
+ registry: Map<string, any>,
332
+ ): Promise<void> {
333
+ if (
334
+ !state.opts?.enableBuildPrerender ||
335
+ !state.isBuildMode ||
336
+ !state.resolvedStaticModules?.size
337
+ )
338
+ return;
339
+
340
+ const manifestEntries: Record<string, string> = {};
341
+ let staticDone = 0;
342
+ let staticSkip = 0;
343
+ let totalStaticCount = 0;
344
+
345
+ // Count handlers for the log header
346
+ for (const [, exportNames] of state.resolvedStaticModules) {
347
+ totalStaticCount += exportNames.length;
348
+ }
349
+ const startStatic = performance.now();
350
+ console.log(
351
+ `[rsc-router] Rendering ${totalStaticCount} static handler(s)...`,
352
+ );
353
+
354
+ for (const [moduleId, exportNames] of state.resolvedStaticModules) {
355
+ let mod: any;
356
+ try {
357
+ mod = await rscEnv!.runner.import(moduleId);
358
+ } catch (err: any) {
359
+ console.error(
360
+ `[rsc-router] Failed to import static module ${moduleId}: ${err.message}`,
361
+ );
362
+ notifyOnError(registry, err, "static");
363
+ throw err;
364
+ }
365
+
366
+ for (const name of exportNames) {
367
+ const def = mod[name];
368
+ if (!def || def.__brand !== "staticHandler" || !def.$$id) continue;
369
+ // Passthrough handlers stay live in the bundle
370
+ if (def.options?.passthrough) continue;
371
+
372
+ const startHandler = performance.now();
373
+ let handled = false;
374
+ for (const [, routerInstance] of registry) {
375
+ if (!routerInstance.renderStaticSegment) continue;
376
+ try {
377
+ const result = await routerInstance.renderStaticSegment(
378
+ def.handler,
379
+ def.$$id,
380
+ (def as any).$$routePrefix,
381
+ state.resolvedBuildEnv,
382
+ !state.isBuildMode,
383
+ );
384
+ if (result) {
385
+ const hasHandles = Object.keys(result.handles).length > 0;
386
+ const exportValue = hasHandles
387
+ ? JSON.stringify(result)
388
+ : JSON.stringify(result.encoded);
389
+ manifestEntries[def.$$id] = stageBuildAssetModule(
390
+ state.projectRoot,
391
+ "__st",
392
+ exportValue,
393
+ );
394
+ const elapsed = (performance.now() - startHandler).toFixed(0);
395
+ console.log(
396
+ `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
397
+ );
398
+ staticDone++;
399
+ handled = true;
400
+ break;
401
+ }
402
+ } catch (err: any) {
403
+ if (err.name === "Skip") {
404
+ const elapsed = (performance.now() - startHandler).toFixed(0);
405
+ console.log(
406
+ `[rsc-router] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
407
+ );
408
+ staticSkip++;
409
+ notifyOnError(registry, err, "static", undefined, undefined, true);
410
+ handled = true;
411
+ break;
412
+ }
413
+ // Regular error: log, notify, and fail the build
414
+ const elapsed = (performance.now() - startHandler).toFixed(0);
415
+ console.error(
416
+ `[rsc-router] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
417
+ );
418
+ notifyOnError(registry, err, "static");
419
+ throw err;
420
+ }
421
+ }
422
+ if (!handled) {
423
+ console.warn(
424
+ `[rsc-router] No router could render static handler "${name}"`,
425
+ );
426
+ }
427
+ }
428
+ }
429
+
430
+ const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
431
+ if (staticDone > 0) {
432
+ state.staticManifestEntries = manifestEntries;
433
+ }
434
+ const staticParts = [`${staticDone} done`];
435
+ if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
436
+ console.log(
437
+ `[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
438
+ );
439
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Route Types Writer
3
+ *
4
+ * Generates and writes TypeScript route type files (named-routes.gen.ts)
5
+ * from discovered router manifests and static source parsing.
6
+ */
7
+
8
+ import { dirname, basename, join, resolve } from "node:path";
9
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
10
+ import {
11
+ generateRouteTypesSource,
12
+ writeCombinedRouteTypes,
13
+ findRouterFiles,
14
+ buildCombinedRouteMapForRouterFile,
15
+ } from "../../build/generate-route-types.js";
16
+ import type { DiscoveryState } from "./state.js";
17
+ import { markSelfGenWrite } from "./self-gen-tracking.js";
18
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
19
+
20
+ /**
21
+ * Filter out auto-generated route names from a manifest.
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
+ */
26
+ function filterUserNamedRoutes(
27
+ manifest: Record<string, string>,
28
+ ): Record<string, string> {
29
+ const filtered: Record<string, string> = {};
30
+ for (const [name, pattern] of Object.entries(manifest)) {
31
+ if (!isAutoGeneratedRouteName(name)) {
32
+ filtered[name] = pattern;
33
+ }
34
+ }
35
+ return filtered;
36
+ }
37
+
38
+ /**
39
+ * Write combined route types for all router files.
40
+ * Only writes when content has changed to avoid triggering HMR loops.
41
+ */
42
+ export function writeCombinedRouteTypesWithTracking(
43
+ state: DiscoveryState,
44
+ opts?: { preserveIfLarger?: boolean },
45
+ ): void {
46
+ const routerFiles =
47
+ state.cachedRouterFiles ??
48
+ findRouterFiles(state.projectRoot, state.scanFilter);
49
+ state.cachedRouterFiles = routerFiles;
50
+
51
+ // Snapshot pre-write content to detect which files actually change.
52
+ const preContent = new Map<string, string>();
53
+ for (const routerFilePath of routerFiles) {
54
+ const routerDir = dirname(routerFilePath);
55
+ const routerBasename = basename(routerFilePath).replace(
56
+ /\.(tsx?|jsx?)$/,
57
+ "",
58
+ );
59
+ const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
60
+ try {
61
+ preContent.set(outPath, readFileSync(outPath, "utf-8"));
62
+ } catch {
63
+ // File doesn't exist yet — any write is a real change.
64
+ }
65
+ }
66
+
67
+ writeCombinedRouteTypes(state.projectRoot, routerFiles, opts);
68
+
69
+ // Mark only files that were actually written so the watcher can
70
+ // distinguish self-triggered change events from manual edits.
71
+ // Marking unchanged files creates stale entries that interfere with
72
+ // multi-server setups (e.g. shared webServer + isolated HMR server).
73
+ for (const routerFilePath of routerFiles) {
74
+ const routerDir = dirname(routerFilePath);
75
+ const routerBasename = basename(routerFilePath).replace(
76
+ /\.(tsx?|jsx?)$/,
77
+ "",
78
+ );
79
+ const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
80
+ if (!existsSync(outPath)) continue;
81
+ try {
82
+ const content = readFileSync(outPath, "utf-8");
83
+ if (content !== preContent.get(outPath)) {
84
+ markSelfGenWrite(state, outPath, content);
85
+ }
86
+ } catch {
87
+ // Ignore transient fs errors while files are being rewritten.
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Write per-router route types files from runtime discovery data.
94
+ */
95
+ export function writeRouteTypesFiles(state: DiscoveryState): void {
96
+ if (state.perRouterManifests.length === 0) return;
97
+
98
+ // Delete old combined named-routes.gen.ts if it exists
99
+ try {
100
+ const entryDir = dirname(
101
+ resolve(state.projectRoot, state.resolvedEntryPath!),
102
+ );
103
+ const oldCombinedPath = join(entryDir, "named-routes.gen.ts");
104
+ if (existsSync(oldCombinedPath)) {
105
+ unlinkSync(oldCombinedPath);
106
+ console.log(
107
+ `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
108
+ );
109
+ }
110
+ } catch {}
111
+
112
+ for (const {
113
+ id,
114
+ routeManifest,
115
+ routeSearchSchemas,
116
+ sourceFile,
117
+ } of state.perRouterManifests) {
118
+ if (!sourceFile) continue;
119
+
120
+ // Validate sourceFile points to a real project file, not node_modules or
121
+ // a Vite internal path. A bad sourceFile leads to route types written to
122
+ // the wrong location, causing non-deterministic type resolution.
123
+ if (sourceFile.includes("node_modules")) {
124
+ throw new Error(
125
+ `[rsc-router] Router "${id}" has sourceFile inside node_modules: ${sourceFile}\n` +
126
+ `This means createRouter() stack trace parsing matched a Vite internal frame.\n` +
127
+ `Set an explicit \`id\` on createRouter() or check the call site.`,
128
+ );
129
+ }
130
+
131
+ const routerDir = dirname(sourceFile);
132
+ const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
133
+ const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
134
+
135
+ // Filter out auto-generated route names (e.g. "$path____debug_reverse-test")
136
+ // to match the static parser's output and prevent HMR oscillation.
137
+ const userRoutes = filterUserNamedRoutes(routeManifest);
138
+ let effectiveSearchSchemas = routeSearchSchemas;
139
+
140
+ // Runtime manifest may omit search schema metadata in some module-runner
141
+ // flows. Fall back to static source parsing from the router file.
142
+ if (
143
+ (!effectiveSearchSchemas ||
144
+ Object.keys(effectiveSearchSchemas).length === 0) &&
145
+ sourceFile
146
+ ) {
147
+ const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
148
+ if (Object.keys(staticParsed.searchSchemas).length > 0) {
149
+ const filtered: Record<string, Record<string, string>> = {};
150
+ for (const name of Object.keys(userRoutes)) {
151
+ const schema = staticParsed.searchSchemas[name];
152
+ if (schema) filtered[name] = schema;
153
+ }
154
+ if (Object.keys(filtered).length > 0) {
155
+ effectiveSearchSchemas = filtered;
156
+ }
157
+ }
158
+ }
159
+
160
+ const source = generateRouteTypesSource(
161
+ userRoutes,
162
+ effectiveSearchSchemas && Object.keys(effectiveSearchSchemas).length > 0
163
+ ? effectiveSearchSchemas
164
+ : undefined,
165
+ );
166
+ const existing = existsSync(outPath)
167
+ ? readFileSync(outPath, "utf-8")
168
+ : null;
169
+ if (existing !== source) {
170
+ markSelfGenWrite(state, outPath, source);
171
+ writeFileSync(outPath, source);
172
+ console.log(`[rsc-router] Generated route types -> ${outPath}`);
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Supplement gen files with route groups from runtime manifests that the
179
+ * static parser cannot resolve (factory calls like createDocsPatterns()).
180
+ * Only adds groups whose dot-prefix (e.g. "docs.") is entirely absent
181
+ * from the static output. Groups partially visible to the static parser
182
+ * are left alone so renames/removals propagate immediately without
183
+ * requiring a server restart.
184
+ *
185
+ * The runtime manifest (cachedManifest / perRouterManifestMap) is updated
186
+ * automatically: the virtual:rsc-router/routes-manifest module imports the
187
+ * gen file, so when we write new content here, Vite's HMR invalidates the
188
+ * virtual module and re-evaluates it on the next request.
189
+ */
190
+ export function supplementGenFilesWithRuntimeRoutes(
191
+ state: DiscoveryState,
192
+ ): void {
193
+ // Cache static parsing results to avoid redundant I/O + parsing per router.
194
+ const parseCache = new Map<
195
+ string,
196
+ ReturnType<typeof buildCombinedRouteMapForRouterFile>
197
+ >();
198
+ const getParsed = (file: string) => {
199
+ let cached = parseCache.get(file);
200
+ if (!cached) {
201
+ cached = buildCombinedRouteMapForRouterFile(file);
202
+ parseCache.set(file, cached);
203
+ }
204
+ return cached;
205
+ };
206
+
207
+ for (const {
208
+ routeManifest,
209
+ routeSearchSchemas,
210
+ sourceFile,
211
+ factoryOnlyPrefixes,
212
+ } of state.perRouterManifests) {
213
+ if (!sourceFile) continue;
214
+ if (!factoryOnlyPrefixes || factoryOnlyPrefixes.size === 0) continue;
215
+
216
+ const staticParsed = getParsed(sourceFile);
217
+
218
+ // Merge: static routes (authoritative) + factory-only groups from runtime.
219
+ const mergedRoutes: Record<string, string> = { ...staticParsed.routes };
220
+ const mergedSearchSchemas: Record<string, Record<string, string>> = {
221
+ ...staticParsed.searchSchemas,
222
+ };
223
+
224
+ for (const [name, pattern] of Object.entries(routeManifest)) {
225
+ // Skip internal runtime-only names from unnamed routes/includes.
226
+ if (isAutoGeneratedRouteName(name)) continue;
227
+ const dotIdx = name.indexOf(".");
228
+ if (dotIdx <= 0) continue;
229
+ const prefix = name.substring(0, dotIdx + 1);
230
+ if (factoryOnlyPrefixes.has(prefix)) {
231
+ mergedRoutes[name] = pattern;
232
+ // Also merge search schemas from factory-generated routes
233
+ if (routeSearchSchemas?.[name]) {
234
+ mergedSearchSchemas[name] = routeSearchSchemas[name];
235
+ }
236
+ }
237
+ }
238
+
239
+ const routerDir = dirname(sourceFile);
240
+ const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
241
+ const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
242
+ const source = generateRouteTypesSource(
243
+ mergedRoutes,
244
+ Object.keys(mergedSearchSchemas).length > 0
245
+ ? mergedSearchSchemas
246
+ : undefined,
247
+ );
248
+ const existing = existsSync(outPath)
249
+ ? readFileSync(outPath, "utf-8")
250
+ : null;
251
+ if (existing !== source) {
252
+ markSelfGenWrite(state, outPath, source);
253
+ writeFileSync(outPath, source);
254
+ }
255
+ }
256
+ // No manual manifest update needed: the virtual module imports the gen
257
+ // file, so Vite's HMR automatically re-evaluates it with fresh data.
258
+ }