@rangojs/router 0.0.0-experimental.9 → 0.0.0-experimental.a5f27bd5

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 (299) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -155
  4. package/dist/vite/index.js +4440 -2170
  5. package/package.json +60 -54
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +333 -71
  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 +74 -15
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +15 -11
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +405 -45
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +144 -91
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +316 -87
  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 +87 -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 +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +285 -553
  41. package/src/browser/navigation-client.ts +123 -73
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +261 -309
  46. package/src/browser/prefetch/cache.ts +154 -0
  47. package/src/browser/prefetch/fetch.ts +135 -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 +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +182 -70
  53. package/src/browser/react/NavigationProvider.tsx +51 -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 +106 -27
  74. package/src/browser/scroll-restoration.ts +92 -16
  75. package/src/browser/segment-reconciler.ts +216 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +504 -599
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +107 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +82 -21
  82. package/src/build/generate-route-types.ts +36 -752
  83. package/src/build/index.ts +6 -5
  84. package/src/build/route-trie.ts +39 -13
  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 +469 -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 +338 -0
  98. package/src/cache/cache-scope.ts +120 -301
  99. package/src/cache/cf/cf-cache-store.ts +119 -7
  100. package/src/cache/cf/index.ts +8 -2
  101. package/src/cache/document-cache.ts +101 -72
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +0 -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 +17 -7
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +15 -10
  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 +133 -21
  133. package/src/index.ts +164 -52
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +25 -143
  136. package/src/loader.ts +27 -10
  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 +158 -13
  141. package/src/prerender.ts +333 -26
  142. package/src/reverse.ts +184 -121
  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 +934 -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 -1431
  151. package/src/route-map-builder.ts +156 -123
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +48 -9
  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 +158 -0
  158. package/src/router/handler-context.ts +374 -81
  159. package/src/router/intercept-resolution.ts +24 -16
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +83 -32
  164. package/src/router/match-api.ts +118 -119
  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 +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +336 -84
  169. package/src/router/match-middleware/cache-store.ts +43 -24
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -20
  171. package/src/router/match-middleware/segment-resolution.ts +16 -8
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -28
  174. package/src/router/metrics.ts +235 -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 +324 -367
  178. package/src/router/pattern-matching.ts +197 -41
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -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 +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1239 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1315
  192. package/src/router/segment-wrappers.ts +289 -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 +16 -9
  198. package/src/router.ts +590 -1983
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +661 -1015
  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 +230 -0
  216. package/src/segment-system.tsx +25 -13
  217. package/src/server/context.ts +173 -48
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  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 +430 -70
  223. package/src/server.ts +35 -155
  224. package/src/ssr/index.tsx +100 -31
  225. package/src/static-handler.ts +114 -0
  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 +687 -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 +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1757
  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 -1282
  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 +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -1963
  261. package/src/vite/plugin-types.ts +131 -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 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  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 +254 -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 +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  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/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/router.gen.ts +0 -6
  294. package/src/urls.gen.ts +0 -8
  295. package/src/vite/expose-handle-id.ts +0 -209
  296. package/src/vite/expose-loader-id.ts +0 -426
  297. package/src/vite/expose-location-state-id.ts +0 -177
  298. package/src/vite/expose-prerender-handler-id.ts +0 -429
  299. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,752 +1,36 @@
