@rangojs/router 0.0.0-experimental.002d056c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,363 @@
1
+ import type { Plugin, ResolvedConfig } from "vite";
2
+ import MagicString from "magic-string";
3
+ import path from "node:path";
4
+ import fs from "node:fs";
5
+ import { normalizePath } from "./expose-id-utils.js";
6
+
7
+ /**
8
+ * Type for the RSC plugin's manager API
9
+ */
10
+ interface RscPluginManager {
11
+ serverReferenceMetaMap: Record<
12
+ string,
13
+ {
14
+ importId: string;
15
+ referenceKey: string;
16
+ exportNames: string[];
17
+ }
18
+ >;
19
+ config: ResolvedConfig;
20
+ }
21
+
22
+ interface RscPluginApi {
23
+ manager: RscPluginManager;
24
+ }
25
+
26
+ /**
27
+ * Get the RSC plugin's API from Vite config
28
+ */
29
+ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
30
+ // Try by name first
31
+ let plugin = config.plugins.find((p) => p.name === "rsc:minimal");
32
+
33
+ // Fallback: find by API structure if name lookup fails
34
+ if (!plugin) {
35
+ plugin = config.plugins.find(
36
+ (p) =>
37
+ (p.api as RscPluginApi | undefined)?.manager?.serverReferenceMetaMap !==
38
+ undefined,
39
+ );
40
+ if (plugin) {
41
+ console.warn(
42
+ `[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
43
+ `Consider updating the name lookup if the plugin was renamed.`,
44
+ );
45
+ }
46
+ }
47
+
48
+ return plugin?.api as RscPluginApi | undefined;
49
+ }
50
+
51
+ /**
52
+ * Check if a file is a "use server" module (has the directive at the module level).
53
+ * This distinguishes module-level server action files from files with inline actions.
54
+ *
55
+ * Module-level "use server" files should have their hash replaced with file paths
56
+ * for revalidation matching. Inline actions (defined in RSC components) should
57
+ * keep their hashed IDs for client security.
58
+ */
59
+ function isUseServerModule(filePath: string): boolean {
60
+ try {
61
+ const content = fs.readFileSync(filePath, "utf-8");
62
+ // Remove leading comments and whitespace to find the first meaningful content
63
+ const trimmed = content
64
+ .replace(/^\s*\/\/[^\n]*\n/gm, "") // Remove single-line comments
65
+ .replace(/^\s*\/\*[\s\S]*?\*\/\s*/gm, "") // Remove multi-line comments
66
+ .trimStart();
67
+
68
+ // Check if the file starts with "use server" directive
69
+ return (
70
+ trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'")
71
+ );
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Transform code to expose action IDs on createServerReference calls.
79
+ * Wraps each call with an IIFE that attaches $id to the returned function.
80
+ *
81
+ * @param code - The source code to transform
82
+ * @param sourceId - The source file identifier (for sourcemap)
83
+ * @param hashToFileMap - Optional mapping from hash to file path (for server bundles)
84
+ */
85
+ /**
86
+ * Apply createServerReference wrapping to a MagicString instance.
87
+ * Returns true if any changes were made.
88
+ */
89
+ function applyServerReferenceWrapping(
90
+ code: string,
91
+ s: MagicString,
92
+ hashToFileMap?: Map<string, string>,
93
+ ): boolean {
94
+ if (!code.includes("createServerReference(")) {
95
+ return false;
96
+ }
97
+
98
+ // Match: createServerReference("hash#actionName", ...) or $$ReactClient.createServerReference(...)
99
+ // The RSC plugin uses $$ReactClient namespace in transformed code.
100
+ // Note: [^)]* cannot handle nested parens in trailing args. This is safe in practice
101
+ // because the RSC plugin always generates simple variable references (e.g., callServer)
102
+ // as the second argument, never nested function calls.
103
+ const pattern =
104
+ /((?:\$\$\w+\.)?createServerReference)\(("[^"]+#[^"]+")([^)]*)\)/g;
105
+
106
+ let hasChanges = false;
107
+ let match: RegExpExecArray | null;
108
+
109
+ while ((match = pattern.exec(code)) !== null) {
110
+ hasChanges = true;
111
+ const [fullMatch, fnCall, idArg, rest] = match;
112
+ const start = match.index;
113
+ const end = start + fullMatch.length;
114
+
115
+ // Parse the ID to potentially replace hash with file path
116
+ let finalIdArg = idArg;
117
+ if (hashToFileMap) {
118
+ // idArg is like '"hash#actionName"', extract the parts
119
+ const idValue = idArg.slice(1, -1); // Remove quotes
120
+ const hashMatch = idValue.match(/^([^#]+)#(.+)$/);
121
+ if (hashMatch) {
122
+ const [, hash, actionName] = hashMatch;
123
+ const filePath = hashToFileMap.get(hash);
124
+ if (filePath) {
125
+ // Replace hash with file path for server-side
126
+ finalIdArg = `"${filePath}#${actionName}"`;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Wrap the createServerReference call to attach $$id to the returned function
132
+ const replacement = `(function(fn) { fn.$$id = ${finalIdArg}; return fn; })(${fnCall}(${idArg}${rest}))`;
133
+ s.overwrite(start, end, replacement);
134
+ }
135
+
136
+ return hasChanges;
137
+ }
138
+
139
+ function transformServerReferences(
140
+ code: string,
141
+ sourceId?: string,
142
+ hashToFileMap?: Map<string, string>,
143
+ ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
144
+ const s = new MagicString(code);
145
+ if (!applyServerReferenceWrapping(code, s, hashToFileMap)) {
146
+ return null;
147
+ }
148
+
149
+ return {
150
+ code: s.toString(),
151
+ map: s.generateMap({ source: sourceId, includeContent: true }),
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Transform registerServerReference calls in server bundles to use file paths instead of hashes.
157
+ * Pattern: registerServerReference(fn, "hash", "exportName")
158
+ * React's registerServerReference sets $$id = hash + "#" + exportName
159
+ * By replacing the hash with file path, $$id will contain the file path for revalidation matching.
160
+ *
161
+ * Only actions from module-level "use server" files are transformed.
162
+ * Inline actions (defined in RSC components with "use server" inside a function) are NOT in
163
+ * hashToFileMap and keep their hashed IDs. This is intentional for client security:
164
+ * - Module-level "use server" files: shared action modules, file path helps revalidation
165
+ * - Inline actions: one-off actions in RSC, hash ID prevents file path exposure to client
166
+ *
167
+ * @param code - The source code to transform
168
+ * @param sourceId - The source file identifier (for sourcemap)
169
+ * @param hashToFileMap - Mapping from hash to file path (only module-level "use server" files)
170
+ */
171
+ /**
172
+ * Apply registerServerReference wrapping to a MagicString instance.
173
+ * Returns true if any changes were made.
174
+ *
175
+ * Only actions from module-level "use server" files are transformed.
176
+ * Inline actions keep their hashed IDs for client security.
177
+ */
178
+ function applyRegisterReferenceWrapping(
179
+ code: string,
180
+ s: MagicString,
181
+ hashToFileMap: Map<string, string>,
182
+ ): boolean {
183
+ if (!code.includes("registerServerReference(")) {
184
+ return false;
185
+ }
186
+
187
+ // Match: registerServerReference(fn, "hash", "exportName")
188
+ // The hash is the second argument, exportName is the third
189
+ const pattern =
190
+ /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
191
+
192
+ let hasChanges = false;
193
+ let match: RegExpExecArray | null;
194
+
195
+ while ((match = pattern.exec(code)) !== null) {
196
+ const [fullMatch, fnArg, hash, exportName] = match;
197
+ const start = match.index;
198
+ const end = start + fullMatch.length;
199
+
200
+ // Look up the file path for this hash
201
+ const filePath = hashToFileMap.get(hash);
202
+ if (filePath) {
203
+ hasChanges = true;
204
+ // WRAP the call to add $id property with file path
205
+ // Keep the original hash for React's action registry (so loadServerAction works)
206
+ // Add $id (single dollar) with file path for revalidation matching
207
+ // Note: We use $id instead of $$id because React's registerServerReference
208
+ // sets $$id as a non-writable property
209
+ const filePathId = `${filePath}#${exportName}`;
210
+ const replacement = `(function(fn) { fn.$id = "${filePathId}"; return fn; })(registerServerReference(${fnArg}, "${hash}", "${exportName}"))`;
211
+ s.overwrite(start, end, replacement);
212
+ }
213
+ }
214
+
215
+ return hasChanges;
216
+ }
217
+
218
+ function transformRegisterServerReference(
219
+ code: string,
220
+ sourceId?: string,
221
+ hashToFileMap?: Map<string, string>,
222
+ ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
223
+ if (!hashToFileMap) return null;
224
+
225
+ const s = new MagicString(code);
226
+ if (!applyRegisterReferenceWrapping(code, s, hashToFileMap)) {
227
+ return null;
228
+ }
229
+
230
+ return {
231
+ code: s.toString(),
232
+ map: s.generateMap({ source: sourceId, includeContent: true }),
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Vite plugin that exposes action IDs on server reference functions.
238
+ *
239
+ * When React Server Components creates server references via createServerReference(),
240
+ * the action ID (format: "hash#actionName") is passed as the first argument but not
241
+ * exposed on the returned function. This plugin transforms the output to attach
242
+ * the $id property to each server reference function, enabling the router to
243
+ * identify which action was called during revalidation.
244
+ *
245
+ * Server bundles (RSC/SSR) get file paths in $id for filtering (e.g., "src/actions.ts#add").
246
+ * Client bundles keep hashed IDs for security (e.g., "ec387bc704d4#add").
247
+ *
248
+ * Works in:
249
+ * - Build mode: uses renderChunk to transform bundled chunks
250
+ * - Dev mode: uses transform with enforce:"post" to transform after RSC plugin
251
+ */
252
+ export function exposeActionId(): Plugin {
253
+ let config: ResolvedConfig;
254
+ let isBuild = false;
255
+ let hashToFileMap: Map<string, string> | undefined;
256
+ let rscPluginApi: RscPluginApi | undefined;
257
+
258
+ return {
259
+ name: "@rangojs/router:expose-action-id",
260
+ // Run after all other plugins (including RSC plugin's transforms)
261
+ enforce: "post",
262
+
263
+ configResolved(resolvedConfig) {
264
+ config = resolvedConfig;
265
+ isBuild = config.command === "build";
266
+
267
+ // Get RSC plugin API - rsc-router requires @vitejs/plugin-rsc
268
+ rscPluginApi = getRscPluginApi(config);
269
+ },
270
+
271
+ buildStart() {
272
+ // Verify RSC plugin is present at build start (after all config hooks have run)
273
+ // This allows rsc-router:rsc-integration to dynamically add the RSC plugin
274
+ if (!rscPluginApi) {
275
+ rscPluginApi = getRscPluginApi(config);
276
+ }
277
+
278
+ if (!rscPluginApi) {
279
+ throw new Error(
280
+ "[rsc-router] Could not find @vitejs/plugin-rsc. " +
281
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
282
+ );
283
+ }
284
+
285
+ if (!isBuild) return;
286
+
287
+ hashToFileMap = new Map();
288
+ const { serverReferenceMetaMap } = rscPluginApi.manager;
289
+
290
+ for (const [absolutePath, meta] of Object.entries(
291
+ serverReferenceMetaMap,
292
+ )) {
293
+ // Only include module-level "use server" files
294
+ // Inline actions (defined in RSC components) should keep hashed IDs for client security
295
+ if (!isUseServerModule(absolutePath)) {
296
+ continue;
297
+ }
298
+
299
+ const relativePath = normalizePath(
300
+ path.relative(config.root, absolutePath),
301
+ );
302
+
303
+ // The referenceKey in build mode is the hash
304
+ // Map hash -> relative file path
305
+ hashToFileMap.set(meta.referenceKey, relativePath);
306
+ }
307
+ },
308
+
309
+ // Dev mode only: transform hook runs after RSC plugin creates server references
310
+ // In dev mode, IDs already contain file paths, not hashes
311
+ transform(code, id) {
312
+ // Skip in build mode - renderChunk handles it
313
+ if (isBuild) {
314
+ return;
315
+ }
316
+
317
+ // Quick bail-out: only process if code has createServerReference
318
+ if (!code.includes("createServerReference(")) {
319
+ return;
320
+ }
321
+
322
+ // Skip node_modules
323
+ if (id.includes("/node_modules/")) {
324
+ return;
325
+ }
326
+
327
+ // Dev mode: no hash-to-file mapping needed (IDs are already file paths)
328
+ return transformServerReferences(code, id);
329
+ },
330
+
331
+ // Build mode: renderChunk runs after all transforms and bundling complete
332
+ renderChunk(code, chunk) {
333
+ // Only RSC bundle should get file paths for revalidation matching
334
+ // SSR bundle must NOT use file paths because client components run there
335
+ // and need to match the client bundle during hydration (otherwise: error #418)
336
+ const isRscEnv = this.environment?.name === "rsc";
337
+
338
+ // Only use file path mapping for RSC environment
339
+ const effectiveMap = isRscEnv ? hashToFileMap : undefined;
340
+
341
+ // For RSC bundles, both createServerReference and registerServerReference
342
+ // may need transforming. Use a single MagicString for correct sourcemaps.
343
+ if (isRscEnv && hashToFileMap) {
344
+ const s = new MagicString(code);
345
+ const changed1 = applyServerReferenceWrapping(code, s, effectiveMap);
346
+ const changed2 = applyRegisterReferenceWrapping(code, s, hashToFileMap);
347
+ if (changed1 || changed2) {
348
+ return {
349
+ code: s.toString(),
350
+ map: s.generateMap({
351
+ source: chunk.fileName,
352
+ includeContent: true,
353
+ }),
354
+ };
355
+ }
356
+ return null;
357
+ }
358
+
359
+ // Non-RSC environments: only transform createServerReference calls
360
+ return transformServerReferences(code, chunk.fileName, effectiveMap);
361
+ },
362
+ };
363
+ }
@@ -0,0 +1,287 @@
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
+ * Generate an 8-char hex hash for an inline static handler call site.
24
+ * Uses file path and line number (plus optional index for same-line collisions).
25
+ */
26
+ export function hashInlineId(
27
+ filePath: string,
28
+ lineNumber: number,
29
+ index?: number,
30
+ ): string {
31
+ const input =
32
+ index !== undefined && index > 0
33
+ ? `${filePath}:${lineNumber}:${index}`
34
+ : `${filePath}:${lineNumber}`;
35
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
36
+ }
37
+
38
+ export interface DetectedImports {
39
+ loader: boolean;
40
+ handle: boolean;
41
+ locationState: boolean;
42
+ prerenderHandler: boolean;
43
+ staticHandler: boolean;
44
+ router: boolean;
45
+ any: boolean;
46
+ }
47
+
48
+ /**
49
+ * Build a map from local binding name to exported names by walking
50
+ * ExportNamedDeclaration nodes. Handles `export const X`, `export { X }`,
51
+ * and `export { X as Y }`. Skips re-exports (`export { X } from "..."`).
52
+ */
53
+ export function buildExportMap(program: any): Map<string, string[]> {
54
+ const exportMap = new Map<string, string[]>();
55
+
56
+ const pushExport = (local: string, exported: string) => {
57
+ const list = exportMap.get(local);
58
+ if (list) {
59
+ if (!list.includes(exported)) list.push(exported);
60
+ return;
61
+ }
62
+ exportMap.set(local, [exported]);
63
+ };
64
+
65
+ for (const node of program.body ?? []) {
66
+ if (node?.type !== "ExportNamedDeclaration") continue;
67
+
68
+ if (node.declaration?.type === "VariableDeclaration") {
69
+ for (const decl of node.declaration.declarations ?? []) {
70
+ if (decl?.id?.type === "Identifier") {
71
+ pushExport(decl.id.name, decl.id.name);
72
+ }
73
+ }
74
+ }
75
+
76
+ if (!node.source && Array.isArray(node.specifiers)) {
77
+ for (const spec of node.specifiers) {
78
+ if (
79
+ spec?.type === "ExportSpecifier" &&
80
+ spec.local?.type === "Identifier" &&
81
+ spec.exported?.type === "Identifier"
82
+ ) {
83
+ pushExport(spec.local.name, spec.exported.name);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ return exportMap;
90
+ }
91
+
92
+ /**
93
+ * Single-pass detection of all create* imports from @rangojs/router.
94
+ * Returns which create functions are imported so we can skip unnecessary transforms.
95
+ */
96
+ export function detectImports(code: string): DetectedImports {
97
+ // Extract all import declarations from @rangojs/router in one scan
98
+ const importPattern =
99
+ /import\s*\{([^}]*)\}\s*from\s*["']@rangojs\/router(?:\/[^"']*)?["']/g;
100
+
101
+ const result: DetectedImports = {
102
+ loader: false,
103
+ handle: false,
104
+ locationState: false,
105
+ prerenderHandler: false,
106
+ staticHandler: false,
107
+ router: false,
108
+ any: false,
109
+ };
110
+
111
+ let match: RegExpExecArray | null;
112
+ while ((match = importPattern.exec(code)) !== null) {
113
+ const imports = match[1];
114
+ if (/\bcreateLoader\b/.test(imports)) result.loader = true;
115
+ if (/\bcreateHandle\b/.test(imports)) result.handle = true;
116
+ if (/\bcreateLocationState\b/.test(imports)) result.locationState = true;
117
+ if (/\bPrerender\b/.test(imports)) result.prerenderHandler = true;
118
+ if (/\bStatic\b/.test(imports)) result.staticHandler = true;
119
+ if (/\bcreateRouter\b/.test(imports)) result.router = true;
120
+ }
121
+
122
+ // createRouter has a stricter check: only from "@rangojs/router" (not sub-paths).
123
+ // NOTE: This is intentional — detectImports is used as a fast pre-filter in
124
+ // exposeInternalIds (which does NOT handle router transforms). The separate
125
+ // exposeRouterId plugin handles createRouter and DOES accept the /server subpath.
126
+ if (result.router) {
127
+ result.router =
128
+ /import\s*\{[^}]*\bcreateRouter\b[^}]*\}\s*from\s*["']@rangojs\/router["']/.test(
129
+ code,
130
+ );
131
+ }
132
+
133
+ result.any =
134
+ result.loader ||
135
+ result.handle ||
136
+ result.locationState ||
137
+ result.prerenderHandler ||
138
+ result.staticHandler ||
139
+ result.router;
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Skip past a string literal, template literal, or comment starting at pos.
146
+ * Returns the index after the closing delimiter, or pos if not at a
147
+ * string/comment start. Handles escape sequences and nested ${} in templates.
148
+ */
149
+ export function skipStringOrComment(code: string, pos: number): number {
150
+ const ch = code[pos];
151
+
152
+ if (ch === '"' || ch === "'") {
153
+ for (let j = pos + 1; j < code.length; j++) {
154
+ if (code[j] === "\\") {
155
+ j++;
156
+ continue;
157
+ }
158
+ if (code[j] === ch) return j + 1;
159
+ }
160
+ return code.length;
161
+ }
162
+
163
+ if (ch === "`") {
164
+ let j = pos + 1;
165
+ while (j < code.length) {
166
+ if (code[j] === "\\") {
167
+ j += 2;
168
+ continue;
169
+ }
170
+ if (code[j] === "`") return j + 1;
171
+ if (code[j] === "$" && j + 1 < code.length && code[j + 1] === "{") {
172
+ j += 2;
173
+ let braceDepth = 1;
174
+ while (j < code.length && braceDepth > 0) {
175
+ const inner = skipStringOrComment(code, j);
176
+ if (inner > j) {
177
+ j = inner;
178
+ continue;
179
+ }
180
+ if (code[j] === "{") braceDepth++;
181
+ else if (code[j] === "}") braceDepth--;
182
+ if (braceDepth > 0) j++;
183
+ }
184
+ if (braceDepth === 0) j++;
185
+ continue;
186
+ }
187
+ j++;
188
+ }
189
+ return j;
190
+ }
191
+
192
+ if (ch === "/" && pos + 1 < code.length) {
193
+ if (code[pos + 1] === "/") {
194
+ const eol = code.indexOf("\n", pos + 2);
195
+ return eol === -1 ? code.length : eol + 1;
196
+ }
197
+ if (code[pos + 1] === "*") {
198
+ const end = code.indexOf("*/", pos + 2);
199
+ return end === -1 ? code.length : end + 2;
200
+ }
201
+ }
202
+
203
+ return pos;
204
+ }
205
+
206
+ /**
207
+ * Find the matching closing paren starting after an already-opened paren.
208
+ * Skips strings, template literals, and comments so parens inside them
209
+ * don't affect depth tracking. Returns the index after the closing paren.
210
+ */
211
+ export function findMatchingParen(code: string, startPos: number): number {
212
+ let depth = 1;
213
+ let i = startPos;
214
+ while (i < code.length && depth > 0) {
215
+ const skipped = skipStringOrComment(code, i);
216
+ if (skipped > i) {
217
+ i = skipped;
218
+ continue;
219
+ }
220
+ if (code[i] === "(") depth++;
221
+ if (code[i] === ")") depth--;
222
+ i++;
223
+ }
224
+ return i;
225
+ }
226
+
227
+ /**
228
+ * Count the number of top-level arguments in a function call.
229
+ * Skips nested parens, brackets, braces, strings, and comments.
230
+ */
231
+ export function countArgs(
232
+ code: string,
233
+ startPos: number,
234
+ endPos: number,
235
+ ): number {
236
+ let depth = 0;
237
+ let argCount = 0;
238
+ let hasContent = false;
239
+ let i = startPos;
240
+
241
+ while (i < endPos) {
242
+ const skipped = skipStringOrComment(code, i);
243
+ if (skipped > i) {
244
+ hasContent = true;
245
+ i = skipped;
246
+ continue;
247
+ }
248
+
249
+ const char = code[i];
250
+ if (char === "(" || char === "[" || char === "{") {
251
+ depth++;
252
+ hasContent = true;
253
+ } else if (char === ")" || char === "]" || char === "}") {
254
+ depth--;
255
+ } else if (char === "," && depth === 0) {
256
+ argCount++;
257
+ } else if (!/\s/.test(char)) {
258
+ hasContent = true;
259
+ }
260
+ i++;
261
+ }
262
+
263
+ return hasContent ? argCount + 1 : 0;
264
+ }
265
+
266
+ /**
267
+ * Find the end of a statement: skip whitespace and optional semicolon after
268
+ * a closing paren position.
269
+ */
270
+ export function findStatementEnd(code: string, pos: number): number {
271
+ let i = pos;
272
+ while (i < code.length && /\s/.test(code[i])) {
273
+ i++;
274
+ }
275
+ if (i < code.length && code[i] === ";") {
276
+ i++;
277
+ }
278
+ return i;
279
+ }
280
+
281
+ /**
282
+ * Escape special regex characters in a string so it can be safely
283
+ * interpolated into a RegExp pattern.
284
+ */
285
+ export function escapeRegExp(input: string): string {
286
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
287
+ }