@rangojs/router 0.0.0-experimental.0f44aca1

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 +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -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 +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -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 +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -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 +538 -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 +469 -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 +540 -0
  105. package/src/cache/cf/index.ts +25 -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 +43 -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 +275 -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 +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -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 +192 -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 +748 -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 +316 -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 +1239 -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 +289 -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 +1002 -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 +235 -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 +914 -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 +102 -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 +110 -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 +131 -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 +365 -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 +254 -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 +510 -0
  298. package/src/vite/router-discovery.ts +785 -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,469 @@
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
+ );
50
+ }
51
+
52
+ function findRouterFilesRecursive(
53
+ dir: string,
54
+ filter: ScanFilter | undefined,
55
+ results: string[],
56
+ ): void {
57
+ let entries;
58
+ try {
59
+ entries = readdirSync(dir, { withFileTypes: true });
60
+ } catch (err) {
61
+ console.warn(
62
+ `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
63
+ );
64
+ return;
65
+ }
66
+
67
+ const childDirs: string[] = [];
68
+ const routerFilesInDir: string[] = [];
69
+
70
+ for (const entry of entries) {
71
+ const fullPath = join(dir, entry.name);
72
+ if (entry.isDirectory()) {
73
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
74
+ childDirs.push(fullPath);
75
+ continue;
76
+ }
77
+
78
+ if (!isRoutableSourceFile(entry.name)) continue;
79
+ if (filter && !filter(fullPath)) continue;
80
+
81
+ try {
82
+ const source = readFileSync(fullPath, "utf-8");
83
+ if (ROUTER_CALL_PATTERN.test(source)) {
84
+ routerFilesInDir.push(fullPath);
85
+ }
86
+ } catch {
87
+ continue;
88
+ }
89
+ }
90
+
91
+ // A directory that contains a router file is treated as a router root.
92
+ // Once found, deeper directories are skipped to avoid redundant scans.
93
+ if (routerFilesInDir.length > 0) {
94
+ results.push(...routerFilesInDir);
95
+ return;
96
+ }
97
+
98
+ for (const childDir of childDirs) {
99
+ findRouterFilesRecursive(childDir, filter, results);
100
+ }
101
+ }
102
+
103
+ export function findNestedRouterConflict(
104
+ routerFiles: string[],
105
+ ): { ancestor: string; nested: string } | null {
106
+ const routerDirs = [
107
+ ...new Set(routerFiles.map((filePath) => dirname(resolve(filePath)))),
108
+ ].sort((a, b) => a.length - b.length);
109
+
110
+ for (let i = 0; i < routerDirs.length; i++) {
111
+ const ancestorDir = routerDirs[i];
112
+ const prefix = ancestorDir.endsWith(sep)
113
+ ? ancestorDir
114
+ : `${ancestorDir}${sep}`;
115
+ for (let j = i + 1; j < routerDirs.length; j++) {
116
+ const nestedDir = routerDirs[j];
117
+ if (!nestedDir.startsWith(prefix)) continue;
118
+ const ancestorFile = routerFiles.find(
119
+ (filePath) => dirname(resolve(filePath)) === ancestorDir,
120
+ );
121
+ const nestedFile = routerFiles.find(
122
+ (filePath) => dirname(resolve(filePath)) === nestedDir,
123
+ );
124
+ if (ancestorFile && nestedFile) {
125
+ return { ancestor: ancestorFile, nested: nestedFile };
126
+ }
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ export function formatNestedRouterConflictError(
134
+ conflict: { ancestor: string; nested: string },
135
+ prefix = "[rsc-router]",
136
+ ): string {
137
+ return (
138
+ `${prefix} Nested router roots are not supported.\n` +
139
+ `Router root: ${conflict.ancestor}\n` +
140
+ `Nested router: ${conflict.nested}\n` +
141
+ `Move the nested router into a sibling directory or configure it as a separate app root.`
142
+ );
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Router file URL extraction
147
+ // ---------------------------------------------------------------------------
148
+
149
+ /**
150
+ * Extract the url patterns variable from a router file using AST.
151
+ * Detects two patterns:
152
+ * 1. createRouter(...).routes(variableName)
153
+ * 2. createRouter({ urls: variableName, ... })
154
+ * Returns the local variable name.
155
+ */
156
+ export function extractUrlsVariableFromRouter(code: string): string | null {
157
+ const sourceFile = ts.createSourceFile(
158
+ "router.tsx",
159
+ code,
160
+ ts.ScriptTarget.Latest,
161
+ true,
162
+ ts.ScriptKind.TSX,
163
+ );
164
+ let result: string | null = null;
165
+
166
+ function isCreateRouterCall(node: ts.Node): boolean {
167
+ if (!ts.isCallExpression(node)) return false;
168
+ const callee = node.expression;
169
+ return ts.isIdentifier(callee) && callee.text === "createRouter";
170
+ }
171
+
172
+ function visit(node: ts.Node) {
173
+ if (result) return;
174
+
175
+ // Pattern 1: createRouter(...).routes(variableName)
176
+ // The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
177
+ if (
178
+ ts.isCallExpression(node) &&
179
+ ts.isPropertyAccessExpression(node.expression) &&
180
+ node.expression.name.text === "routes" &&
181
+ node.arguments.length >= 1 &&
182
+ ts.isIdentifier(node.arguments[0])
183
+ ) {
184
+ // Walk up the chain: createRouter().middleware(...).routes(x) etc.
185
+ // The innermost call should be createRouter(...)
186
+ let inner: ts.Expression = node.expression.expression;
187
+ while (
188
+ ts.isCallExpression(inner) &&
189
+ ts.isPropertyAccessExpression(inner.expression)
190
+ ) {
191
+ inner = inner.expression.expression;
192
+ }
193
+ if (isCreateRouterCall(inner)) {
194
+ result = (node.arguments[0] as ts.Identifier).text;
195
+ return;
196
+ }
197
+ }
198
+
199
+ // Pattern 2: createRouter({ urls: variableName, ... })
200
+ if (isCreateRouterCall(node)) {
201
+ const callExpr = node as ts.CallExpression;
202
+ for (const arg of callExpr.arguments) {
203
+ if (ts.isObjectLiteralExpression(arg)) {
204
+ for (const prop of arg.properties) {
205
+ if (
206
+ ts.isPropertyAssignment(prop) &&
207
+ ts.isIdentifier(prop.name) &&
208
+ prop.name.text === "urls" &&
209
+ ts.isIdentifier(prop.initializer)
210
+ ) {
211
+ result = prop.initializer.text;
212
+ return;
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ ts.forEachChild(node, visit);
220
+ }
221
+
222
+ visit(sourceFile);
223
+ return result;
224
+ }
225
+
226
+ /**
227
+ * Resolve routes and search schemas from a router source file by following the
228
+ * variable passed to `.routes(...)` or `urls: ...` in createRouter options.
229
+ */
230
+ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
231
+ routes: Record<string, string>;
232
+ searchSchemas: Record<string, Record<string, string>>;
233
+ } {
234
+ let routerSource: string;
235
+ try {
236
+ routerSource = readFileSync(routerFilePath, "utf-8");
237
+ } catch {
238
+ return { routes: {}, searchSchemas: {} };
239
+ }
240
+
241
+ const urlsVarName = extractUrlsVariableFromRouter(routerSource);
242
+ if (!urlsVarName) {
243
+ return { routes: {}, searchSchemas: {} };
244
+ }
245
+
246
+ const imported = resolveImportedVariable(routerSource, urlsVarName);
247
+ if (imported) {
248
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
249
+ if (!targetFile) {
250
+ return { routes: {}, searchSchemas: {} };
251
+ }
252
+ return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
253
+ }
254
+
255
+ return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Unresolvable include detection (full include tree walk)
260
+ // ---------------------------------------------------------------------------
261
+
262
+ /**
263
+ * Walk the full include tree starting from a router file and detect
264
+ * all includes that the static parser cannot resolve.
265
+ * Returns an array of diagnostics; empty means fully resolvable.
266
+ */
267
+ export function detectUnresolvableIncludes(
268
+ routerFilePath: string,
269
+ ): UnresolvableInclude[] {
270
+ const realPath = resolve(routerFilePath);
271
+ let source: string;
272
+ try {
273
+ source = readFileSync(realPath, "utf-8");
274
+ } catch {
275
+ return [];
276
+ }
277
+
278
+ // Extract the urls variable from the router file
279
+ const urlsVarName = extractUrlsVariableFromRouter(source);
280
+ if (!urlsVarName) return [];
281
+
282
+ // Resolve where the urls variable comes from
283
+ const imported = resolveImportedVariable(source, urlsVarName);
284
+ let targetFile: string;
285
+ let exportedName: string | undefined;
286
+
287
+ if (imported) {
288
+ const resolved = resolveImportPath(imported.specifier, realPath);
289
+ if (!resolved) {
290
+ return [
291
+ {
292
+ pathPrefix: "/",
293
+ namePrefix: null,
294
+ reason: "file-not-found",
295
+ sourceFile: realPath,
296
+ detail: `import "${imported.specifier}" resolved to no file`,
297
+ },
298
+ ];
299
+ }
300
+ targetFile = resolved;
301
+ exportedName = imported.exportedName;
302
+ } else {
303
+ // Same-file urls() definition
304
+ targetFile = realPath;
305
+ exportedName = urlsVarName;
306
+ }
307
+
308
+ const diagnostics: UnresolvableInclude[] = [];
309
+ buildCombinedRouteMapWithSearch(
310
+ targetFile,
311
+ exportedName,
312
+ new Set(),
313
+ diagnostics,
314
+ );
315
+ return diagnostics;
316
+ }
317
+
318
+ /**
319
+ * Walk the include tree for a standalone urls() module file and detect
320
+ * all unresolvable includes. Mirrors detectUnresolvableIncludes() but
321
+ * operates on urls() variable declarations instead of going through
322
+ * createRouter().
323
+ */
324
+ export function detectUnresolvableIncludesForUrlsFile(
325
+ filePath: string,
326
+ ): UnresolvableInclude[] {
327
+ const realPath = resolve(filePath);
328
+ let source: string;
329
+ try {
330
+ source = readFileSync(realPath, "utf-8");
331
+ } catch {
332
+ return [];
333
+ }
334
+
335
+ const varNames = findUrlsVariableNames(source);
336
+ if (varNames.length === 0) return [];
337
+
338
+ const diagnostics: UnresolvableInclude[] = [];
339
+ for (const varName of varNames) {
340
+ buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
341
+ }
342
+ return diagnostics;
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Per-router named-routes.gen.ts writer
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /**
350
+ * Scan for files containing createRouter() and return their paths.
351
+ * Call once at startup; the result can be reused on subsequent watcher triggers.
352
+ */
353
+ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
354
+ const result: string[] = [];
355
+ findRouterFilesRecursive(root, filter, result);
356
+ return result;
357
+ }
358
+
359
+ /**
360
+ * Write named-routes.gen.ts files from static source parsing.
361
+ * Dev-only: provides initial .gen.ts files for IDE types before runtime
362
+ * discovery runs. Must NOT be called during production builds -- runtime
363
+ * discovery in buildStart produces the definitive file.
364
+ */
365
+ export function writeCombinedRouteTypes(
366
+ root: string,
367
+ knownRouterFiles?: string[],
368
+ opts?: { preserveIfLarger?: boolean },
369
+ ): void {
370
+ // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
371
+ try {
372
+ const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
373
+ if (existsSync(oldCombinedPath)) {
374
+ unlinkSync(oldCombinedPath);
375
+ console.log(
376
+ `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
377
+ );
378
+ }
379
+ } catch {}
380
+
381
+ const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
382
+ if (routerFilePaths.length === 0) return;
383
+
384
+ const nestedRouterConflict = findNestedRouterConflict(routerFilePaths);
385
+ if (nestedRouterConflict) {
386
+ throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
387
+ }
388
+
389
+ for (const routerFilePath of routerFilePaths) {
390
+ let routerSource: string;
391
+ try {
392
+ routerSource = readFileSync(routerFilePath, "utf-8");
393
+ } catch {
394
+ continue;
395
+ }
396
+ // Extract the urls variable name from .routes(varName) or urls: varName
397
+ const urlsVarName = extractUrlsVariableFromRouter(routerSource);
398
+ if (!urlsVarName) continue;
399
+
400
+ // Resolve the variable to its source module
401
+ let result: {
402
+ routes: Record<string, string>;
403
+ searchSchemas: Record<string, Record<string, string>>;
404
+ };
405
+
406
+ const imported = resolveImportedVariable(routerSource, urlsVarName);
407
+ if (imported) {
408
+ // Variable is imported from another module
409
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
410
+ if (!targetFile) continue;
411
+ result = buildCombinedRouteMapWithSearch(
412
+ targetFile,
413
+ imported.exportedName,
414
+ );
415
+ } else {
416
+ // Variable is defined in the same file
417
+ result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
418
+ }
419
+
420
+ const routerBasename = pathBasename(routerFilePath).replace(
421
+ /\.(tsx?|jsx?)$/,
422
+ "",
423
+ );
424
+ const outPath = join(
425
+ dirname(routerFilePath),
426
+ `${routerBasename}.named-routes.gen.ts`,
427
+ );
428
+ const existing = existsSync(outPath)
429
+ ? readFileSync(outPath, "utf-8")
430
+ : null;
431
+
432
+ // When the static parser can't extract routes (e.g. callback-style urls()),
433
+ // write an empty placeholder so the build-time transform's injected import
434
+ // resolves. Runtime discovery will overwrite this with the real routes.
435
+ if (Object.keys(result.routes).length === 0) {
436
+ if (!existing) {
437
+ const emptySource = generateRouteTypesSource({});
438
+ writeFileSync(outPath, emptySource);
439
+ }
440
+ continue;
441
+ }
442
+
443
+ const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
444
+ const source = generateRouteTypesSource(
445
+ result.routes,
446
+ hasSearchSchemas ? result.searchSchemas : undefined,
447
+ );
448
+ if (existing !== source) {
449
+ // On initial dev startup, don't overwrite a file from runtime discovery
450
+ // (which has all dynamic routes) with a smaller set from the static
451
+ // parser. The static parser can't see routes generated by Array.from()
452
+ // or other dynamic code. During HMR (file watcher), always write so
453
+ // newly added routes appear immediately.
454
+ if (opts?.preserveIfLarger && existing) {
455
+ const existingCount = countPublicRouteEntries(existing);
456
+ const newCount = Object.keys(result.routes).filter(
457
+ (name) => !isAutoGeneratedRouteName(name),
458
+ ).length;
459
+ if (existingCount > newCount) {
460
+ continue;
461
+ }
462
+ }
463
+ writeFileSync(outPath, source);
464
+ console.log(
465
+ `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
466
+ );
467
+ }
468
+ }
469
+ }
@@ -0,0 +1,78 @@
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 (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
65
+ results.push(...findTsFiles(fullPath, filter));
66
+ } else if (
67
+ (entry.name.endsWith(".ts") ||
68
+ entry.name.endsWith(".tsx") ||
69
+ entry.name.endsWith(".js") ||
70
+ entry.name.endsWith(".jsx")) &&
71
+ !entry.name.includes(".gen.")
72
+ ) {
73
+ if (filter && !filter(fullPath)) continue;
74
+ results.push(fullPath);
75
+ }
76
+ }
77
+ return results;
78
+ }