@rangojs/router 0.0.0-experimental.002d056c

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 (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -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/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,385 @@
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
+ for (const { manifest } of allManifests) {
55
+ if (!manifest.prerenderRoutes) continue;
56
+ const defs = manifest._prerenderDefs || {};
57
+ for (const routeName of manifest.prerenderRoutes) {
58
+ const pattern = manifest.routeManifest[routeName];
59
+ if (!pattern) continue;
60
+ const def = defs[routeName];
61
+ const isPassthroughRoute = !!def?.options?.passthrough;
62
+ const hasDynamic = pattern.includes(":") || pattern.includes("*");
63
+ if (!hasDynamic) {
64
+ // Static route: use pattern directly (strip trailing slash for URL)
65
+ entries.push({
66
+ urlPath: pattern.replace(/\/$/, "") || "/",
67
+ routeName,
68
+ concurrency: 1,
69
+ isPassthroughRoute,
70
+ });
71
+ } else {
72
+ // Dynamic route: call getParams() to enumerate param combinations
73
+ if (def?.getParams) {
74
+ try {
75
+ const buildVars: Record<string, any> = {};
76
+ const getParamsCtx = {
77
+ build: true as const,
78
+ set: ((keyOrVar: any, value: any) => {
79
+ contextSet(buildVars, keyOrVar, value);
80
+ }) as any,
81
+ reverse: getParamsReverse,
82
+ };
83
+ const paramsList = await def.getParams(getParamsCtx);
84
+ const concurrency = def.options?.concurrency ?? 1;
85
+ const hasBuildVars =
86
+ Object.keys(buildVars).length > 0 ||
87
+ Object.getOwnPropertySymbols(buildVars).length > 0;
88
+ for (const params of paramsList) {
89
+ let url = substituteRouteParams(
90
+ pattern,
91
+ params as Record<string, string>,
92
+ encodePathParam,
93
+ );
94
+ // Anonymous wildcard fallback: use conventional keys if provided
95
+ if (url.includes("*")) {
96
+ const wildcardValue =
97
+ (params as Record<string, string>)["*"] ??
98
+ (params as Record<string, string>).splat;
99
+ if (wildcardValue !== undefined) {
100
+ url = url.replace(/\*[^/]*$/, encodePathParam(wildcardValue));
101
+ }
102
+ }
103
+ entries.push({
104
+ urlPath: url.replace(/\/$/, "") || "/",
105
+ routeName,
106
+ concurrency,
107
+ ...(hasBuildVars ? { buildVars } : {}),
108
+ isPassthroughRoute,
109
+ });
110
+ }
111
+ } catch (err: any) {
112
+ // Skip in getParams() skips the entire route
113
+ if (err.name === "Skip") {
114
+ console.log(
115
+ `[rsc-router] SKIP route "${routeName}" - ${err.message}`,
116
+ );
117
+ notifyOnError(
118
+ registry,
119
+ err,
120
+ "prerender",
121
+ routeName,
122
+ undefined,
123
+ true,
124
+ );
125
+ continue;
126
+ }
127
+ // Regular error: fail the build
128
+ console.error(
129
+ `[rsc-router] Failed to get params for prerender route "${routeName}": ${err.message}`,
130
+ );
131
+ notifyOnError(registry, err, "prerender", routeName);
132
+ throw err;
133
+ }
134
+ } else {
135
+ console.warn(
136
+ `[rsc-router] Dynamic prerender route "${routeName}" has no getParams(), skipping`,
137
+ );
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ if (entries.length === 0) return;
144
+
145
+ // Determine the max concurrency for the log header
146
+ const maxConcurrency = Math.max(...entries.map((e) => e.concurrency));
147
+ const concurrencyNote =
148
+ maxConcurrency > 1 ? ` (concurrency: ${maxConcurrency})` : "";
149
+ console.log(
150
+ `[rsc-router] Pre-rendering ${entries.length} URL(s)${concurrencyNote}...`,
151
+ );
152
+
153
+ const { hashParams } = await rscEnv.runner.import("@rangojs/router/build");
154
+
155
+ const manifestEntries: Record<string, string> = {};
156
+ let doneCount = 0;
157
+ let skipCount = 0;
158
+ const startTotal = performance.now();
159
+
160
+ // Group entries by concurrency for batched rendering.
161
+ // Within each group, all entries share the same concurrency limit.
162
+ const groups = groupByConcurrency(entries);
163
+
164
+ for (const group of groups) {
165
+ await runWithConcurrency(
166
+ group.entries,
167
+ group.concurrency,
168
+ async (entry) => {
169
+ const startUrl = performance.now();
170
+ for (const [, routerInstance] of registry) {
171
+ if (!routerInstance.matchForPrerender) continue;
172
+ try {
173
+ const result = await routerInstance.matchForPrerender(
174
+ entry.urlPath,
175
+ {},
176
+ entry.buildVars,
177
+ entry.isPassthroughRoute,
178
+ );
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
+
191
+ const paramHash = hashParams(result.params || {});
192
+ const mainKey = `${result.routeName}/${paramHash}`;
193
+ const mainValue = JSON.stringify({
194
+ segments: result.segments,
195
+ handles: result.handles,
196
+ });
197
+ manifestEntries[mainKey] = stageBuildAssetModule(
198
+ state.projectRoot,
199
+ "__pr",
200
+ mainValue,
201
+ );
202
+ if (result.interceptSegments?.length) {
203
+ const interceptKey = `${result.routeName}/${paramHash}/i`;
204
+ const interceptValue = JSON.stringify({
205
+ segments: [...result.segments, ...result.interceptSegments],
206
+ handles: {
207
+ ...result.handles,
208
+ ...(result.interceptHandles || {}),
209
+ },
210
+ });
211
+ manifestEntries[interceptKey] = stageBuildAssetModule(
212
+ state.projectRoot,
213
+ "__pr",
214
+ interceptValue,
215
+ );
216
+ }
217
+ const elapsed = (performance.now() - startUrl).toFixed(0);
218
+ console.log(
219
+ `[rsc-router] OK ${entry.urlPath.padEnd(40)} (${elapsed}ms)`,
220
+ );
221
+ doneCount++;
222
+ break;
223
+ } catch (err: any) {
224
+ if (err.name === "Skip") {
225
+ const elapsed = (performance.now() - startUrl).toFixed(0);
226
+ console.log(
227
+ `[rsc-router] SKIP ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
228
+ );
229
+ skipCount++;
230
+ notifyOnError(
231
+ registry,
232
+ err,
233
+ "prerender",
234
+ entry.routeName,
235
+ entry.urlPath,
236
+ true,
237
+ );
238
+ break;
239
+ }
240
+ // Regular error: log, notify, and fail the build
241
+ const elapsed = (performance.now() - startUrl).toFixed(0);
242
+ console.error(
243
+ `[rsc-router] FAIL ${entry.urlPath.padEnd(40)} (${elapsed}ms) - ${err.message}`,
244
+ );
245
+ notifyOnError(
246
+ registry,
247
+ err,
248
+ "prerender",
249
+ entry.routeName,
250
+ entry.urlPath,
251
+ );
252
+ throw err;
253
+ }
254
+ }
255
+ },
256
+ );
257
+ }
258
+
259
+ const totalElapsed = (performance.now() - startTotal).toFixed(0);
260
+ if (doneCount > 0) {
261
+ state.prerenderManifestEntries = manifestEntries;
262
+ }
263
+ const parts = [`${doneCount} done`];
264
+ if (skipCount > 0) parts.push(`${skipCount} skipped`);
265
+ console.log(
266
+ `[rsc-router] Pre-render complete: ${parts.join(", ")} (${totalElapsed}ms total)`,
267
+ );
268
+ }
269
+
270
+ /**
271
+ * Render Static handlers at build time. Each Static handler is called
272
+ * with a synthetic BuildContext and its output is RSC-serialized.
273
+ * Stages asset modules and stores handlerId-to-file entries in
274
+ * state.staticManifestEntries.
275
+ */
276
+ export async function renderStaticHandlers(
277
+ state: DiscoveryState,
278
+ rscEnv: any,
279
+ registry: Map<string, any>,
280
+ ): Promise<void> {
281
+ if (
282
+ !state.opts?.enableBuildPrerender ||
283
+ !state.isBuildMode ||
284
+ !state.resolvedStaticModules?.size
285
+ )
286
+ return;
287
+
288
+ const manifestEntries: Record<string, string> = {};
289
+ let staticDone = 0;
290
+ let staticSkip = 0;
291
+ let totalStaticCount = 0;
292
+
293
+ // Count handlers for the log header
294
+ for (const [, exportNames] of state.resolvedStaticModules) {
295
+ totalStaticCount += exportNames.length;
296
+ }
297
+ const startStatic = performance.now();
298
+ console.log(
299
+ `[rsc-router] Rendering ${totalStaticCount} static handler(s)...`,
300
+ );
301
+
302
+ for (const [moduleId, exportNames] of state.resolvedStaticModules) {
303
+ let mod: any;
304
+ try {
305
+ mod = await rscEnv!.runner.import(moduleId);
306
+ } catch (err: any) {
307
+ console.error(
308
+ `[rsc-router] Failed to import static module ${moduleId}: ${err.message}`,
309
+ );
310
+ notifyOnError(registry, err, "static");
311
+ throw err;
312
+ }
313
+
314
+ for (const name of exportNames) {
315
+ const def = mod[name];
316
+ if (!def || def.__brand !== "staticHandler" || !def.$$id) continue;
317
+ // Passthrough handlers stay live in the bundle
318
+ if (def.options?.passthrough) continue;
319
+
320
+ const startHandler = performance.now();
321
+ let handled = false;
322
+ for (const [, routerInstance] of registry) {
323
+ if (!routerInstance.renderStaticSegment) continue;
324
+ try {
325
+ const result = await routerInstance.renderStaticSegment(
326
+ def.handler,
327
+ def.$$id,
328
+ (def as any).$$routePrefix,
329
+ );
330
+ if (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
+ );
340
+ const elapsed = (performance.now() - startHandler).toFixed(0);
341
+ console.log(
342
+ `[rsc-router] OK ${name.padEnd(40)} (${elapsed}ms)`,
343
+ );
344
+ staticDone++;
345
+ handled = true;
346
+ break;
347
+ }
348
+ } catch (err: any) {
349
+ if (err.name === "Skip") {
350
+ const elapsed = (performance.now() - startHandler).toFixed(0);
351
+ console.log(
352
+ `[rsc-router] SKIP ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
353
+ );
354
+ staticSkip++;
355
+ notifyOnError(registry, err, "static", undefined, undefined, true);
356
+ handled = true;
357
+ break;
358
+ }
359
+ // Regular error: log, notify, and fail the build
360
+ const elapsed = (performance.now() - startHandler).toFixed(0);
361
+ console.error(
362
+ `[rsc-router] FAIL ${name.padEnd(40)} (${elapsed}ms) - ${err.message}`,
363
+ );
364
+ notifyOnError(registry, err, "static");
365
+ throw err;
366
+ }
367
+ }
368
+ if (!handled) {
369
+ console.warn(
370
+ `[rsc-router] No router could render static handler "${name}"`,
371
+ );
372
+ }
373
+ }
374
+ }
375
+
376
+ const totalStaticElapsed = (performance.now() - startStatic).toFixed(0);
377
+ if (staticDone > 0) {
378
+ state.staticManifestEntries = manifestEntries;
379
+ }
380
+ const staticParts = [`${staticDone} done`];
381
+ if (staticSkip > 0) staticParts.push(`${staticSkip} skipped`);
382
+ console.log(
383
+ `[rsc-router] Static render complete: ${staticParts.join(", ")} (${totalStaticElapsed}ms total)`,
384
+ );
385
+ }
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Self-Generated File Tracking
3
+ *
4
+ * Tracks gen files recently written by the discovery plugin so the
5
+ * file watcher can distinguish self-triggered change events from
6
+ * manual edits.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+ import { readFileSync } from "node:fs";
11
+ import type { DiscoveryState } from "./state.js";
12
+
13
+ export function markSelfGenWrite(
14
+ state: DiscoveryState,
15
+ filePath: string,
16
+ content: string,
17
+ ): void {
18
+ const hash = createHash("sha256").update(content).digest("hex");
19
+ state.selfWrittenGenFiles.set(filePath, { at: Date.now(), hash });
20
+ }
21
+
22
+ export function consumeSelfGenWrite(
23
+ state: DiscoveryState,
24
+ filePath: string,
25
+ ): boolean {
26
+ const info = state.selfWrittenGenFiles.get(filePath);
27
+ if (!info) return false;
28
+ if (Date.now() - info.at > state.SELF_WRITE_WINDOW_MS) {
29
+ state.selfWrittenGenFiles.delete(filePath);
30
+ return false;
31
+ }
32
+ try {
33
+ const current = readFileSync(filePath, "utf-8");
34
+ const currentHash = createHash("sha256").update(current).digest("hex");
35
+ if (currentHash === info.hash) {
36
+ state.selfWrittenGenFiles.delete(filePath);
37
+ return true;
38
+ }
39
+ // Hash mismatch: file was changed externally. Keep the entry so a
40
+ // subsequent watcher event from our own write can still be consumed
41
+ // (e.g. when multiple Vite servers watch the same directory).
42
+ return false;
43
+ } catch {
44
+ state.selfWrittenGenFiles.delete(filePath);
45
+ return false;
46
+ }
47
+ }