@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) 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 +4951 -930
  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/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  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/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  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 +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -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 +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  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 +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /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
+ }