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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,418 @@
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
+ * @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
362
+ * builder function). When provided, variableName is ignored and the block
363
+ * is parsed directly for path()/include() calls.
364
+ */
365
+ export function buildCombinedRouteMapWithSearch(
366
+ filePath: string,
367
+ variableName?: string,
368
+ visited?: Set<string>,
369
+ diagnosticsOut?: UnresolvableInclude[],
370
+ inlineBlock?: string,
371
+ ): {
372
+ routes: Record<string, string>;
373
+ searchSchemas: Record<string, Record<string, string>>;
374
+ } {
375
+ visited = visited ?? new Set();
376
+ const realPath = resolve(filePath);
377
+ const key = variableName ? `${realPath}:${variableName}` : realPath;
378
+ if (visited.has(key)) {
379
+ console.warn(`[rsc-router] Circular include detected, skipping: ${key}`);
380
+ return { routes: {}, searchSchemas: {} };
381
+ }
382
+ visited.add(key);
383
+
384
+ let source: string;
385
+ try {
386
+ source = readFileSync(realPath, "utf-8");
387
+ } catch {
388
+ return { routes: {}, searchSchemas: {} };
389
+ }
390
+
391
+ let block: string;
392
+ if (inlineBlock) {
393
+ block = inlineBlock;
394
+ } else if (variableName) {
395
+ const extracted = extractUrlsBlockForVariable(source, variableName);
396
+ if (!extracted) return { routes: {}, searchSchemas: {} };
397
+ block = extracted;
398
+ } else {
399
+ block = source;
400
+ }
401
+
402
+ const searchSchemas: Record<string, Record<string, string>> = {};
403
+ const routes = buildRouteMapFromBlock(
404
+ block,
405
+ source,
406
+ realPath,
407
+ visited,
408
+ searchSchemas,
409
+ diagnosticsOut,
410
+ );
411
+
412
+ // Remove from visited so sibling branches can include the same variable
413
+ // without false circular-include detection. Only ancestors in the current
414
+ // recursion path should trigger the cycle guard.
415
+ visited.delete(key);
416
+
417
+ return { routes, searchSchemas };
418
+ }
@@ -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
+ }