@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,299 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+
4
+ /**
5
+ * Normalize path to forward slashes.
6
+ */
7
+ export function normalizePath(p: string): string {
8
+ return p.split(path.sep).join("/");
9
+ }
10
+
11
+ /**
12
+ * Generate a short hash for an ID.
13
+ * Uses first 8 chars of SHA-256 hash for uniqueness while keeping IDs short.
14
+ * Appends export name for easier debugging in production: "abc123#ExportName"
15
+ */
16
+ export function hashId(filePath: string, exportName: string): string {
17
+ const input = `${filePath}#${exportName}`;
18
+ const hash = crypto.createHash("sha256").update(input).digest("hex");
19
+ return `${hash.slice(0, 8)}#${exportName}`;
20
+ }
21
+
22
+ /**
23
+ * Build a stable ID for an export binding. Uses hashed IDs in production
24
+ * builds (short + opaque) and readable path#name IDs in dev.
25
+ */
26
+ export function makeStubId(
27
+ filePath: string,
28
+ exportName: string,
29
+ isBuild: boolean,
30
+ ): string {
31
+ return isBuild ? hashId(filePath, exportName) : `${filePath}#${exportName}`;
32
+ }
33
+
34
+ /**
35
+ * Generate an 8-char hex hash for an inline static handler call site.
36
+ * Uses file path and line number (plus optional index for same-line collisions).
37
+ */
38
+ export function hashInlineId(
39
+ filePath: string,
40
+ lineNumber: number,
41
+ index?: number,
42
+ ): string {
43
+ const input =
44
+ index !== undefined && index > 0
45
+ ? `${filePath}:${lineNumber}:${index}`
46
+ : `${filePath}:${lineNumber}`;
47
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
48
+ }
49
+
50
+ export interface DetectedImports {
51
+ loader: boolean;
52
+ handle: boolean;
53
+ locationState: boolean;
54
+ prerenderHandler: boolean;
55
+ staticHandler: boolean;
56
+ router: boolean;
57
+ any: boolean;
58
+ }
59
+
60
+ /**
61
+ * Build a map from local binding name to exported names by walking
62
+ * ExportNamedDeclaration nodes. Handles `export const X`, `export { X }`,
63
+ * and `export { X as Y }`. Skips re-exports (`export { X } from "..."`).
64
+ */
65
+ export function buildExportMap(program: any): Map<string, string[]> {
66
+ const exportMap = new Map<string, string[]>();
67
+
68
+ const pushExport = (local: string, exported: string) => {
69
+ const list = exportMap.get(local);
70
+ if (list) {
71
+ if (!list.includes(exported)) list.push(exported);
72
+ return;
73
+ }
74
+ exportMap.set(local, [exported]);
75
+ };
76
+
77
+ for (const node of program.body ?? []) {
78
+ if (node?.type !== "ExportNamedDeclaration") continue;
79
+
80
+ if (node.declaration?.type === "VariableDeclaration") {
81
+ for (const decl of node.declaration.declarations ?? []) {
82
+ if (decl?.id?.type === "Identifier") {
83
+ pushExport(decl.id.name, decl.id.name);
84
+ }
85
+ }
86
+ }
87
+
88
+ if (!node.source && Array.isArray(node.specifiers)) {
89
+ for (const spec of node.specifiers) {
90
+ if (
91
+ spec?.type === "ExportSpecifier" &&
92
+ spec.local?.type === "Identifier" &&
93
+ spec.exported?.type === "Identifier"
94
+ ) {
95
+ pushExport(spec.local.name, spec.exported.name);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ return exportMap;
102
+ }
103
+
104
+ /**
105
+ * Single-pass detection of all create* imports from @rangojs/router.
106
+ * Returns which create functions are imported so we can skip unnecessary transforms.
107
+ */
108
+ export function detectImports(code: string): DetectedImports {
109
+ // Extract all import declarations from @rangojs/router in one scan
110
+ const importPattern =
111
+ /import\s*\{([^}]*)\}\s*from\s*["']@rangojs\/router(?:\/[^"']*)?["']/g;
112
+
113
+ const result: DetectedImports = {
114
+ loader: false,
115
+ handle: false,
116
+ locationState: false,
117
+ prerenderHandler: false,
118
+ staticHandler: false,
119
+ router: false,
120
+ any: false,
121
+ };
122
+
123
+ let match: RegExpExecArray | null;
124
+ while ((match = importPattern.exec(code)) !== null) {
125
+ const imports = match[1];
126
+ if (/\bcreateLoader\b/.test(imports)) result.loader = true;
127
+ if (/\bcreateHandle\b/.test(imports)) result.handle = true;
128
+ if (/\bcreateLocationState\b/.test(imports)) result.locationState = true;
129
+ if (/\bPrerender\b/.test(imports)) result.prerenderHandler = true;
130
+ if (/\bStatic\b/.test(imports)) result.staticHandler = true;
131
+ if (/\bcreateRouter\b/.test(imports)) result.router = true;
132
+ }
133
+
134
+ // createRouter has a stricter check: only from "@rangojs/router" (not sub-paths).
135
+ // NOTE: This is intentional — detectImports is used as a fast pre-filter in
136
+ // exposeInternalIds (which does NOT handle router transforms). The separate
137
+ // exposeRouterId plugin handles createRouter and DOES accept the /server subpath.
138
+ if (result.router) {
139
+ result.router =
140
+ /import\s*\{[^}]*\bcreateRouter\b[^}]*\}\s*from\s*["']@rangojs\/router["']/.test(
141
+ code,
142
+ );
143
+ }
144
+
145
+ result.any =
146
+ result.loader ||
147
+ result.handle ||
148
+ result.locationState ||
149
+ result.prerenderHandler ||
150
+ result.staticHandler ||
151
+ result.router;
152
+
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * Skip past a string literal, template literal, or comment starting at pos.
158
+ * Returns the index after the closing delimiter, or pos if not at a
159
+ * string/comment start. Handles escape sequences and nested ${} in templates.
160
+ */
161
+ export function skipStringOrComment(code: string, pos: number): number {
162
+ const ch = code[pos];
163
+
164
+ if (ch === '"' || ch === "'") {
165
+ for (let j = pos + 1; j < code.length; j++) {
166
+ if (code[j] === "\\") {
167
+ j++;
168
+ continue;
169
+ }
170
+ if (code[j] === ch) return j + 1;
171
+ }
172
+ return code.length;
173
+ }
174
+
175
+ if (ch === "`") {
176
+ let j = pos + 1;
177
+ while (j < code.length) {
178
+ if (code[j] === "\\") {
179
+ j += 2;
180
+ continue;
181
+ }
182
+ if (code[j] === "`") return j + 1;
183
+ if (code[j] === "$" && j + 1 < code.length && code[j + 1] === "{") {
184
+ j += 2;
185
+ let braceDepth = 1;
186
+ while (j < code.length && braceDepth > 0) {
187
+ const inner = skipStringOrComment(code, j);
188
+ if (inner > j) {
189
+ j = inner;
190
+ continue;
191
+ }
192
+ if (code[j] === "{") braceDepth++;
193
+ else if (code[j] === "}") braceDepth--;
194
+ if (braceDepth > 0) j++;
195
+ }
196
+ if (braceDepth === 0) j++;
197
+ continue;
198
+ }
199
+ j++;
200
+ }
201
+ return j;
202
+ }
203
+
204
+ if (ch === "/" && pos + 1 < code.length) {
205
+ if (code[pos + 1] === "/") {
206
+ const eol = code.indexOf("\n", pos + 2);
207
+ return eol === -1 ? code.length : eol + 1;
208
+ }
209
+ if (code[pos + 1] === "*") {
210
+ const end = code.indexOf("*/", pos + 2);
211
+ return end === -1 ? code.length : end + 2;
212
+ }
213
+ }
214
+
215
+ return pos;
216
+ }
217
+
218
+ /**
219
+ * Find the matching closing paren starting after an already-opened paren.
220
+ * Skips strings, template literals, and comments so parens inside them
221
+ * don't affect depth tracking. Returns the index after the closing paren.
222
+ */
223
+ export function findMatchingParen(code: string, startPos: number): number {
224
+ let depth = 1;
225
+ let i = startPos;
226
+ while (i < code.length && depth > 0) {
227
+ const skipped = skipStringOrComment(code, i);
228
+ if (skipped > i) {
229
+ i = skipped;
230
+ continue;
231
+ }
232
+ if (code[i] === "(") depth++;
233
+ if (code[i] === ")") depth--;
234
+ i++;
235
+ }
236
+ return i;
237
+ }
238
+
239
+ /**
240
+ * Count the number of top-level arguments in a function call.
241
+ * Skips nested parens, brackets, braces, strings, and comments.
242
+ */
243
+ export function countArgs(
244
+ code: string,
245
+ startPos: number,
246
+ endPos: number,
247
+ ): number {
248
+ let depth = 0;
249
+ let argCount = 0;
250
+ let hasContent = false;
251
+ let i = startPos;
252
+
253
+ while (i < endPos) {
254
+ const skipped = skipStringOrComment(code, i);
255
+ if (skipped > i) {
256
+ hasContent = true;
257
+ i = skipped;
258
+ continue;
259
+ }
260
+
261
+ const char = code[i];
262
+ if (char === "(" || char === "[" || char === "{") {
263
+ depth++;
264
+ hasContent = true;
265
+ } else if (char === ")" || char === "]" || char === "}") {
266
+ depth--;
267
+ } else if (char === "," && depth === 0) {
268
+ argCount++;
269
+ } else if (!/\s/.test(char)) {
270
+ hasContent = true;
271
+ }
272
+ i++;
273
+ }
274
+
275
+ return hasContent ? argCount + 1 : 0;
276
+ }
277
+
278
+ /**
279
+ * Find the end of a statement: skip whitespace and optional semicolon after
280
+ * a closing paren position.
281
+ */
282
+ export function findStatementEnd(code: string, pos: number): number {
283
+ let i = pos;
284
+ while (i < code.length && /\s/.test(code[i])) {
285
+ i++;
286
+ }
287
+ if (i < code.length && code[i] === ";") {
288
+ i++;
289
+ }
290
+ return i;
291
+ }
292
+
293
+ /**
294
+ * Escape special regex characters in a string so it can be safely
295
+ * interpolated into a RegExp pattern.
296
+ */
297
+ export function escapeRegExp(input: string): string {
298
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
299
+ }
@@ -0,0 +1,296 @@
1
+ import { parseAst } from "vite";
2
+ import {
3
+ findMatchingParen,
4
+ countArgs,
5
+ findStatementEnd,
6
+ buildExportMap,
7
+ escapeRegExp,
8
+ } from "../expose-id-utils.js";
9
+ import type { CreateExportBinding } from "./types.js";
10
+
11
+ /**
12
+ * Check whether every non-type export in `code` is accounted for by the given
13
+ * bindings. Returns false if any export exists that is not one of the known
14
+ * create* call locals/exports, allowing callers to bail out for mixed-export
15
+ * files.
16
+ */
17
+ export function isExportOnlyFile(
18
+ code: string,
19
+ bindings: CreateExportBinding[],
20
+ ): boolean {
21
+ if (bindings.length === 0) return false;
22
+
23
+ const knownLocals = new Set<string>();
24
+ const knownExports = new Set<string>();
25
+ for (const b of bindings) {
26
+ knownLocals.add(b.localName);
27
+ for (const e of b.exportNames) knownExports.add(e);
28
+ }
29
+
30
+ // Bail on star re-exports (unknown exports)
31
+ if (/export\s*\*/.test(code)) return false;
32
+
33
+ // Check `export const/let/var/function/class/default X` declarations
34
+ const declExportPattern =
35
+ /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
36
+ let match: RegExpExecArray | null;
37
+ while ((match = declExportPattern.exec(code)) !== null) {
38
+ if (!knownExports.has(match[2])) return false;
39
+ }
40
+
41
+ // Check `export { X }` and `export { X as Y }` specifiers: the local name
42
+ // must reference a known create* binding.
43
+ const specExportPattern = /export\s*\{([^}]+)\}/g;
44
+ while ((match = specExportPattern.exec(code)) !== null) {
45
+ const specifiers = match[1]
46
+ .split(",")
47
+ .map((s) => s.trim())
48
+ .filter(Boolean);
49
+ for (const spec of specifiers) {
50
+ const m = spec.match(
51
+ /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/,
52
+ );
53
+ if (!m) continue;
54
+ const local = m[1];
55
+ if (!knownLocals.has(local)) return false;
56
+ }
57
+ }
58
+
59
+ return true;
60
+ }
61
+
62
+ // NOTE: This regex may over-count when the fn name appears inside strings or
63
+ // comments, but it's only used for the warning heuristic (totalCalls >
64
+ // supportedBindings) and the inline-extraction pre-check, so over-counting
65
+ // triggers a harmless extra AST parse rather than affecting correctness.
66
+ export function countCreateCallsForNames(
67
+ code: string,
68
+ fnNames: string[],
69
+ ): number {
70
+ const pattern = new RegExp(
71
+ `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
72
+ "g",
73
+ );
74
+ return (code.match(pattern) || []).length;
75
+ }
76
+
77
+ export function getImportedFnNames(
78
+ code: string,
79
+ importedName: string,
80
+ ): string[] {
81
+ const importPattern =
82
+ /import\s*\{([^}]*)\}\s*from\s*["']@rangojs\/router(?:\/[^"']*)?["']/g;
83
+
84
+ const localNames = new Set<string>();
85
+ let match: RegExpExecArray | null;
86
+
87
+ while ((match = importPattern.exec(code)) !== null) {
88
+ const specList = match[1]
89
+ .split(",")
90
+ .map((s) => s.trim())
91
+ .filter(Boolean);
92
+
93
+ for (const spec of specList) {
94
+ const m = spec.match(
95
+ /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/,
96
+ );
97
+ if (!m) continue;
98
+ const imported = m[1];
99
+ const local = m[2] || imported;
100
+ if (imported === importedName) {
101
+ localNames.add(local);
102
+ }
103
+ }
104
+ }
105
+
106
+ const names = Array.from(localNames);
107
+ return names.length > 0 ? names : [importedName];
108
+ }
109
+
110
+ export function getCalledIdentifierFromCall(callExpr: any): string | null {
111
+ const callee = callExpr?.callee;
112
+ if (callee?.type === "Identifier") return callee.name;
113
+ if (
114
+ callee?.type === "TSInstantiationExpression" &&
115
+ callee.expression?.type === "Identifier"
116
+ ) {
117
+ return callee.expression.name;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ export function collectCreateExportBindingsFallback(
123
+ code: string,
124
+ fnNames: string[],
125
+ ): CreateExportBinding[] {
126
+ const alternation = fnNames.map(escapeRegExp).join("|");
127
+ const exportConstPattern = new RegExp(
128
+ `export\\s+const\\s+(\\w+)\\s*=\\s*(?:${alternation})\\s*(?:<[^>]*>)?\\s*\\(`,
129
+ "g",
130
+ );
131
+ const localDeclPattern = new RegExp(
132
+ `\\bconst\\s+(\\w+)\\s*=\\s*((?:${alternation})\\s*(?:<[^>]*>)?\\s*\\()`,
133
+ "g",
134
+ );
135
+ const exportSpecPattern = /export\s*\{([^}]+)\}/g;
136
+
137
+ const exportMap = new Map<string, string[]>();
138
+ const pushExport = (local: string, exported: string) => {
139
+ const list = exportMap.get(local);
140
+ if (list) {
141
+ if (!list.includes(exported)) list.push(exported);
142
+ return;
143
+ }
144
+ exportMap.set(local, [exported]);
145
+ };
146
+
147
+ let match: RegExpExecArray | null;
148
+ while ((match = exportConstPattern.exec(code)) !== null) {
149
+ pushExport(match[1], match[1]);
150
+ }
151
+
152
+ while ((match = exportSpecPattern.exec(code)) !== null) {
153
+ const specifiers = match[1]
154
+ .split(",")
155
+ .map((s) => s.trim())
156
+ .filter(Boolean);
157
+ for (const specifier of specifiers) {
158
+ const specMatch = specifier.match(
159
+ /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/,
160
+ );
161
+ if (!specMatch) continue;
162
+ const local = specMatch[1];
163
+ const exported = specMatch[2] || local;
164
+ pushExport(local, exported);
165
+ }
166
+ }
167
+
168
+ const bindings: CreateExportBinding[] = [];
169
+ while ((match = localDeclPattern.exec(code)) !== null) {
170
+ const localName = match[1];
171
+ const exportNames = exportMap.get(localName) ?? [];
172
+ if (exportNames.length === 0) continue;
173
+
174
+ const openParenPos = match.index + match[0].length - 1;
175
+ const closeParenPos = findMatchingParen(code, openParenPos + 1) - 1;
176
+ if (closeParenPos <= openParenPos) continue;
177
+
178
+ bindings.push({
179
+ localName,
180
+ exportNames,
181
+ callExprStart: match.index + match[0].length - match[2].length,
182
+ callOpenParenPos: openParenPos,
183
+ callCloseParenPos: closeParenPos,
184
+ argCount: countArgs(code, openParenPos + 1, closeParenPos),
185
+ statementEnd: findStatementEnd(code, closeParenPos + 1),
186
+ });
187
+ }
188
+
189
+ return bindings;
190
+ }
191
+
192
+ export function collectCreateExportBindings(
193
+ code: string,
194
+ fnNames: string[],
195
+ program?: any,
196
+ ): CreateExportBinding[] {
197
+ if (!program) {
198
+ try {
199
+ program = parseAst(code, { jsx: true });
200
+ } catch {
201
+ return collectCreateExportBindingsFallback(code, fnNames);
202
+ }
203
+ }
204
+
205
+ const exportMap = buildExportMap(program);
206
+ const fnNameSet = new Set(fnNames);
207
+ const bindings: CreateExportBinding[] = [];
208
+
209
+ const collectFromVarDecl = (varDecl: any, statementEnd: number) => {
210
+ if (varDecl?.type !== "VariableDeclaration" || varDecl.kind !== "const") {
211
+ return;
212
+ }
213
+
214
+ for (const decl of varDecl.declarations ?? []) {
215
+ const calledIdentifier = getCalledIdentifierFromCall(decl?.init);
216
+ if (
217
+ decl?.id?.type !== "Identifier" ||
218
+ decl?.init?.type !== "CallExpression" ||
219
+ !calledIdentifier ||
220
+ !fnNameSet.has(calledIdentifier)
221
+ ) {
222
+ continue;
223
+ }
224
+
225
+ const localName = decl.id.name;
226
+ const exportNames = exportMap.get(localName) ?? [];
227
+ if (exportNames.length === 0) continue;
228
+
229
+ const callStart = decl.init.start as number;
230
+ const callEnd = decl.init.end as number;
231
+ const calleeEnd = decl.init.callee.end as number;
232
+
233
+ let openParenPos = -1;
234
+ for (let i = calleeEnd; i < callEnd; i++) {
235
+ if (code[i] === "(") {
236
+ openParenPos = i;
237
+ break;
238
+ }
239
+ }
240
+ if (openParenPos === -1) continue;
241
+
242
+ const closeParenPos = findMatchingParen(code, openParenPos + 1) - 1;
243
+ if (closeParenPos <= openParenPos) continue;
244
+
245
+ bindings.push({
246
+ localName,
247
+ exportNames,
248
+ callExprStart: decl.init.start as number,
249
+ callOpenParenPos: openParenPos,
250
+ callCloseParenPos: closeParenPos,
251
+ argCount: decl.init.arguments?.length ?? 0,
252
+ statementEnd,
253
+ });
254
+ }
255
+ };
256
+
257
+ for (const node of program.body ?? []) {
258
+ if (node?.type === "VariableDeclaration") {
259
+ collectFromVarDecl(node, node.end as number);
260
+ continue;
261
+ }
262
+
263
+ if (
264
+ node?.type === "ExportNamedDeclaration" &&
265
+ node.declaration?.type === "VariableDeclaration"
266
+ ) {
267
+ collectFromVarDecl(node.declaration, node.end as number);
268
+ }
269
+ }
270
+
271
+ // When the JS parser misidentifies TypeScript generics (e.g.,
272
+ // createLocationState<{ text: string }>({...})) as binary expressions,
273
+ // the AST path finds 0 bindings even though calls exist. Fall back to
274
+ // regex-based detection which handles generics via <[^>]*> matching.
275
+ if (bindings.length === 0) {
276
+ return collectCreateExportBindingsFallback(code, fnNames);
277
+ }
278
+
279
+ return bindings;
280
+ }
281
+
282
+ export function buildUnsupportedShapeWarning(
283
+ filePath: string,
284
+ fnName: string,
285
+ ): string {
286
+ return [
287
+ `[rsc-router] Unsupported ${fnName} shape in "${filePath}".`,
288
+ `Supported shapes are:`,
289
+ ` - export const X = ${fnName}(...)`,
290
+ ` - const X = ${fnName}(...); export { X }`,
291
+ ` - const X = ${fnName}(...); export { X as Y }`,
292
+ `Potentially unsupported forms include:`,
293
+ ` - export let/var X = ${fnName}(...)`,
294
+ ` - inline ${fnName}(...) calls`,
295
+ ].join("\n");
296
+ }