@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /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
+ }