@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -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 +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  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 +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +92 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +773 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1323
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,1038 +1,36 @@
1
- import { readFileSync, writeFileSync, existsSync, readdirSync, unlinkSync } from "node:fs";
2
- import { join, dirname, resolve, relative, basename as pathBasename } from "node:path";
3
- // @ts-ignore -- picomatch ships no .d.ts; types are trivial
4
- import picomatch from "picomatch";
5
-
6
- /**
7
- * Extract route definitions from source code by statically parsing path() calls.
8
- * No code execution needed -- works on raw source text.
9
- *
10
- * Handles multi-line handlers with JSX, nested braces, string literals,
11
- * and comments. Skips unnamed paths (no { name: "..." }).
12
- */
13
- export function extractRoutesFromSource(
14
- code: string
15
- ): Array<{ name: string; pattern: string; search?: Record<string, string> }> {
16
- const routes: Array<{ name: string; pattern: string; search?: Record<string, string> }> = [];
17
- // Match `path(...)` and typed helpers like `path.json(...)`, `path.md(...)`.
18
- // Keep this generic so new helpers are picked up without parser updates.
19
- const regex = /\bpath(?:\.[a-zA-Z_$][\w$]*)?\s*\(/g;
20
- let match;
21
-
22
- while ((match = regex.exec(code)) !== null) {
23
- const result = parsePathCall(code, match.index + match[0].length);
24
- if (result) routes.push(result);
25
- }
26
-
27
- return routes;
28
- }
29
-
30
- /**
31
- * Generate a per-module types file from extracted routes.
32
- * Output has zero imports, preventing circular references.
33
- */
34
- export function generatePerModuleTypesSource(
35
- routes: Array<{ name: string; pattern: string; search?: Record<string, string> }>
36
- ): string {
37
- const valid = routes.filter(({ name }) => {
38
- if (!name || /["'\\`\n\r]/.test(name)) {
39
- console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
40
- return false;
41
- }
42
- return true;
43
- });
44
-
45
- // Deduplicate by name (last definition wins for same name)
46
- const deduped = new Map<string, { pattern: string; search?: Record<string, string> }>();
47
- for (const { name, pattern, search } of valid) {
48
- deduped.set(name, { pattern, search });
49
- }
50
- const sorted = [...deduped.entries()]
51
- .sort(([a], [b]) => a.localeCompare(b));
52
- const body = sorted
53
- .map(([name, { pattern, search }]) => {
54
- // Quote names that aren't valid bare identifiers (dots, dashes, etc.)
55
- const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
56
- if (search && Object.keys(search).length > 0) {
57
- const searchBody = Object.entries(search)
58
- .map(([k, v]) => `${k}: "${v}"`)
59
- .join(", ");
60
- return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
61
- }
62
- return ` ${key}: "${pattern}",`;
63
- })
64
- .join("\n");
65
- return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // Mini-parser internals
70
- // ---------------------------------------------------------------------------
71
-
72
- function isWhitespace(ch: string): boolean {
73
- return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
74
- }
75
-
76
- /** Read a single- or double-quoted string literal starting at pos. */
77
- function readString(
78
- code: string,
79
- pos: number
80
- ): { value: string; end: number } | null {
81
- const quote = code[pos];
82
- if (quote !== '"' && quote !== "'") return null;
83
-
84
- let value = "";
85
- pos++;
86
- while (pos < code.length) {
87
- if (code[pos] === "\\") {
88
- pos++;
89
- if (pos < code.length) {
90
- value += code[pos];
91
- pos++;
92
- }
93
- continue;
94
- }
95
- if (code[pos] === quote) {
96
- return { value, end: pos + 1 };
97
- }
98
- value += code[pos];
99
- pos++;
100
- }
101
- return null;
102
- }
103
-
104
- /** Skip past any string literal (single, double, or template). */
105
- function skipStringLiteral(code: string, pos: number): number {
106
- const quote = code[pos];
107
-
108
- if (quote === "`") {
109
- pos++;
110
- while (pos < code.length) {
111
- if (code[pos] === "\\") {
112
- pos += 2;
113
- continue;
114
- }
115
- if (code[pos] === "`") return pos + 1;
116
- if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
117
- pos += 2;
118
- let braceDepth = 1;
119
- while (pos < code.length && braceDepth > 0) {
120
- if (code[pos] === "{") braceDepth++;
121
- else if (code[pos] === "}") braceDepth--;
122
- else if (code[pos] === "\\") pos++;
123
- else if (
124
- code[pos] === '"' ||
125
- code[pos] === "'" ||
126
- code[pos] === "`"
127
- ) {
128
- pos = skipStringLiteral(code, pos);
129
- continue;
130
- }
131
- if (braceDepth > 0) pos++;
132
- }
133
- continue;
134
- }
135
- pos++;
136
- }
137
- return pos;
138
- }
139
-
140
- // Simple single/double quoted string
141
- pos++;
142
- while (pos < code.length) {
143
- if (code[pos] === "\\") {
144
- pos += 2;
145
- continue;
146
- }
147
- if (code[pos] === quote) return pos + 1;
148
- pos++;
149
- }
150
- return pos;
151
- }
152
-
153
- /**
154
- * Check if code at pos starts with `name` as a standalone identifier
155
- * followed by `:` (an object property).
156
- */
157
- function matchesNameColon(code: string, pos: number): boolean {
158
- if (code.slice(pos, pos + 4) !== "name") return false;
159
- if (pos > 0 && /\w/.test(code[pos - 1])) return false;
160
- const afterName = pos + 4;
161
- if (afterName < code.length && /\w/.test(code[afterName])) return false;
162
- let checkPos = afterName;
163
- while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
164
- return code[checkPos] === ":";
165
- }
166
-
167
- /** Extract the string value after `name:` starting at the `n` of `name`. */
168
- function extractNameValue(
169
- code: string,
170
- pos: number
171
- ): { value: string; end: number } | null {
172
- pos += 4; // skip 'name'
173
- while (pos < code.length && isWhitespace(code[pos])) pos++;
174
- pos++; // skip ':'
175
- while (pos < code.length && isWhitespace(code[pos])) pos++;
176
- return readString(code, pos);
177
- }
178
-
179
- /**
180
- * Parse a single path() call starting right after the opening paren.
181
- * Returns { name, pattern } or null if the call is unnamed.
182
- */
183
- /**
184
- * Check if code at pos starts with `search` as a standalone identifier
185
- * followed by `:` (an object property).
186
- */
187
- function matchesSearchColon(code: string, pos: number): boolean {
188
- if (code.slice(pos, pos + 6) !== "search") return false;
189
- if (pos > 0 && /\w/.test(code[pos - 1])) return false;
190
- const afterSearch = pos + 6;
191
- if (afterSearch < code.length && /\w/.test(code[afterSearch])) return false;
192
- let checkPos = afterSearch;
193
- while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
194
- return code[checkPos] === ":";
195
- }
196
-
197
- /**
198
- * Extract a search schema object literal after `search:`.
199
- * Parses { key: "type", key2: "type2" } at the position after `search:`.
200
- * Returns the parsed schema and end position, or null if not an object literal.
201
- */
202
- function extractSearchValue(
203
- code: string,
204
- pos: number
205
- ): { value: Record<string, string>; end: number } | null {
206
- pos += 6; // skip 'search'
207
- while (pos < code.length && isWhitespace(code[pos])) pos++;
208
- pos++; // skip ':'
209
- while (pos < code.length && isWhitespace(code[pos])) pos++;
210
-
211
- if (code[pos] !== "{") return null;
212
- pos++; // skip '{'
213
-
214
- const schema: Record<string, string> = {};
215
-
216
- while (pos < code.length) {
217
- while (pos < code.length && isWhitespace(code[pos])) pos++;
218
- if (code[pos] === "}") return { value: schema, end: pos + 1 };
219
- if (code[pos] === ",") { pos++; continue; }
220
-
221
- // Parse key (identifier or string)
222
- let key: string;
223
- if (code[pos] === '"' || code[pos] === "'") {
224
- const keyStr = readString(code, pos);
225
- if (!keyStr) return null;
226
- key = keyStr.value;
227
- pos = keyStr.end;
228
- } else {
229
- const keyStart = pos;
230
- while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
231
- if (pos === keyStart) return null;
232
- key = code.slice(keyStart, pos);
233
- }
234
-
235
- // Skip colon
236
- while (pos < code.length && isWhitespace(code[pos])) pos++;
237
- if (code[pos] !== ":") return null;
238
- pos++;
239
- while (pos < code.length && isWhitespace(code[pos])) pos++;
240
-
241
- // Parse value (must be a string literal)
242
- const valStr = readString(code, pos);
243
- if (!valStr) return null;
244
- schema[key] = valStr.value;
245
- pos = valStr.end;
246
- }
247
-
248
- return null;
249
- }
250
-
251
- function parsePathCall(
252
- code: string,
253
- pos: number
254
- ): { name: string; pattern: string; search?: Record<string, string> } | null {
255
- // Skip whitespace to first argument
256
- while (pos < code.length && isWhitespace(code[pos])) pos++;
257
-
258
- // First argument must be a string literal (the pattern)
259
- const patternStr = readString(code, pos);
260
- if (!patternStr) return null;
261
- const pattern = patternStr.value;
262
- pos = patternStr.end;
263
-
264
- // Scan the rest of the call tracking depth.
265
- // depth=1: inside path(), depth=2: inside an object/paren at top level of call.
266
- // We look for `name: "..."` and `search: { ... }` at depth 2 (options object properties).
267
- let depth = 1;
268
- let name: string | null = null;
269
- let search: Record<string, string> | undefined;
270
-
271
- while (pos < code.length && depth > 0) {
272
- const ch = code[pos];
273
-
274
- if (isWhitespace(ch)) {
275
- pos++;
276
- continue;
277
- }
278
-
279
- // Line comment
280
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
281
- pos += 2;
282
- while (pos < code.length && code[pos] !== "\n") pos++;
283
- continue;
284
- }
285
-
286
- // Block comment
287
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
288
- pos += 2;
289
- while (
290
- pos < code.length - 1 &&
291
- !(code[pos] === "*" && code[pos + 1] === "/")
292
- )
293
- pos++;
294
- pos += 2;
295
- continue;
296
- }
297
-
298
- // At depth 2 (inside an object at call top-level), look for name: "..." and search: { ... }
299
- if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
300
- const nameResult = extractNameValue(code, pos);
301
- if (nameResult) {
302
- name = nameResult.value;
303
- pos = nameResult.end;
304
- continue;
305
- }
306
- }
307
-
308
- if (depth === 2 && ch === "s" && matchesSearchColon(code, pos)) {
309
- const searchResult = extractSearchValue(code, pos);
310
- if (searchResult) {
311
- search = searchResult.value;
312
- pos = searchResult.end;
313
- continue;
314
- }
315
- }
316
-
317
- // Skip string literals.
318
- // Treat ' preceded by a word char as an apostrophe (e.g. "shouldn't"),
319
- // not a string delimiter. In valid JS/TS, opening ' is never preceded
320
- // by a word character.
321
- if (ch === '"' || ch === "`" || (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))) {
322
- pos = skipStringLiteral(code, pos);
323
- continue;
324
- }
325
-
326
- // Track depth
327
- if (ch === "(" || ch === "{" || ch === "[") depth++;
328
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
329
-
330
- pos++;
331
- }
332
-
333
- if (name === null) return null;
334
- return { name, pattern, ...(search ? { search } : {}) };
335
- }
336
-
337
- /**
338
- * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
339
- * with route name -> pattern mappings. This enables Handler<"routeName">
340
- * without circular references since the file has no imports from the app.
341
- */
342
- export function generateRouteTypesSource(
343
- routeManifest: Record<string, string>,
344
- searchSchemas?: Record<string, Record<string, string>>
345
- ): string {
346
- const entries = Object.entries(routeManifest).sort(([a], [b]) =>
347
- a.localeCompare(b)
348
- );
349
-
350
- const objectBody = entries
351
- .map(([name, pattern]) => {
352
- const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
353
- const search = searchSchemas?.[name];
354
- if (search && Object.keys(search).length > 0) {
355
- const searchBody = Object.entries(search)
356
- .map(([k, v]) => `${k}: "${v}"`)
357
- .join(", ");
358
- return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
359
- }
360
- return ` ${key}: "${pattern}",`;
361
- })
362
- .join("\n");
363
-
364
- return `// Auto-generated by @rangojs/router - do not edit
365
- export const NamedRoutes = {
366
- ${objectBody}
367
- } as const;
368
-
369
- declare global {
370
- namespace RSCRouter {
371
- interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
372
- }
373
- }
374
- `;
375
- }
376
-
377
- /** Default exclude patterns for route type scanning. */
378
- export const DEFAULT_EXCLUDE_PATTERNS: string[] = [
379
- "**/__tests__/**",
380
- "**/__mocks__/**",
381
- "**/dist/**",
382
- "**/coverage/**",
383
- "**/*.test.{ts,tsx}",
384
- "**/*.spec.{ts,tsx}",
385
- ];
386
-
387
- export type ScanFilter = (absolutePath: string) => boolean;
388
-
389
- /**
390
- * Compile include/exclude glob patterns into a single predicate.
391
- * Paths are made root-relative before matching.
392
- * Returns undefined when no filtering is needed (no include, default exclude).
393
- */
394
- export function createScanFilter(
395
- root: string,
396
- opts: { include?: string[]; exclude?: string[] },
397
- ): ScanFilter | undefined {
398
- const { include, exclude } = opts;
399
- const hasInclude = include && include.length > 0;
400
- const hasCustomExclude = exclude !== undefined;
401
-
402
- if (!hasInclude && !hasCustomExclude) return undefined;
403
-
404
- const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
405
- const includeMatcher = hasInclude ? picomatch(include) : null;
406
- const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
407
-
408
- return (absolutePath: string) => {
409
- const rel = relative(root, absolutePath);
410
- if (excludeMatcher && excludeMatcher(rel)) return false;
411
- if (includeMatcher) return includeMatcher(rel);
412
- return true;
413
- };
414
- }
415
-
416
- /**
417
- * Recursively find .ts/.tsx files under a directory, skipping node_modules
418
- * and .gen. files.
419
- */
420
- export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
421
- const results: string[] = [];
422
- let entries;
423
- try {
424
- entries = readdirSync(dir, { withFileTypes: true });
425
- } catch (err) {
426
- console.warn(`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`);
427
- return results;
428
- }
429
- for (const entry of entries) {
430
- const fullPath = join(dir, entry.name);
431
- if (entry.isDirectory()) {
432
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
433
- results.push(...findTsFiles(fullPath, filter));
434
- } else if (
435
- (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
436
- !entry.name.includes(".gen.")
437
- ) {
438
- if (filter && !filter(fullPath)) continue;
439
- results.push(fullPath);
440
- }
441
- }
442
- return results;
443
- }
444
-
445
- /**
446
- * Generate per-module route type files by statically parsing url module source.
447
- * Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
448
- * extracted route name/pattern pairs. Only writes when content has changed.
449
- */
450
- export function writePerModuleRouteTypes(root: string, filter?: ScanFilter): void {
451
- const files = findTsFiles(root, filter);
452
- for (const filePath of files) {
453
- writePerModuleRouteTypesForFile(filePath);
454
- }
455
- }
456
-
457
- /**
458
- * Generate per-module route types for a single url module file.
459
- * No-ops if the file doesn't contain `urls(` or has no named routes.
460
- */
461
- export function writePerModuleRouteTypesForFile(filePath: string): void {
462
- try {
463
- const source = readFileSync(filePath, "utf-8");
464
- if (!source.includes("urls(")) return;
465
-
466
- const routes = extractRoutesFromSource(source);
467
- if (routes.length === 0) return;
468
-
469
- const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
470
- const genSource = generatePerModuleTypesSource(routes);
471
- const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
472
- if (existing !== genSource) {
473
- writeFileSync(genPath, genSource);
474
- console.log(`[rsc-router] Generated route types -> ${genPath}`);
475
- }
476
- } catch (err) {
477
- console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`);
478
- }
479
- }
480
-
481
- // ---------------------------------------------------------------------------
482
- // Static include() parsing
483
- // ---------------------------------------------------------------------------
484
-
485
- /**
486
- * Extract include() calls from source code by statically parsing.
487
- * Returns the path prefix, variable name, and optional name prefix for each.
488
- */
489
- export function extractIncludesFromSource(
490
- code: string
491
- ): Array<{ pathPrefix: string; variableName: string; namePrefix: string | null }> {
492
- const results: Array<{
493
- pathPrefix: string;
494
- variableName: string;
495
- namePrefix: string | null;
496
- }> = [];
497
- const regex = /\binclude\s*\(/g;
498
- let match;
499
-
500
- while ((match = regex.exec(code)) !== null) {
501
- const result = parseIncludeCall(code, match.index + match[0].length);
502
- if (result) results.push(result);
503
- }
504
-
505
- return results;
506
- }
507
-
508
- /**
509
- * Parse a single include() call starting right after the opening paren.
510
- * Expects: include("prefix", variableName, { name: "prefix" })
511
- */
512
- function parseIncludeCall(
513
- code: string,
514
- pos: number
515
- ): {
516
- pathPrefix: string;
517
- variableName: string;
518
- namePrefix: string | null;
519
- } | null {
520
- // Skip whitespace to first argument
521
- while (pos < code.length && isWhitespace(code[pos])) pos++;
522
-
523
- // First arg: string literal (pathPrefix)
524
- const prefixStr = readString(code, pos);
525
- if (!prefixStr) return null;
526
- const pathPrefix = prefixStr.value;
527
- pos = prefixStr.end;
528
-
529
- // Comma
530
- while (pos < code.length && isWhitespace(code[pos])) pos++;
531
- if (pos >= code.length || code[pos] !== ",") return null;
532
- pos++;
533
- while (pos < code.length && isWhitespace(code[pos])) pos++;
534
-
535
- // Second arg: identifier (variableName)
536
- const varStart = pos;
537
- while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
538
- if (pos === varStart) return null;
539
- const variableName = code.slice(varStart, pos);
540
-
541
- // Scan rest of call for optional { name: "..." }
542
- let namePrefix: string | null = null;
543
- let depth = 1; // inside include()
544
-
545
- while (pos < code.length && depth > 0) {
546
- const ch = code[pos];
547
-
548
- if (isWhitespace(ch)) {
549
- pos++;
550
- continue;
551
- }
552
-
553
- // Line comment
554
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
555
- pos += 2;
556
- while (pos < code.length && code[pos] !== "\n") pos++;
557
- continue;
558
- }
559
-
560
- // Block comment
561
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
562
- pos += 2;
563
- while (
564
- pos < code.length - 1 &&
565
- !(code[pos] === "*" && code[pos + 1] === "/")
566
- )
567
- pos++;
568
- pos += 2;
569
- continue;
570
- }
571
-
572
- // At depth 2 (inside options object), look for name: "..."
573
- if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
574
- const nameResult = extractNameValue(code, pos);
575
- if (nameResult) {
576
- namePrefix = nameResult.value;
577
- pos = nameResult.end;
578
- continue;
579
- }
580
- }
581
-
582
- // Skip string literals
583
- if (
584
- ch === '"' ||
585
- ch === "`" ||
586
- (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
587
- ) {
588
- pos = skipStringLiteral(code, pos);
589
- continue;
590
- }
591
-
592
- // Track depth
593
- if (ch === "(" || ch === "{" || ch === "[") depth++;
594
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
595
-
596
- pos++;
597
- }
598
-
599
- return { pathPrefix, variableName, namePrefix };
600
- }
601
-
602
- // ---------------------------------------------------------------------------
603
- // Import resolution
604
- // ---------------------------------------------------------------------------
605
-
606
- /**
607
- * Find the import statement for a local variable name.
608
- * Returns the import specifier and the exported name from the source module.
609
- */
610
- function resolveImportedVariable(
611
- code: string,
612
- localName: string
613
- ): { specifier: string; exportedName: string } | null {
614
- const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
615
- let match;
616
-
617
- while ((match = importRegex.exec(code)) !== null) {
618
- const imports = match[1];
619
- const specifier = match[2];
620
-
621
- const parts = imports
622
- .split(",")
623
- .map((s) => s.trim())
624
- .filter(Boolean);
625
- for (const part of parts) {
626
- const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
627
- if (asMatch && asMatch[2] === localName)
628
- return { specifier, exportedName: asMatch[1] };
629
- if (part === localName) return { specifier, exportedName: localName };
630
- }
631
- }
632
-
633
- return null;
634
- }
635
-
636
- /**
637
- * Resolve an import specifier relative to the importing file.
638
- * Strips .js/.mjs extensions and tries .ts/.tsx candidates.
639
- */
640
- function resolveImportPath(
641
- importSpec: string,
642
- fromFile: string
643
- ): string | null {
644
- if (!importSpec.startsWith(".")) return null;
645
-
646
- const dir = dirname(fromFile);
647
- let base = importSpec;
648
- if (base.endsWith(".js")) base = base.slice(0, -3);
649
- else if (base.endsWith(".mjs")) base = base.slice(0, -4);
650
-
651
- const candidates = [
652
- resolve(dir, base + ".ts"),
653
- resolve(dir, base + ".tsx"),
654
- resolve(dir, base + "/index.ts"),
655
- resolve(dir, base + "/index.tsx"),
656
- ];
657
-
658
- for (const candidate of candidates) {
659
- if (existsSync(candidate)) return candidate;
660
- }
661
- return null;
662
- }
663
-
664
- // ---------------------------------------------------------------------------
665
- // urls() block extraction for same-file variables
666
- // ---------------------------------------------------------------------------
667
-
668
- function escapeRegExp(s: string): string {
669
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
670
- }
671
-
672
- /**
673
- * Extract the source of a specific `const varName = urls(...)` block.
674
- * Used for same-file variables where include() references a urls() defined
675
- * in the same module rather than imported.
676
- */
677
- function extractUrlsBlockForVariable(
678
- code: string,
679
- varName: string
680
- ): string | null {
681
- const pattern = new RegExp(
682
- `(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*urls\\s*\\(`
683
- );
684
- const match = pattern.exec(code);
685
- if (!match) return null;
686
-
687
- // Start from the opening paren of urls(
688
- const openParen = match.index + match[0].length - 1;
689
- let depth = 1;
690
- let pos = openParen + 1;
691
-
692
- while (pos < code.length && depth > 0) {
693
- const ch = code[pos];
694
-
695
- // Skip strings
696
- if (
697
- ch === '"' ||
698
- ch === "`" ||
699
- (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
700
- ) {
701
- pos = skipStringLiteral(code, pos);
702
- continue;
703
- }
704
-
705
- // Line comment
706
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
707
- pos += 2;
708
- while (pos < code.length && code[pos] !== "\n") pos++;
709
- continue;
710
- }
711
-
712
- // Block comment
713
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
714
- pos += 2;
715
- while (
716
- pos < code.length - 1 &&
717
- !(code[pos] === "*" && code[pos + 1] === "/")
718
- )
719
- pos++;
720
- pos += 2;
721
- continue;
722
- }
723
-
724
- if (ch === "(" || ch === "{" || ch === "[") depth++;
725
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
726
-
727
- pos++;
728
- }
729
-
730
- return code.slice(openParen, pos);
731
- }
732
-
733
- // ---------------------------------------------------------------------------
734
- // Combined route map building
735
- // ---------------------------------------------------------------------------
736
-
737
- /**
738
- * Recursively build a route map from a urls module file.
739
- * Extracts local path() routes and follows include() calls to sub-modules.
740
- * Handles both imported and same-file variables.
741
- */
742
- export function buildCombinedRouteMap(
743
- filePath: string,
744
- variableName?: string,
745
- visited?: Set<string>
746
- ): Record<string, string> {
747
- visited = visited ?? new Set();
748
- const realPath = resolve(filePath);
749
- const key = variableName ? `${realPath}:${variableName}` : realPath;
750
- if (visited.has(key)) return {};
751
- visited.add(key);
752
-
753
- let source: string;
754
- try {
755
- source = readFileSync(realPath, "utf-8");
756
- } catch {
757
- return {};
758
- }
759
-
760
- // If a specific variable is requested, extract just its urls() block
761
- let block: string;
762
- if (variableName) {
763
- const extracted = extractUrlsBlockForVariable(source, variableName);
764
- if (!extracted) return {};
765
- block = extracted;
766
- } else {
767
- block = source;
768
- }
769
-
770
- return buildRouteMapFromBlock(block, source, realPath, visited);
771
- }
772
-
773
- function buildRouteMapFromBlock(
774
- block: string,
775
- fullSource: string,
776
- filePath: string,
777
- visited: Set<string>,
778
- searchSchemasOut?: Record<string, Record<string, string>>
779
- ): Record<string, string> {
780
- const routeMap: Record<string, string> = {};
781
-
782
- // Extract local path() routes
783
- const localRoutes = extractRoutesFromSource(block);
784
- for (const { name, pattern, search } of localRoutes) {
785
- routeMap[name] = pattern;
786
- if (search && searchSchemasOut) {
787
- searchSchemasOut[name] = search;
788
- }
789
- }
790
-
791
- // Extract include() calls
792
- const includes = extractIncludesFromSource(block);
793
- for (const { pathPrefix, variableName, namePrefix } of includes) {
794
- let childResult: { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> };
795
-
796
- // Try import resolution first
797
- const imported = resolveImportedVariable(fullSource, variableName);
798
- if (imported) {
799
- const targetFile = resolveImportPath(imported.specifier, filePath);
800
- if (!targetFile) continue;
801
- childResult = buildCombinedRouteMapWithSearch(
802
- targetFile,
803
- imported.exportedName,
804
- visited
805
- );
806
- } else {
807
- // Same-file variable
808
- childResult = buildCombinedRouteMapWithSearch(filePath, variableName, visited);
809
- }
810
-
811
- // Apply prefixes
812
- for (const [name, pattern] of Object.entries(childResult.routes)) {
813
- const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
814
- let prefixedPattern: string;
815
- if (pattern === "/") {
816
- prefixedPattern = pathPrefix || "/";
817
- } else if (pathPrefix.endsWith("/") && pattern.startsWith("/")) {
818
- prefixedPattern = pathPrefix + pattern.slice(1);
819
- } else {
820
- prefixedPattern = pathPrefix + pattern;
821
- }
822
- routeMap[prefixedName] = prefixedPattern;
823
- // Propagate search schemas with prefix
824
- if (childResult.searchSchemas[name] && searchSchemasOut) {
825
- searchSchemasOut[prefixedName] = childResult.searchSchemas[name];
826
- }
827
- }
828
- }
829
-
830
- return routeMap;
831
- }
832
-
833
- /**
834
- * Build route map and search schemas together.
835
- * Internal helper used by the include resolution path.
836
- */
837
- function buildCombinedRouteMapWithSearch(
838
- filePath: string,
839
- variableName?: string,
840
- visited?: Set<string>
841
- ): { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> } {
842
- visited = visited ?? new Set();
843
- const realPath = resolve(filePath);
844
- const key = variableName ? `${realPath}:${variableName}` : realPath;
845
- if (visited.has(key)) return { routes: {}, searchSchemas: {} };
846
- visited.add(key);
847
-
848
- let source: string;
849
- try {
850
- source = readFileSync(realPath, "utf-8");
851
- } catch {
852
- return { routes: {}, searchSchemas: {} };
853
- }
854
-
855
- let block: string;
856
- if (variableName) {
857
- const extracted = extractUrlsBlockForVariable(source, variableName);
858
- if (!extracted) return { routes: {}, searchSchemas: {} };
859
- block = extracted;
860
- } else {
861
- block = source;
862
- }
863
-
864
- const searchSchemas: Record<string, Record<string, string>> = {};
865
- const routes = buildRouteMapFromBlock(block, source, realPath, visited, searchSchemas);
866
- return { routes, searchSchemas };
867
- }
868
-
869
- // ---------------------------------------------------------------------------
870
- // Router file URL extraction
871
- // ---------------------------------------------------------------------------
872
-
873
- /**
874
- * Extract the url patterns variable from a router file.
875
- * Looks for patterns like:
876
- * .routes(variableName)
877
- * urls: variableName
878
- * Returns the local variable name and optional import info.
879
- */
880
- function extractUrlsVariableFromRouter(
881
- code: string
882
- ): string | null {
883
- // Pattern 1: .routes(variableName) where variableName is an identifier (not a string)
884
- const routesCallMatch = code.match(/\.routes\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/);
885
- if (routesCallMatch) return routesCallMatch[1];
886
-
887
- // Pattern 2: urls: variableName in createRouter options
888
- const urlsOptionMatch = code.match(/urls\s*:\s*([a-zA-Z_$][\w$]*)/);
889
- if (urlsOptionMatch) return urlsOptionMatch[1];
890
-
891
- return null;
892
- }
893
-
894
- /**
895
- * Resolve routes and search schemas from a router source file by following the
896
- * variable passed to `.routes(...)` or `urls: ...` in createRouter options.
897
- */
898
- export function buildCombinedRouteMapForRouterFile(
899
- routerFilePath: string,
900
- ): { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> } {
901
- let routerSource: string;
902
- try {
903
- routerSource = readFileSync(routerFilePath, "utf-8");
904
- } catch {
905
- return { routes: {}, searchSchemas: {} };
906
- }
907
-
908
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
909
- if (!urlsVarName) {
910
- return { routes: {}, searchSchemas: {} };
911
- }
912
-
913
- const imported = resolveImportedVariable(routerSource, urlsVarName);
914
- if (imported) {
915
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
916
- if (!targetFile) {
917
- return { routes: {}, searchSchemas: {} };
918
- }
919
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
920
- }
921
-
922
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
923
- }
924
-
925
- // ---------------------------------------------------------------------------
926
- // Per-router named-routes.gen.ts writer
927
- // ---------------------------------------------------------------------------
928
-
929
- /**
930
- * Scan for files containing createRouter() and return their paths.
931
- * Call once at startup; the result can be reused on subsequent watcher triggers.
932
- */
933
- export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
934
- const files = findTsFiles(root, filter);
935
- const result: string[] = [];
936
- for (const filePath of files) {
937
- if (filePath.includes(".gen.")) continue;
938
- try {
939
- const source = readFileSync(filePath, "utf-8");
940
- if (/\bcreateRouter\s*[<(]/.test(source)) {
941
- result.push(filePath);
942
- }
943
- } catch {
944
- continue;
945
- }
946
- }
947
- return result;
948
- }
949
-
950
- /**
951
- * Generate per-router named-routes.gen.ts files from known router file paths.
952
- * Re-reads each router file and resolves url patterns via static source parsing.
953
- *
954
- * Pass `knownRouterFiles` from a previous `findRouterFiles()` call to skip the
955
- * full directory scan. If omitted, falls back to scanning (startup path).
956
- */
957
- /**
958
- * Write named-routes.gen.ts files from static source parsing.
959
- * Dev-only: provides initial .gen.ts files for IDE types before runtime
960
- * discovery runs. Must NOT be called during production builds — runtime
961
- * discovery in buildStart produces the definitive file.
962
- */
963
- export function writeCombinedRouteTypes(root: string, knownRouterFiles?: string[], opts?: { preserveIfLarger?: boolean }): void {
964
- // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
965
- try {
966
- const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
967
- if (existsSync(oldCombinedPath)) {
968
- unlinkSync(oldCombinedPath);
969
- console.log(`[rsc-router] Removed stale combined route types: ${oldCombinedPath}`);
970
- }
971
- } catch {}
972
-
973
- const routerFilePaths = knownRouterFiles ?? findRouterFiles(root);
974
- if (routerFilePaths.length === 0) return;
975
-
976
- for (const routerFilePath of routerFilePaths) {
977
- let routerSource: string;
978
- try {
979
- routerSource = readFileSync(routerFilePath, "utf-8");
980
- } catch {
981
- continue;
982
- }
983
- // Extract the urls variable name from .routes(varName) or urls: varName
984
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
985
- if (!urlsVarName) continue;
986
-
987
- // Resolve the variable to its source module
988
- let result: { routes: Record<string, string>; searchSchemas: Record<string, Record<string, string>> };
989
-
990
- const imported = resolveImportedVariable(routerSource, urlsVarName);
991
- if (imported) {
992
- // Variable is imported from another module
993
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
994
- if (!targetFile) continue;
995
- result = buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
996
- } else {
997
- // Variable is defined in the same file
998
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
999
- }
1000
-
1001
- const routerBasename = pathBasename(routerFilePath).replace(/\.(tsx?|jsx?)$/, "");
1002
- const outPath = join(dirname(routerFilePath), `${routerBasename}.named-routes.gen.ts`);
1003
- const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
1004
-
1005
- // When the static parser can't extract routes (e.g. callback-style urls()),
1006
- // write an empty placeholder so the build-time transform's injected import
1007
- // resolves. Runtime discovery will overwrite this with the real routes.
1008
- if (Object.keys(result.routes).length === 0) {
1009
- if (!existing) {
1010
- const emptySource = generateRouteTypesSource({});
1011
- writeFileSync(outPath, emptySource);
1012
- }
1013
- continue;
1014
- }
1015
-
1016
- const hasSearchSchemas = Object.keys(result.searchSchemas).length > 0;
1017
- const source = generateRouteTypesSource(
1018
- result.routes,
1019
- hasSearchSchemas ? result.searchSchemas : undefined
1020
- );
1021
- if (existing !== source) {
1022
- // On initial dev startup, don't overwrite a file from runtime discovery
1023
- // (which has all dynamic routes) with a smaller set from the static
1024
- // parser. The static parser can't see routes generated by Array.from()
1025
- // or other dynamic code. During HMR (file watcher), always write so
1026
- // newly added routes appear immediately.
1027
- if (opts?.preserveIfLarger && existing) {
1028
- const existingCount = (existing.match(/^\s+["a-zA-Z_$][^:]*:\s*"/gm) || []).length;
1029
- const newCount = Object.keys(result.routes).length;
1030
- if (existingCount > newCount) {
1031
- continue;
1032
- }
1033
- }
1034
- writeFileSync(outPath, source);
1035
- console.log(`[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`);
1036
- }
1037
- }
1038
- }
1
+ // Barrel re-export -- see route-types/ for implementations.
2
+ export {
3
+ extractParamsFromPattern,
4
+ formatRouteEntry,
5
+ } from "./route-types/param-extraction.js";
6
+ export { extractRoutesFromSource } from "./route-types/ast-route-extraction.js";
7
+ export {
8
+ generatePerModuleTypesSource,
9
+ generateRouteTypesSource,
10
+ } from "./route-types/codegen.js";
11
+ export {
12
+ DEFAULT_EXCLUDE_PATTERNS,
13
+ type ScanFilter,
14
+ createScanFilter,
15
+ findTsFiles,
16
+ } from "./route-types/scan-filter.js";
17
+ export {
18
+ writePerModuleRouteTypes,
19
+ writePerModuleRouteTypesForFile,
20
+ } from "./route-types/per-module-writer.js";
21
+ export {
22
+ type UnresolvableReason,
23
+ type UnresolvableInclude,
24
+ extractIncludesWithDiagnostics,
25
+ } from "./route-types/include-resolution.js";
26
+ export {
27
+ extractUrlsVariableFromRouter,
28
+ buildCombinedRouteMapForRouterFile,
29
+ detectUnresolvableIncludes,
30
+ detectUnresolvableIncludesForUrlsFile,
31
+ findNestedRouterConflict,
32
+ formatNestedRouterConflictError,
33
+ findRouterFiles,
34
+ writeCombinedRouteTypes,
35
+ } from "./route-types/router-processing.js";
36
+ export { findUrlsVariableNames } from "./route-types/per-module-writer.js";