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

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 (312) 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 +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  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 +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,618 @@
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ unlinkSync,
6
+ readdirSync,
7
+ } from "node:fs";
8
+ import {
9
+ join,
10
+ dirname,
11
+ resolve,
12
+ sep,
13
+ basename as pathBasename,
14
+ } from "node:path";
15
+ import ts from "typescript";
16
+ import { generateRouteTypesSource } from "./codegen.js";
17
+ import type { ScanFilter } from "./scan-filter.js";
18
+ import {
19
+ resolveImportedVariable,
20
+ resolveImportPath,
21
+ buildCombinedRouteMapWithSearch,
22
+ type UnresolvableInclude,
23
+ } from "./include-resolution.js";
24
+ import { findUrlsVariableNames } from "./per-module-writer.js";
25
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
26
+
27
+ function countPublicRouteEntries(source: string): number {
28
+ const matches =
29
+ source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
30
+ let count = 0;
31
+ for (const match of matches) {
32
+ const routeName = match[1] || match[2];
33
+ if (routeName && !isAutoGeneratedRouteName(routeName.trim())) {
34
+ count++;
35
+ }
36
+ }
37
+ return count;
38
+ }
39
+
40
+ const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
41
+
42
+ function isRoutableSourceFile(name: string): boolean {
43
+ return (
44
+ (name.endsWith(".ts") ||
45
+ name.endsWith(".tsx") ||
46
+ name.endsWith(".js") ||
47
+ name.endsWith(".jsx")) &&
48
+ !name.includes(".gen.") &&
49
+ !name.includes(".test.") &&
50
+ !name.includes(".spec.")
51
+ );
52
+ }
53
+
54
+ function findRouterFilesRecursive(
55
+ dir: string,
56
+ filter: ScanFilter | undefined,
57
+ results: string[],
58
+ ): void {
59
+ let entries;
60
+ try {
61
+ entries = readdirSync(dir, { withFileTypes: true });
62
+ } catch (err) {
63
+ console.warn(
64
+ `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
65
+ );
66
+ return;
67
+ }
68
+
69
+ const childDirs: string[] = [];
70
+ const routerFilesInDir: string[] = [];
71
+
72
+ for (const entry of entries) {
73
+ const fullPath = join(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ if (
76
+ entry.name === "node_modules" ||
77
+ entry.name === "dist" ||
78
+ entry.name === "coverage" ||
79
+ entry.name === "__tests__" ||
80
+ entry.name === "__mocks__" ||
81
+ entry.name.startsWith(".")
82
+ )
83
+ continue;
84
+ childDirs.push(fullPath);
85
+ continue;
86
+ }
87
+
88
+ if (!isRoutableSourceFile(entry.name)) continue;
89
+ if (filter && !filter(fullPath)) continue;
90
+
91
+ try {
92
+ const source = readFileSync(fullPath, "utf-8");
93
+ if (ROUTER_CALL_PATTERN.test(source)) {
94
+ routerFilesInDir.push(fullPath);
95
+ }
96
+ } catch {
97
+ continue;
98
+ }
99
+ }
100
+
101
+ // A directory that contains a router file is treated as a router root.
102
+ // Once found, deeper directories are skipped to avoid redundant scans.
103
+ if (routerFilesInDir.length > 0) {
104
+ results.push(...routerFilesInDir);
105
+ return;
106
+ }
107
+
108
+ for (const childDir of childDirs) {
109
+ findRouterFilesRecursive(childDir, filter, results);
110
+ }
111
+ }
112
+
113
+ export function findNestedRouterConflict(
114
+ routerFiles: string[],
115
+ ): { ancestor: string; nested: string } | null {
116
+ const routerDirs = [
117
+ ...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
118
+ ].sort((a, b) => a.length - b.length);
119
+
120
+ for (let i = 0; i < routerDirs.length; i++) {
121
+ const ancestorDir = routerDirs[i];
122
+ const prefix = ancestorDir.endsWith(sep)
123
+ ? ancestorDir
124
+ : `${ancestorDir}${sep}`;
125
+ for (let j = i + 1; j < routerDirs.length; j++) {
126
+ const nestedDir = routerDirs[j];
127
+ if (!nestedDir.startsWith(prefix)) continue;
128
+ const ancestorFile = routerFiles.find(
129
+ (filePath) => dirname(resolve(filePath)) === ancestorDir,
130
+ );
131
+ const nestedFile = routerFiles.find(
132
+ (filePath) => dirname(resolve(filePath)) === nestedDir,
133
+ );
134
+ if (ancestorFile && nestedFile) {
135
+ return { ancestor: ancestorFile, nested: nestedFile };
136
+ }
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ export function formatNestedRouterConflictError(
144
+ conflict: { ancestor: string; nested: string },
145
+ prefix = "[rsc-router]",
146
+ ): string {
147
+ return (
148
+ `${prefix} Nested router roots are not supported.\n` +
149
+ `Router root: ${conflict.ancestor}\n` +
150
+ `Nested router: ${conflict.nested}\n` +
151
+ `Move the nested router into a sibling directory or configure it as a separate app root.`
152
+ );
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Router file URL extraction
157
+ // ---------------------------------------------------------------------------
158
+
159
+ /**
160
+ * Result of extracting URL patterns from a router file.
161
+ * - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
162
+ * - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
163
+ */
164
+ export type UrlsExtractionResult =
165
+ | { kind: "variable"; name: string }
166
+ | { kind: "inline"; block: string };
167
+
168
+ /**
169
+ * Extract the url patterns from a router file using AST.
170
+ * Detects four patterns:
171
+ * 1. createRouter(...).routes(variableName)
172
+ * 2. createRouter({ urls: variableName, ... })
173
+ * 3. createRouter(...).routes(({ path, ... }) => [...])
174
+ * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
175
+ * Returns either a variable name or an inline code block.
176
+ */
177
+ export function extractUrlsFromRouter(
178
+ code: string,
179
+ ): UrlsExtractionResult | null {
180
+ const sourceFile = ts.createSourceFile(
181
+ "router.tsx",
182
+ code,
183
+ ts.ScriptTarget.Latest,
184
+ true,
185
+ ts.ScriptKind.TSX,
186
+ );
187
+ let result: UrlsExtractionResult | null = null;
188
+
189
+ function isCreateRouterCall(node: ts.Node): boolean {
190
+ if (!ts.isCallExpression(node)) return false;
191
+ const callee = node.expression;
192
+ return ts.isIdentifier(callee) && callee.text === "createRouter";
193
+ }
194
+
195
+ /** Check if a node is an arrow/function expression (inline builder). */
196
+ function isInlineBuilder(node: ts.Node): boolean {
197
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
198
+ }
199
+
200
+ /** Check if a .routes() call chains from createRouter(). */
201
+ function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
202
+ if (
203
+ !ts.isPropertyAccessExpression(node.expression) ||
204
+ node.expression.name.text !== "routes"
205
+ )
206
+ return false;
207
+ let inner: ts.Expression = node.expression.expression;
208
+ while (
209
+ ts.isCallExpression(inner) &&
210
+ ts.isPropertyAccessExpression(inner.expression)
211
+ ) {
212
+ inner = inner.expression.expression;
213
+ }
214
+ return isCreateRouterCall(inner);
215
+ }
216
+
217
+ function visit(node: ts.Node) {
218
+ if (result) return;
219
+
220
+ // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
221
+ if (
222
+ ts.isCallExpression(node) &&
223
+ node.arguments.length >= 1 &&
224
+ isRoutesOnCreateRouter(node)
225
+ ) {
226
+ const arg = node.arguments[0];
227
+ if (ts.isIdentifier(arg)) {
228
+ result = { kind: "variable", name: arg.text };
229
+ } else if (isInlineBuilder(arg)) {
230
+ result = { kind: "inline", block: arg.getText(sourceFile) };
231
+ }
232
+ return;
233
+ }
234
+
235
+ // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
236
+ if (isCreateRouterCall(node)) {
237
+ const callExpr = node as ts.CallExpression;
238
+ for (const callArg of callExpr.arguments) {
239
+ if (ts.isObjectLiteralExpression(callArg)) {
240
+ for (const prop of callArg.properties) {
241
+ if (
242
+ ts.isPropertyAssignment(prop) &&
243
+ ts.isIdentifier(prop.name) &&
244
+ prop.name.text === "urls"
245
+ ) {
246
+ if (ts.isIdentifier(prop.initializer)) {
247
+ result = { kind: "variable", name: prop.initializer.text };
248
+ } else if (isInlineBuilder(prop.initializer)) {
249
+ result = {
250
+ kind: "inline",
251
+ block: prop.initializer.getText(sourceFile),
252
+ };
253
+ }
254
+ return;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ ts.forEachChild(node, visit);
262
+ }
263
+
264
+ visit(sourceFile);
265
+ return result;
266
+ }
267
+
268
+ /**
269
+ * Extract the `basename` string literal from createRouter({ basename: "..." }).
270
+ * Returns the basename value or undefined if not present.
271
+ */
272
+ export function extractBasenameFromRouter(code: string): string | undefined {
273
+ const sourceFile = ts.createSourceFile(
274
+ "router.tsx",
275
+ code,
276
+ ts.ScriptTarget.Latest,
277
+ true,
278
+ ts.ScriptKind.TSX,
279
+ );
280
+ let result: string | undefined;
281
+
282
+ function visit(node: ts.Node) {
283
+ if (result !== undefined) return;
284
+ if (
285
+ ts.isCallExpression(node) &&
286
+ ts.isIdentifier(node.expression) &&
287
+ node.expression.text === "createRouter"
288
+ ) {
289
+ for (const arg of node.arguments) {
290
+ if (ts.isObjectLiteralExpression(arg)) {
291
+ for (const prop of arg.properties) {
292
+ if (
293
+ ts.isPropertyAssignment(prop) &&
294
+ ts.isIdentifier(prop.name) &&
295
+ prop.name.text === "basename" &&
296
+ ts.isStringLiteral(prop.initializer)
297
+ ) {
298
+ result = prop.initializer.text;
299
+ return;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ ts.forEachChild(node, visit);
306
+ }
307
+
308
+ visit(sourceFile);
309
+ return result;
310
+ }
311
+
312
+ /** @deprecated Use extractUrlsFromRouter instead */
313
+ export function extractUrlsVariableFromRouter(code: string): string | null {
314
+ const result = extractUrlsFromRouter(code);
315
+ return result?.kind === "variable" ? result.name : null;
316
+ }
317
+
318
+ /** Apply a basename prefix to all route patterns in a result set. */
319
+ function applyBasenameToRoutes(
320
+ result: {
321
+ routes: Record<string, string>;
322
+ searchSchemas: Record<string, Record<string, string>>;
323
+ },
324
+ basename: string,
325
+ ): {
326
+ routes: Record<string, string>;
327
+ searchSchemas: Record<string, Record<string, string>>;
328
+ } {
329
+ const prefixed: Record<string, string> = {};
330
+ for (const [name, pattern] of Object.entries(result.routes)) {
331
+ if (pattern === "/") {
332
+ prefixed[name] = basename;
333
+ } else if (basename.endsWith("/") && pattern.startsWith("/")) {
334
+ prefixed[name] = basename + pattern.slice(1);
335
+ } else {
336
+ prefixed[name] = basename + pattern;
337
+ }
338
+ }
339
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
340
+ }
341
+
342
+ /**
343
+ * Resolve routes and search schemas from a router source file by following the
344
+ * variable passed to `.routes(...)` or `urls: ...` in createRouter options,
345
+ * or by parsing an inline builder function directly.
346
+ */
347
+ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
348
+ routes: Record<string, string>;
349
+ searchSchemas: Record<string, Record<string, string>>;
350
+ } {
351
+ let routerSource: string;
352
+ try {
353
+ routerSource = readFileSync(routerFilePath, "utf-8");
354
+ } catch {
355
+ return { routes: {}, searchSchemas: {} };
356
+ }
357
+
358
+ const extraction = extractUrlsFromRouter(routerSource);
359
+ if (!extraction) {
360
+ return { routes: {}, searchSchemas: {} };
361
+ }
362
+
363
+ // Detect basename from createRouter({ basename: "..." })
364
+ const rawBasename = extractBasenameFromRouter(routerSource);
365
+ const basename = rawBasename
366
+ ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
367
+ : undefined;
368
+
369
+ let result: {
370
+ routes: Record<string, string>;
371
+ searchSchemas: Record<string, Record<string, string>>;
372
+ };
373
+
374
+ // Inline builder: extract routes directly from the function body
375
+ if (extraction.kind === "inline") {
376
+ result = buildCombinedRouteMapWithSearch(
377
+ routerFilePath,
378
+ undefined,
379
+ undefined,
380
+ undefined,
381
+ extraction.block,
382
+ );
383
+ } else {
384
+ // Variable reference: follow imports or same-file declaration
385
+ const imported = resolveImportedVariable(routerSource, extraction.name);
386
+ if (imported) {
387
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
388
+ if (!targetFile) {
389
+ return { routes: {}, searchSchemas: {} };
390
+ }
391
+ result = buildCombinedRouteMapWithSearch(
392
+ targetFile,
393
+ imported.exportedName,
394
+ );
395
+ } else {
396
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
397
+ }
398
+ }
399
+
400
+ // Apply basename prefix to all extracted route patterns
401
+ if (basename) {
402
+ result = applyBasenameToRoutes(result, basename);
403
+ }
404
+
405
+ return result;
406
+ }
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // Unresolvable include detection (full include tree walk)
410
+ // ---------------------------------------------------------------------------
411
+
412
+ /**
413
+ * Walk the full include tree starting from a router file and detect
414
+ * all includes that the static parser cannot resolve.
415
+ * Returns an array of diagnostics; empty means fully resolvable.
416
+ */
417
+ export function detectUnresolvableIncludes(
418
+ routerFilePath: string,
419
+ ): UnresolvableInclude[] {
420
+ const realPath = resolve(routerFilePath);
421
+ let source: string;
422
+ try {
423
+ source = readFileSync(realPath, "utf-8");
424
+ } catch {
425
+ return [];
426
+ }
427
+
428
+ // Extract the urls source from the router file
429
+ const extraction = extractUrlsFromRouter(source);
430
+ if (!extraction) return [];
431
+
432
+ const diagnostics: UnresolvableInclude[] = [];
433
+
434
+ if (extraction.kind === "inline") {
435
+ // Inline builder: parse directly
436
+ buildCombinedRouteMapWithSearch(
437
+ realPath,
438
+ undefined,
439
+ new Set(),
440
+ diagnostics,
441
+ extraction.block,
442
+ );
443
+ return diagnostics;
444
+ }
445
+
446
+ // Variable reference: resolve where it comes from
447
+ const imported = resolveImportedVariable(source, extraction.name);
448
+ let targetFile: string;
449
+ let exportedName: string | undefined;
450
+
451
+ if (imported) {
452
+ const resolved = resolveImportPath(imported.specifier, realPath);
453
+ if (!resolved) {
454
+ return [
455
+ {
456
+ pathPrefix: "/",
457
+ namePrefix: null,
458
+ reason: "file-not-found",
459
+ sourceFile: realPath,
460
+ detail: `import "${imported.specifier}" resolved to no file`,
461
+ },
462
+ ];
463
+ }
464
+ targetFile = resolved;
465
+ exportedName = imported.exportedName;
466
+ } else {
467
+ // Same-file urls() definition
468
+ targetFile = realPath;
469
+ exportedName = extraction.name;
470
+ }
471
+
472
+ buildCombinedRouteMapWithSearch(
473
+ targetFile,
474
+ exportedName,
475
+ new Set(),
476
+ diagnostics,
477
+ );
478
+ return diagnostics;
479
+ }
480
+
481
+ /**
482
+ * Walk the include tree for a standalone urls() module file and detect
483
+ * all unresolvable includes. Mirrors detectUnresolvableIncludes() but
484
+ * operates on urls() variable declarations instead of going through
485
+ * createRouter().
486
+ */
487
+ export function detectUnresolvableIncludesForUrlsFile(
488
+ filePath: string,
489
+ ): UnresolvableInclude[] {
490
+ const realPath = resolve(filePath);
491
+ let source: string;
492
+ try {
493
+ source = readFileSync(realPath, "utf-8");
494
+ } catch {
495
+ return [];
496
+ }
497
+
498
+ const varNames = findUrlsVariableNames(source);
499
+ if (varNames.length === 0) return [];
500
+
501
+ const diagnostics: UnresolvableInclude[] = [];
502
+ for (const varName of varNames) {
503
+ buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
504
+ }
505
+ return diagnostics;
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // Per-router named-routes.gen.ts writer
510
+ // ---------------------------------------------------------------------------
511
+
512
+ /**
513
+ * Scan for files containing createRouter() and return their paths.
514
+ * Call once at startup; the result can be reused on subsequent watcher triggers.
515
+ */
516
+ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
517
+ const result: string[] = [];
518
+ findRouterFilesRecursive(root, filter, result);
519
+ return result;
520
+ }
521
+
522
+ /**
523
+ * Write named-routes.gen.ts files from static source parsing.
524
+ * Dev-only: provides initial .gen.ts files for IDE types before runtime
525
+ * discovery runs. Must NOT be called during production builds -- runtime
526
+ * discovery in buildStart produces the definitive file.
527
+ */
528
+ export function writeCombinedRouteTypes(
529
+ root: string,
530
+ knownRouterFiles?: string[],
531
+ opts?: { preserveIfLarger?: boolean },
532
+ ): void {
533
+ // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
534
+ try {
535
+ const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
536
+ if (existsSync(oldCombinedPath)) {
537
+ unlinkSync(oldCombinedPath);
538
+ console.log(
539
+ `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
540
+ );
541
+ }
542
+ } catch {}
543
+
544
+ const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
545
+ if (routerFilePaths.length === 0) return;
546
+
547
+ const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
548
+ if (nestedRouterConflict) {
549
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
550
+ }
551
+
552
+ for (const routerFilePath of routerFilePaths) {
553
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
554
+ if (
555
+ Object.keys(result.routes).length === 0 &&
556
+ Object.keys(result.searchSchemas).length === 0
557
+ ) {
558
+ // Check if the file even has a createRouter call — if not, skip entirely.
559
+ // If it does, fall through to write an empty placeholder below.
560
+ let routerSource: string;
561
+ try {
562
+ routerSource = readFileSync(routerFilePath, "utf-8");
563
+ } catch {
564
+ continue;
565
+ }
566
+ if (!extractUrlsFromRouter(routerSource)) continue;
567
+ }
568
+
569
+ const routerBasename = pathBasename(routerFilePath).replace(
570
+ /\.(tsx?|jsx?)$/,
571
+ "",
572
+ );
573
+ const outPath = join(
574
+ dirname(routerFilePath),
575
+ `${routerBasename}.named-routes.gen.ts`,
576
+ );
577
+ const existing = existsSync(outPath)
578
+ ? readFileSync(outPath, "utf-8")
579
+ : null;
580
+
581
+ // When the static parser can't extract routes (e.g. callback-style urls()),
582
+ // write an empty placeholder so the build-time transform's injected import
583
+ // resolves. Runtime discovery will overwrite this with the real routes.
584
+ if (Object.keys(result.routes).length === 0) {
585
+ if (!existing) {
586
+ const emptySource = generateRouteTypesSource({});
587
+ writeFileSync(outPath, emptySource);
588
+ }
589
+ continue;
590
+ }
591
+
592
+ const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
593
+ const source = generateRouteTypesSource(
594
+ result.routes,
595
+ hasSearchSchemas ? result.searchSchemas : undefined,
596
+ );
597
+ if (existing !== source) {
598
+ // On initial dev startup, don't overwrite a file from runtime discovery
599
+ // (which has all dynamic routes) with a smaller set from the static
600
+ // parser. The static parser can't see routes generated by Array.from()
601
+ // or other dynamic code. During HMR (file watcher), always write so
602
+ // newly added routes appear immediately.
603
+ if (opts?.preserveIfLarger && existing) {
604
+ const existingCount = countPublicRouteEntries(existing);
605
+ const newCount = Object.keys(result.routes).filter(
606
+ (name) => !isAutoGeneratedRouteName(name),
607
+ ).length;
608
+ if (existingCount > newCount) {
609
+ continue;
610
+ }
611
+ }
612
+ writeFileSync(outPath, source);
613
+ console.log(
614
+ `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
615
+ );
616
+ }
617
+ }
618
+ }
@@ -0,0 +1,85 @@
1
+ import { join, relative } from "node:path";
2
+ import { readdirSync } from "node:fs";
3
+ // @ts-ignore -- picomatch ships no .d.ts; types are trivial
4
+ import picomatch from "picomatch";
5
+
6
+ /** Default exclude patterns for route type scanning. */
7
+ export const DEFAULT_EXCLUDE_PATTERNS: string[] = [
8
+ "**/__tests__/**",
9
+ "**/__mocks__/**",
10
+ "**/dist/**",
11
+ "**/coverage/**",
12
+ "**/*.test.{ts,tsx,js,jsx}",
13
+ "**/*.spec.{ts,tsx,js,jsx}",
14
+ ];
15
+
16
+ export type ScanFilter = (absolutePath: string) => boolean;
17
+
18
+ /**
19
+ * Compile include/exclude glob patterns into a single predicate.
20
+ * Paths are made root-relative before matching.
21
+ * Returns undefined when no filtering is needed (no include, default exclude).
22
+ */
23
+ export function createScanFilter(
24
+ root: string,
25
+ opts: { include?: string[]; exclude?: string[] },
26
+ ): ScanFilter | undefined {
27
+ const { include, exclude } = opts;
28
+ const hasInclude = include && include.length > 0;
29
+ const hasCustomExclude = exclude !== undefined;
30
+
31
+ if (!hasInclude && !hasCustomExclude) return undefined;
32
+
33
+ const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
34
+ const includeMatcher = hasInclude ? picomatch(include) : null;
35
+ const excludeMatcher =
36
+ effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
37
+
38
+ return (absolutePath: string) => {
39
+ const rel = relative(root, absolutePath);
40
+ if (excludeMatcher && excludeMatcher(rel)) return false;
41
+ if (includeMatcher) return includeMatcher(rel);
42
+ return true;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Recursively find .ts/.tsx files under a directory, skipping node_modules
48
+ * and .gen. files.
49
+ */
50
+ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
51
+ const results: string[] = [];
52
+ let entries;
53
+ try {
54
+ entries = readdirSync(dir, { withFileTypes: true });
55
+ } catch (err) {
56
+ console.warn(
57
+ `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
58
+ );
59
+ return results;
60
+ }
61
+ for (const entry of entries) {
62
+ const fullPath = join(dir, entry.name);
63
+ if (entry.isDirectory()) {
64
+ if (
65
+ entry.name === "node_modules" ||
66
+ entry.name.startsWith(".") ||
67
+ entry.name === "dist" ||
68
+ entry.name === "build" ||
69
+ entry.name === "coverage"
70
+ )
71
+ continue;
72
+ results.push(...findTsFiles(fullPath, filter));
73
+ } else if (
74
+ (entry.name.endsWith(".ts") ||
75
+ entry.name.endsWith(".tsx") ||
76
+ entry.name.endsWith(".js") ||
77
+ entry.name.endsWith(".jsx")) &&
78
+ !entry.name.includes(".gen.")
79
+ ) {
80
+ if (filter && !filter(fullPath)) continue;
81
+ results.push(fullPath);
82
+ }
83
+ }
84
+ return results;
85
+ }