1
- import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
2
- import { join, dirname, resolve } from "node:path";
3
-
4
- /**
5
- * Extract route definitions from source code by statically parsing path() calls.
6
- * No code execution needed -- works on raw source text.
7
- *
8
- * Handles multi-line handlers with JSX, nested braces, string literals,
9
- * and comments. Skips unnamed paths (no { name: "..." }).
10
- */
11
- export function extractRoutesFromSource(
12
- code: string
13
- ): Array<{ name: string; pattern: string }> {
14
- const routes: Array<{ name: string; pattern: string }> = [];
15
- const regex = /\bpath(?:\.(?:json|text|html|xml|image|stream|any))?\s*\(/g;
16
- let match;
17
-
18
- while ((match = regex.exec(code)) !== null) {
19
- const result = parsePathCall(code, match.index + match[0].length);
20
- if (result) routes.push(result);
21
- }
22
-
23
- return routes;
24
- }
25
-
26
- /**
27
- * Generate a per-module types file from extracted routes.
28
- * Output has zero imports, preventing circular references.
29
- */
30
- export function generatePerModuleTypesSource(
31
- routes: Array<{ name: string; pattern: string }>
32
- ): string {
33
- const valid = routes.filter(({ name }) => {
34
- if (!name || /["'\\`\n\r]/.test(name)) {
35
- console.warn(`[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`);
36
- return false;
37
- }
38
- return true;
39
- });
40
-
41
- // Deduplicate by name (last definition wins for same name)
42
- const deduped = new Map<string, string>();
43
- for (const { name, pattern } of valid) {
44
- deduped.set(name, pattern);
45
- }
46
- const sorted = [...deduped.entries()]
47
- .sort(([a], [b]) => a.localeCompare(b));
48
- const body = sorted
49
- .map(([name, pattern]) => {
50
- // Quote names that aren't valid bare identifiers (dots, dashes, etc.)
51
- const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
52
- return ` ${key}: "${pattern}",`;
53
- })
54
- .join("\n");
55
- return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Mini-parser internals
60
- // ---------------------------------------------------------------------------
61
-
62
- function isWhitespace(ch: string): boolean {
63
- return ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
64
- }
65
-
66
- /** Read a single- or double-quoted string literal starting at pos. */
67
- function readString(
68
- code: string,
69
- pos: number
70
- ): { value: string; end: number } | null {
71
- const quote = code[pos];
72
- if (quote !== '"' && quote !== "'") return null;
73
-
74
- let value = "";
75
- pos++;
76
- while (pos < code.length) {
77
- if (code[pos] === "\\") {
78
- pos++;
79
- if (pos < code.length) {
80
- value += code[pos];
81
- pos++;
82
- }
83
- continue;
84
- }
85
- if (code[pos] === quote) {
86
- return { value, end: pos + 1 };
87
- }
88
- value += code[pos];
89
- pos++;
90
- }
91
- return null;
92
- }
93
-
94
- /** Skip past any string literal (single, double, or template). */
95
- function skipStringLiteral(code: string, pos: number): number {
96
- const quote = code[pos];
97
-
98
- if (quote === "`") {
99
- pos++;
100
- while (pos < code.length) {
101
- if (code[pos] === "\\") {
102
- pos += 2;
103
- continue;
104
- }
105
- if (code[pos] === "`") return pos + 1;
106
- if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
107
- pos += 2;
108
- let braceDepth = 1;
109
- while (pos < code.length && braceDepth > 0) {
110
- if (code[pos] === "{") braceDepth++;
111
- else if (code[pos] === "}") braceDepth--;
112
- else if (code[pos] === "\\") pos++;
113
- else if (
114
- code[pos] === '"' ||
115
- code[pos] === "'" ||
116
- code[pos] === "`"
117
- ) {
118
- pos = skipStringLiteral(code, pos);
119
- continue;
120
- }
121
- if (braceDepth > 0) pos++;
122
- }
123
- continue;
124
- }
125
- pos++;
126
- }
127
- return pos;
128
- }
129
-
130
- // Simple single/double quoted string
131
- pos++;
132
- while (pos < code.length) {
133
- if (code[pos] === "\\") {
134
- pos += 2;
135
- continue;
136
- }
137
- if (code[pos] === quote) return pos + 1;
138
- pos++;
139
- }
140
- return pos;
141
- }
142
-
143
- /**
144
- * Check if code at pos starts with `name` as a standalone identifier
145
- * followed by `:` (an object property).
146
- */
147
- function matchesNameColon(code: string, pos: number): boolean {
148
- if (code.slice(pos, pos + 4) !== "name") return false;
149
- if (pos > 0 && /\w/.test(code[pos - 1])) return false;
150
- const afterName = pos + 4;
151
- if (afterName < code.length && /\w/.test(code[afterName])) return false;
152
- let checkPos = afterName;
153
- while (checkPos < code.length && isWhitespace(code[checkPos])) checkPos++;
154
- return code[checkPos] === ":";
155
- }
156
-
157
- /** Extract the string value after `name:` starting at the `n` of `name`. */
158
- function extractNameValue(
159
- code: string,
160
- pos: number
161
- ): { value: string; end: number } | null {
162
- pos += 4; // skip 'name'
163
- while (pos < code.length && isWhitespace(code[pos])) pos++;
164
- pos++; // skip ':'
165
- while (pos < code.length && isWhitespace(code[pos])) pos++;
166
- return readString(code, pos);
167
- }
168
-
169
- /**
170
- * Parse a single path() call starting right after the opening paren.
171
- * Returns { name, pattern } or null if the call is unnamed.
172
- */
173
- function parsePathCall(
174
- code: string,
175
- pos: number
176
- ): { name: string; pattern: string } | null {
177
- // Skip whitespace to first argument
178
- while (pos < code.length && isWhitespace(code[pos])) pos++;
179
-
180
- // First argument must be a string literal (the pattern)
181
- const patternStr = readString(code, pos);
182
- if (!patternStr) return null;
183
- const pattern = patternStr.value;
184
- pos = patternStr.end;
185
-
186
- // Scan the rest of the call tracking depth.
187
- // depth=1: inside path(), depth=2: inside an object/paren at top level of call.
188
- // We look for `name: "..."` at depth 2 (options object properties).
189
- let depth = 1;
190
- let name: string | null = null;
191
-
192
- while (pos < code.length && depth > 0) {
193
- const ch = code[pos];
194
-
195
- if (isWhitespace(ch)) {
196
- pos++;
197
- continue;
198
- }
199
-
200
- // Line comment
201
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
202
- pos += 2;
203
- while (pos < code.length && code[pos] !== "\n") pos++;
204
- continue;
205
- }
206
-
207
- // Block comment
208
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
209
- pos += 2;
210
- while (
211
- pos < code.length - 1 &&
212
- !(code[pos] === "*" && code[pos + 1] === "/")
213
- )
214
- pos++;
215
- pos += 2;
216
- continue;
217
- }
218
-
219
- // At depth 2 (inside an object at call top-level), look for name: "..."
220
- if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
221
- const nameResult = extractNameValue(code, pos);
222
- if (nameResult) {
223
- name = nameResult.value;
224
- pos = nameResult.end;
225
- continue;
226
- }
227
- }
228
-
229
- // Skip string literals.
230
- // Treat ' preceded by a word char as an apostrophe (e.g. "shouldn't"),
231
- // not a string delimiter. In valid JS/TS, opening ' is never preceded
232
- // by a word character.
233
- if (ch === '"' || ch === "`" || (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))) {
234
- pos = skipStringLiteral(code, pos);
235
- continue;
236
- }
237
-
238
- // Track depth
239
- if (ch === "(" || ch === "{" || ch === "[") depth++;
240
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
241
-
242
- pos++;
243
- }
244
-
245
- if (name === null) return null;
246
- return { name, pattern };
247
- }
248
-
249
- /**
250
- * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
251
- * with route name -> pattern mappings. This enables Handler<"routeName">
252
- * without circular references since the file has no imports from the app.
253
- */
254
- export function generateRouteTypesSource(
255
- routeManifest: Record<string, string>
256
- ): string {
257
- const entries = Object.entries(routeManifest).sort(([a], [b]) =>
258
- a.localeCompare(b)
259
- );
260
-
261
- const interfaceBody = entries
262
- .map(([name, pattern]) => ` "${name}": "${pattern}";`)
263
- .join("\n");
264
-
265
- return `// Auto-generated by @rangojs/router - do not edit
266
- export {};
267
-
268
- declare global {
269
- namespace RSCRouter {
270
- interface GeneratedRouteMap {
271
- ${interfaceBody}
272
- }
273
- }
274
- }
275
- `;
276
- }
277
-
278
- /**
279
- * Recursively find .ts/.tsx files under a directory, skipping node_modules
280
- * and .gen. files.
281
- */
282
- export function findTsFiles(dir: string): string[] {
283
- const results: string[] = [];
284
- let entries;
285
- try {
286
- entries = readdirSync(dir, { withFileTypes: true });
287
- } catch (err) {
288
- console.warn(`[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`);
289
- return results;
290
- }
291
- for (const entry of entries) {
292
- const fullPath = join(dir, entry.name);
293
- if (entry.isDirectory()) {
294
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
295
- results.push(...findTsFiles(fullPath));
296
- } else if (
297
- (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) &&
298
- !entry.name.includes(".gen.")
299
- ) {
300
- results.push(fullPath);
301
- }
302
- }
303
- return results;
304
- }
305
-
306
- /**
307
- * Generate per-module route type files by statically parsing url module source.
308
- * Scans for files containing `urls(` and writes a sibling `.gen.ts` with the
309
- * extracted route name/pattern pairs. Only writes when content has changed.
310
- */
311
- export function writePerModuleRouteTypes(root: string, entry: string): void {
312
- const scanDir = dirname(resolve(root, entry));
313
- const files = findTsFiles(scanDir);
314
- for (const filePath of files) {
315
- writePerModuleRouteTypesForFile(filePath);
316
- }
317
- }
318
-
319
- /**
320
- * Generate per-module route types for a single url module file.
321
- * No-ops if the file doesn't contain `urls(` or has no named routes.
322
- */
323
- export function writePerModuleRouteTypesForFile(filePath: string): void {
324
- try {
325
- const source = readFileSync(filePath, "utf-8");
326
- if (!source.includes("urls(")) return;
327
-
328
- const routes = extractRoutesFromSource(source);
329
- if (routes.length === 0) return;
330
-
331
- const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
332
- const genSource = generatePerModuleTypesSource(routes);
333
- const existing = existsSync(genPath) ? readFileSync(genPath, "utf-8") : null;
334
- if (existing !== genSource) {
335
- writeFileSync(genPath, genSource);
336
- console.log(`[rsc-router] Generated route types -> ${genPath}`);
337
- }
338
- } catch (err) {
339
- console.warn(`[rsc-router] Failed to generate route types for ${filePath}: ${(err as Error).message}`);
340
- }
341
- }
342
-
343
- // ---------------------------------------------------------------------------
344
- // Static include() parsing
345
- // ---------------------------------------------------------------------------
346
-
347
- /**
348
- * Extract include() calls from source code by statically parsing.
349
- * Returns the path prefix, variable name, and optional name prefix for each.
350
- */
351
- export function extractIncludesFromSource(
352
- code: string
353
- ): Array<{ pathPrefix: string; variableName: string; namePrefix: string | null }> {
354
- const results: Array<{
355
- pathPrefix: string;
356
- variableName: string;
357
- namePrefix: string | null;
358
- }> = [];
359
- const regex = /\binclude\s*\(/g;
360
- let match;
361
-
362
- while ((match = regex.exec(code)) !== null) {
363
- const result = parseIncludeCall(code, match.index + match[0].length);
364
- if (result) results.push(result);
365
- }
366
-
367
- return results;
368
- }
369
-
370
- /**
371
- * Parse a single include() call starting right after the opening paren.
372
- * Expects: include("prefix", variableName, { name: "prefix" })
373
- */
374
- function parseIncludeCall(
375
- code: string,
376
- pos: number
377
- ): {
378
- pathPrefix: string;
379
- variableName: string;
380
- namePrefix: string | null;
381
- } | null {
382
- // Skip whitespace to first argument
383
- while (pos < code.length && isWhitespace(code[pos])) pos++;
384
-
385
- // First arg: string literal (pathPrefix)
386
- const prefixStr = readString(code, pos);
387
- if (!prefixStr) return null;
388
- const pathPrefix = prefixStr.value;
389
- pos = prefixStr.end;
390
-
391
- // Comma
392
- while (pos < code.length && isWhitespace(code[pos])) pos++;
393
- if (pos >= code.length || code[pos] !== ",") return null;
394
- pos++;
395
- while (pos < code.length && isWhitespace(code[pos])) pos++;
396
-
397
- // Second arg: identifier (variableName)
398
- const varStart = pos;
399
- while (pos < code.length && /[\w$]/.test(code[pos])) pos++;
400
- if (pos === varStart) return null;
401
- const variableName = code.slice(varStart, pos);
402
-
403
- // Scan rest of call for optional { name: "..." }
404
- let namePrefix: string | null = null;
405
- let depth = 1; // inside include()
406
-
407
- while (pos < code.length && depth > 0) {
408
- const ch = code[pos];
409
-
410
- if (isWhitespace(ch)) {
411
- pos++;
412
- continue;
413
- }
414
-
415
- // Line comment
416
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
417
- pos += 2;
418
- while (pos < code.length && code[pos] !== "\n") pos++;
419
- continue;
420
- }
421
-
422
- // Block comment
423
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
424
- pos += 2;
425
- while (
426
- pos < code.length - 1 &&
427
- !(code[pos] === "*" && code[pos + 1] === "/")
428
- )
429
- pos++;
430
- pos += 2;
431
- continue;
432
- }
433
-
434
- // At depth 2 (inside options object), look for name: "..."
435
- if (depth === 2 && ch === "n" && matchesNameColon(code, pos)) {
436
- const nameResult = extractNameValue(code, pos);
437
- if (nameResult) {
438
- namePrefix = nameResult.value;
439
- pos = nameResult.end;
440
- continue;
441
- }
442
- }
443
-
444
- // Skip string literals
445
- if (
446
- ch === '"' ||
447
- ch === "`" ||
448
- (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
449
- ) {
450
- pos = skipStringLiteral(code, pos);
451
- continue;
452
- }
453
-
454
- // Track depth
455
- if (ch === "(" || ch === "{" || ch === "[") depth++;
456
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
457
-
458
- pos++;
459
- }
460
-
461
- return { pathPrefix, variableName, namePrefix };
462
- }
463
-
464
- // ---------------------------------------------------------------------------
465
- // Import resolution
466
- // ---------------------------------------------------------------------------
467
-
468
- /**
469
- * Find the import statement for a local variable name.
470
- * Returns the import specifier and the exported name from the source module.
471
- */
472
- function resolveImportedVariable(
473
- code: string,
474
- localName: string
475
- ): { specifier: string; exportedName: string } | null {
476
- const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
477
- let match;
478
-
479
- while ((match = importRegex.exec(code)) !== null) {
480
- const imports = match[1];
481
- const specifier = match[2];
482
-
483
- const parts = imports
484
- .split(",")
485
- .map((s) => s.trim())
486
- .filter(Boolean);
487
- for (const part of parts) {
488
- const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
489
- if (asMatch && asMatch[2] === localName)
490
- return { specifier, exportedName: asMatch[1] };
491
- if (part === localName) return { specifier, exportedName: localName };
492
- }
493
- }
494
-
495
- return null;
496
- }
497
-
498
- /**
499
- * Resolve an import specifier relative to the importing file.
500
- * Strips .js/.mjs extensions and tries .ts/.tsx candidates.
501
- */
502
- function resolveImportPath(
503
- importSpec: string,
504
- fromFile: string
505
- ): string | null {
506
- if (!importSpec.startsWith(".")) return null;
507
-
508
- const dir = dirname(fromFile);
509
- let base = importSpec;
510
- if (base.endsWith(".js")) base = base.slice(0, -3);
511
- else if (base.endsWith(".mjs")) base = base.slice(0, -4);
512
-
513
- const candidates = [
514
- resolve(dir, base + ".ts"),
515
- resolve(dir, base + ".tsx"),
516
- resolve(dir, base + "/index.ts"),
517
- resolve(dir, base + "/index.tsx"),
518
- ];
519
-
520
- for (const candidate of candidates) {
521
- if (existsSync(candidate)) return candidate;
522
- }
523
- return null;
524
- }
525
-
526
- // ---------------------------------------------------------------------------
527
- // urls() block extraction for same-file variables
528
- // ---------------------------------------------------------------------------
529
-
530
- function escapeRegExp(s: string): string {
531
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
532
- }
533
-
534
- /**
535
- * Extract the source of a specific `const varName = urls(...)` block.
536
- * Used for same-file variables where include() references a urls() defined
537
- * in the same module rather than imported.
538
- */
539
- function extractUrlsBlockForVariable(
540
- code: string,
541
- varName: string
542
- ): string | null {
543
- const pattern = new RegExp(
544
- `(?:export\\s+)?(?:const|let|var)\\s+${escapeRegExp(varName)}\\s*=\\s*urls\\s*\\(`
545
- );
546
- const match = pattern.exec(code);
547
- if (!match) return null;
548
-
549
- // Start from the opening paren of urls(
550
- const openParen = match.index + match[0].length - 1;
551
- let depth = 1;
552
- let pos = openParen + 1;
553
-
554
- while (pos < code.length && depth > 0) {
555
- const ch = code[pos];
556
-
557
- // Skip strings
558
- if (
559
- ch === '"' ||
560
- ch === "`" ||
561
- (ch === "'" && (pos === 0 || !/\w/.test(code[pos - 1])))
562
- ) {
563
- pos = skipStringLiteral(code, pos);
564
- continue;
565
- }
566
-
567
- // Line comment
568
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
569
- pos += 2;
570
- while (pos < code.length && code[pos] !== "\n") pos++;
571
- continue;
572
- }
573
-
574
- // Block comment
575
- if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
576
- pos += 2;
577
- while (
578
- pos < code.length - 1 &&
579
- !(code[pos] === "*" && code[pos + 1] === "/")
580
- )
581
- pos++;
582
- pos += 2;
583
- continue;
584
- }
585
-
586
- if (ch === "(" || ch === "{" || ch === "[") depth++;
587
- else if (ch === ")" || ch === "}" || ch === "]") depth--;
588
-
589
- pos++;
590
- }
591
-
592
- return code.slice(openParen, pos);
593
- }
594
-
595
- // ---------------------------------------------------------------------------
596
- // Combined route map building
597
- // ---------------------------------------------------------------------------
598
-
599
- /**
600
- * Recursively build a route map from a urls module file.
601
- * Extracts local path() routes and follows include() calls to sub-modules.
602
- * Handles both imported and same-file variables.
603
- */
604
- export function buildCombinedRouteMap(
605
- filePath: string,
606
- variableName?: string,
607
- visited?: Set<string>
608
- ): Record<string, string> {
609
- visited = visited ?? new Set();
610
- const realPath = resolve(filePath);
611
- const key = variableName ? `${realPath}:${variableName}` : realPath;
612
- if (visited.has(key)) return {};
613
- visited.add(key);
614
-
615
- let source: string;
616
- try {
617
- source = readFileSync(realPath, "utf-8");
618
- } catch {
619
- return {};
620
- }
621
-
622
- // If a specific variable is requested, extract just its urls() block
623
- let block: string;
624
- if (variableName) {
625
- const extracted = extractUrlsBlockForVariable(source, variableName);
626
- if (!extracted) return {};
627
- block = extracted;
628
- } else {
629
- block = source;
630
- }
631
-
632
- return buildRouteMapFromBlock(block, source, realPath, visited);
633
- }
634
-
635
- function buildRouteMapFromBlock(
636
- block: string,
637
- fullSource: string,
638
- filePath: string,
639
- visited: Set<string>
640
- ): Record<string, string> {
641
- const routeMap: Record<string, string> = {};
642
-
643
- // Extract local path() routes
644
- const localRoutes = extractRoutesFromSource(block);
645
- for (const { name, pattern } of localRoutes) {
646
- routeMap[name] = pattern;
647
- }
648
-
649
- // Extract include() calls
650
- const includes = extractIncludesFromSource(block);
651
- for (const { pathPrefix, variableName, namePrefix } of includes) {
652
- let childRoutes: Record<string, string>;
653
-
654
- // Try import resolution first
655
- const imported = resolveImportedVariable(fullSource, variableName);
656
- if (imported) {
657
- const targetFile = resolveImportPath(imported.specifier, filePath);
658
- if (!targetFile) continue;
659
- childRoutes = buildCombinedRouteMap(
660
- targetFile,
661
- imported.exportedName,
662
- visited
663
- );
664
- } else {
665
- // Same-file variable
666
- childRoutes = buildCombinedRouteMap(filePath, variableName, visited);
667
- }
668
-
669
- // Apply prefixes
670
- for (const [name, pattern] of Object.entries(childRoutes)) {
671
- const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
672
- const prefixedPattern =
673
- pattern === "/" ? pathPrefix || "/" : pathPrefix + pattern;
674
- routeMap[prefixedName] = prefixedPattern;
675
- }
676
- }
677
-
678
- return routeMap;
679
- }
680
-
681
- // ---------------------------------------------------------------------------
682
- // Combined named-routes.gen.ts writer
683
- // ---------------------------------------------------------------------------
684
-
685
- /**
686
- * Generate named-routes.gen.ts by statically parsing include() calls
687
- * and recursively resolving sub-module routes. No code execution needed.
688
- *
689
- * Finds root url modules (files with urls() + include() that aren't imported
690
- * by other url modules), builds a combined route map, and writes the output.
691
- */
692
- export function writeCombinedRouteTypes(root: string, entry: string): void {
693
- const scanDir = dirname(resolve(root, entry));
694
-
695
- // Find files with both urls( and include( -- these are composing modules
696
- const files = findTsFiles(scanDir);
697
- const urlModulesWithIncludes: string[] = [];
698
-
699
- for (const filePath of files) {
700
- try {
701
- const source = readFileSync(filePath, "utf-8");
702
- if (source.includes("urls(") && source.includes("include(")) {
703
- urlModulesWithIncludes.push(filePath);
704
- }
705
- } catch {
706
- continue;
707
- }
708
- }
709
-
710
- if (urlModulesWithIncludes.length === 0) return;
711
-
712
- // Build set of files imported by url modules (to identify roots)
713
- const importedFiles = new Set<string>();
714
- for (const filePath of urlModulesWithIncludes) {
715
- try {
716
- const source = readFileSync(filePath, "utf-8");
717
- const includes = extractIncludesFromSource(source);
718
- for (const { variableName } of includes) {
719
- const imported = resolveImportedVariable(source, variableName);
720
- if (imported) {
721
- const resolved = resolveImportPath(imported.specifier, filePath);
722
- if (resolved) importedFiles.add(resolved);
723
- }
724
- }
725
- } catch {
726
- continue;
727
- }
728
- }
729
-
730
- // Roots are url modules with includes that aren't imported by others
731
- const roots = urlModulesWithIncludes.filter((f) => !importedFiles.has(f));
732
- if (roots.length === 0) return;
733
-
734
- // Build combined route map from all roots
735
- const mergedManifest: Record<string, string> = {};
736
- for (const rootFile of roots) {
737
- const routeMap = buildCombinedRouteMap(rootFile);
738
- Object.assign(mergedManifest, routeMap);
739
- }
740
-
741
- if (Object.keys(mergedManifest).length === 0) return;
742
-
743
- const outPath = join(scanDir, "named-routes.gen.ts");
744
- const source = generateRouteTypesSource(mergedManifest);
745
- const existing = existsSync(outPath)
746
- ? readFileSync(outPath, "utf-8")
747
- : null;
748
- if (existing !== source) {
749
- writeFileSync(outPath, source);
750
- console.log(`[rsc-router] Generated combined route types -> ${outPath}`);
751
- }
752
- }
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";