@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,411 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import { getStringValue } from "./ast-helpers.js";
5
+ import { extractRoutesFromSource } from "./ast-route-extraction.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Unresolvable include diagnostics
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type UnresolvableReason =
12
+ | "factory-call"
13
+ | "dynamic-expression"
14
+ | "unresolvable-import"
15
+ | "file-not-found";
16
+
17
+ export interface UnresolvableInclude {
18
+ pathPrefix: string;
19
+ namePrefix: string | null;
20
+ reason: UnresolvableReason;
21
+ sourceFile: string;
22
+ detail: string;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // AST-based include() parsing
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function extractNamePrefixFromInclude(node: ts.CallExpression): string | null {
30
+ if (node.arguments.length >= 3) {
31
+ const thirdArg = node.arguments[2];
32
+ if (ts.isObjectLiteralExpression(thirdArg)) {
33
+ for (const prop of thirdArg.properties) {
34
+ if (!ts.isPropertyAssignment(prop)) continue;
35
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
36
+ if (propName === "name") {
37
+ return getStringValue(prop.initializer);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Extract include() calls with diagnostics for unresolvable ones.
47
+ * Returns both resolved includes (identifier second args) and unresolvable
48
+ * includes (factory calls, etc.) with reasons.
49
+ */
50
+ export function extractIncludesWithDiagnostics(code: string): {
51
+ resolved: Array<{
52
+ pathPrefix: string;
53
+ variableName: string;
54
+ namePrefix: string | null;
55
+ }>;
56
+ unresolvable: Array<{
57
+ pathPrefix: string;
58
+ namePrefix: string | null;
59
+ reason: UnresolvableReason;
60
+ detail: string;
61
+ }>;
62
+ } {
63
+ const sourceFile = ts.createSourceFile(
64
+ "input.tsx",
65
+ code,
66
+ ts.ScriptTarget.Latest,
67
+ true,
68
+ ts.ScriptKind.TSX,
69
+ );
70
+ const resolved: Array<{
71
+ pathPrefix: string;
72
+ variableName: string;
73
+ namePrefix: string | null;
74
+ }> = [];
75
+ const unresolvable: Array<{
76
+ pathPrefix: string;
77
+ namePrefix: string | null;
78
+ reason: UnresolvableReason;
79
+ detail: string;
80
+ }> = [];
81
+
82
+ function visit(node: ts.Node) {
83
+ if (ts.isCallExpression(node)) {
84
+ const callee = node.expression;
85
+ if (ts.isIdentifier(callee) && callee.text === "include") {
86
+ if (node.arguments.length < 2) {
87
+ ts.forEachChild(node, visit);
88
+ return;
89
+ }
90
+
91
+ const pathPrefix = getStringValue(node.arguments[0]);
92
+ if (pathPrefix === null) {
93
+ ts.forEachChild(node, visit);
94
+ return;
95
+ }
96
+
97
+ const secondArg = node.arguments[1];
98
+ const namePrefix = extractNamePrefixFromInclude(node);
99
+
100
+ if (ts.isIdentifier(secondArg)) {
101
+ resolved.push({
102
+ pathPrefix,
103
+ variableName: secondArg.text,
104
+ namePrefix,
105
+ });
106
+ } else if (ts.isCallExpression(secondArg)) {
107
+ const callText = secondArg.expression.getText(sourceFile);
108
+ unresolvable.push({
109
+ pathPrefix,
110
+ namePrefix,
111
+ reason: "factory-call",
112
+ detail: `${callText}()`,
113
+ });
114
+ } else {
115
+ unresolvable.push({
116
+ pathPrefix,
117
+ namePrefix,
118
+ reason: "dynamic-expression",
119
+ detail: secondArg.getText(sourceFile),
120
+ });
121
+ }
122
+ }
123
+ }
124
+ ts.forEachChild(node, visit);
125
+ }
126
+
127
+ visit(sourceFile);
128
+ return { resolved, unresolvable };
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Import resolution
133
+ // ---------------------------------------------------------------------------
134
+
135
+ /**
136
+ * Find the import statement for a local variable name.
137
+ * Returns the import specifier and the exported name from the source module.
138
+ */
139
+ export function resolveImportedVariable(
140
+ code: string,
141
+ localName: string,
142
+ ): { specifier: string; exportedName: string } | null {
143
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
144
+ let match;
145
+
146
+ while ((match = importRegex.exec(code)) !== null) {
147
+ const imports = match[1];
148
+ const specifier = match[2];
149
+
150
+ const parts = imports
151
+ .split(",")
152
+ .map((s) => s.trim())
153
+ .filter(Boolean);
154
+ for (const part of parts) {
155
+ const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
156
+ if (asMatch && asMatch[2] === localName)
157
+ return { specifier, exportedName: asMatch[1] };
158
+ if (part === localName) return { specifier, exportedName: localName };
159
+ }
160
+ }
161
+
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Resolve an import specifier relative to the importing file.
167
+ * Strips .js/.mjs/.jsx extensions and tries .ts/.tsx/.js/.jsx candidates.
168
+ */
169
+ export function resolveImportPath(
170
+ importSpec: string,
171
+ fromFile: string,
172
+ ): string | null {
173
+ if (!importSpec.startsWith(".")) return null;
174
+
175
+ const dir = dirname(fromFile);
176
+ let base = importSpec;
177
+ if (base.endsWith(".js")) base = base.slice(0, -3);
178
+ else if (base.endsWith(".mjs")) base = base.slice(0, -4);
179
+ else if (base.endsWith(".jsx")) base = base.slice(0, -4);
180
+
181
+ const candidates = [
182
+ resolve(dir, base + ".ts"),
183
+ resolve(dir, base + ".tsx"),
184
+ resolve(dir, base + ".js"),
185
+ resolve(dir, base + ".jsx"),
186
+ resolve(dir, base + "/index.ts"),
187
+ resolve(dir, base + "/index.tsx"),
188
+ resolve(dir, base + "/index.js"),
189
+ resolve(dir, base + "/index.jsx"),
190
+ ];
191
+
192
+ for (const candidate of candidates) {
193
+ if (existsSync(candidate)) return candidate;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // urls() block extraction for same-file variables
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Extract the source of a specific `const varName = urls(...)` call using
204
+ * the TypeScript AST. Returns the full text of the urls() call expression.
205
+ */
206
+ function extractUrlsBlockForVariable(
207
+ code: string,
208
+ varName: string,
209
+ ): string | null {
210
+ const sourceFile = ts.createSourceFile(
211
+ "input.tsx",
212
+ code,
213
+ ts.ScriptTarget.Latest,
214
+ true,
215
+ ts.ScriptKind.TSX,
216
+ );
217
+ let result: string | null = null;
218
+
219
+ function visit(node: ts.Node) {
220
+ if (result) return;
221
+ if (
222
+ ts.isVariableDeclaration(node) &&
223
+ ts.isIdentifier(node.name) &&
224
+ node.name.text === varName &&
225
+ node.initializer &&
226
+ ts.isCallExpression(node.initializer)
227
+ ) {
228
+ const callee = node.initializer.expression;
229
+ if (ts.isIdentifier(callee) && callee.text === "urls") {
230
+ result = node.initializer.getText(sourceFile);
231
+ return;
232
+ }
233
+ }
234
+ ts.forEachChild(node, visit);
235
+ }
236
+
237
+ visit(sourceFile);
238
+ return result;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Combined route map building
243
+ // ---------------------------------------------------------------------------
244
+
245
+ function buildRouteMapFromBlock(
246
+ block: string,
247
+ fullSource: string,
248
+ filePath: string,
249
+ visited: Set<string>,
250
+ searchSchemasOut?: Record<string, Record<string, string>>,
251
+ diagnosticsOut?: UnresolvableInclude[],
252
+ ): Record<string, string> {
253
+ const routeMap: Record<string, string> = {};
254
+
255
+ // Extract local path() routes
256
+ const localRoutes = extractRoutesFromSource(block);
257
+ for (const { name, pattern, search } of localRoutes) {
258
+ routeMap[name] = pattern;
259
+ if (search && searchSchemasOut) {
260
+ searchSchemasOut[name] = search;
261
+ }
262
+ }
263
+
264
+ // Extract include() calls with diagnostics for unresolvable ones
265
+ const { resolved: includes, unresolvable } =
266
+ extractIncludesWithDiagnostics(block);
267
+
268
+ if (diagnosticsOut) {
269
+ for (const entry of unresolvable) {
270
+ diagnosticsOut.push({ ...entry, sourceFile: filePath });
271
+ }
272
+ }
273
+
274
+ for (const { pathPrefix, variableName, namePrefix } of includes) {
275
+ let childResult: {
276
+ routes: Record<string, string>;
277
+ searchSchemas: Record<string, Record<string, string>>;
278
+ };
279
+
280
+ // Try import resolution first
281
+ const imported = resolveImportedVariable(fullSource, variableName);
282
+ if (imported) {
283
+ const targetFile = resolveImportPath(imported.specifier, filePath);
284
+ if (!targetFile) {
285
+ if (diagnosticsOut) {
286
+ diagnosticsOut.push({
287
+ pathPrefix,
288
+ namePrefix,
289
+ reason: "file-not-found",
290
+ sourceFile: filePath,
291
+ detail: `import "${imported.specifier}" resolved to no file`,
292
+ });
293
+ }
294
+ continue;
295
+ }
296
+ childResult = buildCombinedRouteMapWithSearch(
297
+ targetFile,
298
+ imported.exportedName,
299
+ visited,
300
+ diagnosticsOut,
301
+ );
302
+ } else {
303
+ // Check if variable exists as a same-file urls() definition
304
+ const sameFileBlock = extractUrlsBlockForVariable(
305
+ fullSource,
306
+ variableName,
307
+ );
308
+ if (!sameFileBlock) {
309
+ if (diagnosticsOut) {
310
+ diagnosticsOut.push({
311
+ pathPrefix,
312
+ namePrefix,
313
+ reason: "unresolvable-import",
314
+ sourceFile: filePath,
315
+ detail: `variable "${variableName}" not found in imports or same-file scope`,
316
+ });
317
+ }
318
+ continue;
319
+ }
320
+ childResult = buildCombinedRouteMapWithSearch(
321
+ filePath,
322
+ variableName,
323
+ visited,
324
+ diagnosticsOut,
325
+ );
326
+ }
327
+
328
+ // Includes without a name keep their child names private to the mounted
329
+ // module. They remain active at runtime via an internal scope prefix, but
330
+ // they are intentionally omitted from generated public route maps.
331
+ if (namePrefix === null) {
332
+ continue;
333
+ }
334
+
335
+ // Apply prefixes
336
+ for (const [name, pattern] of Object.entries(childResult.routes)) {
337
+ const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
338
+ let prefixedPattern: string;
339
+ if (pattern === "/") {
340
+ prefixedPattern = pathPrefix || "/";
341
+ } else if (pathPrefix.endsWith("/") && pattern.startsWith("/")) {
342
+ prefixedPattern = pathPrefix + pattern.slice(1);
343
+ } else {
344
+ prefixedPattern = pathPrefix + pattern;
345
+ }
346
+ routeMap[prefixedName] = prefixedPattern;
347
+ // Propagate search schemas with prefix
348
+ if (childResult.searchSchemas[name] && searchSchemasOut) {
349
+ searchSchemasOut[prefixedName] = childResult.searchSchemas[name];
350
+ }
351
+ }
352
+ }
353
+
354
+ return routeMap;
355
+ }
356
+
357
+ /**
358
+ * Build route map and search schemas together.
359
+ * Internal helper used by the include resolution path.
360
+ */
361
+ export function buildCombinedRouteMapWithSearch(
362
+ filePath: string,
363
+ variableName?: string,
364
+ visited?: Set<string>,
365
+ diagnosticsOut?: UnresolvableInclude[],
366
+ ): {
367
+ routes: Record<string, string>;
368
+ searchSchemas: Record<string, Record<string, string>>;
369
+ } {
370
+ visited = visited ?? new Set();
371
+ const realPath = resolve(filePath);
372
+ const key = variableName ? `${realPath}:${variableName}` : realPath;
373
+ if (visited.has(key)) {
374
+ console.warn(`[rsc-router] Circular include detected, skipping: ${key}`);
375
+ return { routes: {}, searchSchemas: {} };
376
+ }
377
+ visited.add(key);
378
+
379
+ let source: string;
380
+ try {
381
+ source = readFileSync(realPath, "utf-8");
382
+ } catch {
383
+ return { routes: {}, searchSchemas: {} };
384
+ }
385
+
386
+ let block: string;
387
+ if (variableName) {
388
+ const extracted = extractUrlsBlockForVariable(source, variableName);
389
+ if (!extracted) return { routes: {}, searchSchemas: {} };
390
+ block = extracted;
391
+ } else {
392
+ block = source;
393
+ }
394
+
395
+ const searchSchemas: Record<string, Record<string, string>> = {};
396
+ const routes = buildRouteMapFromBlock(
397
+ block,
398
+ source,
399
+ realPath,
400
+ visited,
401
+ searchSchemas,
402
+ diagnosticsOut,
403
+ );
404
+
405
+ // Remove from visited so sibling branches can include the same variable
406
+ // without false circular-include detection. Only ancestors in the current
407
+ // recursion path should trigger the cycle guard.
408
+ visited.delete(key);
409
+
410
+ return { routes, searchSchemas };
411
+ }
@@ -0,0 +1,48 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Param extraction from route patterns
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /**
6
+ * Extract typed params from a route pattern string.
7
+ * Matches `:paramName` and `:paramName?` (optional).
8
+ * Custom regex constraints like `:id(\d+)` are ignored for type purposes.
9
+ */
10
+ export function extractParamsFromPattern(
11
+ pattern: string,
12
+ ): Record<string, string> | undefined {
13
+ const params: Record<string, string> = {};
14
+ const regex = /:([a-zA-Z_$][\w$]*)(?:\([^)]+\))?(\?)?/g;
15
+ let match;
16
+ while ((match = regex.exec(pattern)) !== null) {
17
+ params[match[1]!] = match[2] ? "string?" : "string";
18
+ }
19
+ return Object.keys(params).length > 0 ? params : undefined;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Shared route entry formatter
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Format a single route entry for codegen output.
28
+ * Routes without search remain plain strings (params are extracted from
29
+ * the pattern at the type level by ExtractParams).
30
+ * Routes with search become objects with path and search fields.
31
+ */
32
+ export function formatRouteEntry(
33
+ key: string,
34
+ pattern: string,
35
+ _params?: Record<string, string>,
36
+ search?: Record<string, string>,
37
+ ): string {
38
+ const hasSearch = search && Object.keys(search).length > 0;
39
+
40
+ if (!hasSearch) {
41
+ return ` ${key}: "${pattern}",`;
42
+ }
43
+
44
+ const searchBody = Object.entries(search!)
45
+ .map(([k, v]) => `${k}: "${v}"`)
46
+ .join(", ");
47
+ return ` ${key}: { path: "${pattern}", search: { ${searchBody} } },`;
48
+ }
@@ -0,0 +1,128 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import ts from "typescript";
3
+ import { extractParamsFromPattern } from "./param-extraction.js";
4
+ import { extractRoutesFromSource } from "./ast-route-extraction.js";
5
+ import { generatePerModuleTypesSource } from "./codegen.js";
6
+ import { buildCombinedRouteMapWithSearch } from "./include-resolution.js";
7
+ import type { ScanFilter } from "./scan-filter.js";
8
+ import { findTsFiles } from "./scan-filter.js";
9
+
10
+ /**
11
+ * Generate per-module route type files by statically parsing url module source.
12
+ * Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
13
+ * extracted route name/pattern pairs. Only writes when content has changed.
14
+ */
15
+ export function writePerModuleRouteTypes(
16
+ root: string,
17
+ filter?: ScanFilter,
18
+ ): void {
19
+ const files = findTsFiles(root, filter);
20
+ for (const filePath of files) {
21
+ writePerModuleRouteTypesForFile(filePath);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Find all variable names assigned to urls() calls in source code.
27
+ * e.g. `export const patterns = urls(...)` -> ["patterns"]
28
+ */
29
+ export function findUrlsVariableNames(code: string): string[] {
30
+ const sourceFile = ts.createSourceFile(
31
+ "input.tsx",
32
+ code,
33
+ ts.ScriptTarget.Latest,
34
+ true,
35
+ ts.ScriptKind.TSX,
36
+ );
37
+ const names: string[] = [];
38
+
39
+ function visit(node: ts.Node) {
40
+ if (
41
+ ts.isVariableDeclaration(node) &&
42
+ ts.isIdentifier(node.name) &&
43
+ node.initializer &&
44
+ ts.isCallExpression(node.initializer)
45
+ ) {
46
+ const callee = node.initializer.expression;
47
+ if (ts.isIdentifier(callee) && callee.text === "urls") {
48
+ names.push(node.name.text);
49
+ }
50
+ }
51
+ ts.forEachChild(node, visit);
52
+ }
53
+
54
+ visit(sourceFile);
55
+ return names;
56
+ }
57
+
58
+ /**
59
+ * Generate per-module route types for a single url module file.
60
+ * Follows include() calls recursively to produce the full route tree.
61
+ * No-ops if the file doesn't contain `urls(` or has no named routes.
62
+ */
63
+ export function writePerModuleRouteTypesForFile(filePath: string): void {
64
+ try {
65
+ const source = readFileSync(filePath, "utf-8");
66
+ if (!source.includes("urls(")) return;
67
+
68
+ const varNames = findUrlsVariableNames(source);
69
+
70
+ type Route = {
71
+ name: string;
72
+ pattern: string;
73
+ params?: Record<string, string>;
74
+ search?: Record<string, string>;
75
+ };
76
+ let routes: Route[];
77
+
78
+ if (varNames.length > 0) {
79
+ // Follow includes recursively via the combined route map builder.
80
+ // The visited set in buildCombinedRouteMapWithSearch prevents infinite loops.
81
+ routes = [];
82
+ for (const varName of varNames) {
83
+ const { routes: routeMap, searchSchemas } =
84
+ buildCombinedRouteMapWithSearch(filePath, varName);
85
+ for (const [name, pattern] of Object.entries(routeMap)) {
86
+ const params = extractParamsFromPattern(pattern);
87
+ routes.push({
88
+ name,
89
+ pattern,
90
+ ...(params ? { params } : {}),
91
+ ...(searchSchemas[name] ? { search: searchSchemas[name] } : {}),
92
+ });
93
+ }
94
+ }
95
+ } else {
96
+ // Fallback: no urls() variable found, extract path() calls directly
97
+ routes = extractRoutesFromSource(source);
98
+ }
99
+
100
+ const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
101
+
102
+ // When a urls() variable was found but static resolution yields zero
103
+ // routes, write an empty placeholder so generated imports stay
104
+ // resolvable until runtime discovery fills them in.
105
+ if (routes.length === 0) {
106
+ if (varNames.length > 0 && !existsSync(genPath)) {
107
+ writeFileSync(genPath, generatePerModuleTypesSource([]));
108
+ console.log(
109
+ `[rsc-router] Generated route types (placeholder) -> ${genPath}`,
110
+ );
111
+ }
112
+ return;
113
+ }
114
+
115
+ const genSource = generatePerModuleTypesSource(routes);
116
+ const existing = existsSync(genPath)
117
+ ? readFileSync(genPath, "utf-8")
118
+ : null;
119
+ if (existing !== genSource) {
120
+ writeFileSync(genPath, genSource);
121
+ console.log(`[rsc-router] Generated route types -> ${genPath}`);
122
+ }
123
+ } catch (err) {
124
+ console.warn(
125
+ `[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`,
126
+ );
127
+ }
128
+ }