@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,103 @@
1
+ // -- Build-time environment types -------------------------------------------
2
+
3
+ /**
4
+ * Context passed to a buildEnv factory function.
5
+ * Provides Vite config details for conditional env setup.
6
+ */
7
+ export interface BuildEnvFactoryContext {
8
+ /** Vite project root directory. */
9
+ root: string;
10
+ /** Vite mode (e.g. "development", "production"). */
11
+ mode: string;
12
+ /** Vite command ("serve" for dev, "build" for production). */
13
+ command: "serve" | "build";
14
+ /** Router deployment preset. */
15
+ preset: "node" | "cloudflare";
16
+ }
17
+
18
+ /**
19
+ * Factory function that creates build-time environment bindings.
20
+ * Called once at plugin startup. Return `dispose` to clean up resources.
21
+ */
22
+ export type BuildEnvFactory = (
23
+ ctx: BuildEnvFactoryContext,
24
+ ) => Promise<BuildEnvResult> | BuildEnvResult;
25
+
26
+ /**
27
+ * Result of resolving build-time environment bindings.
28
+ */
29
+ export interface BuildEnvResult {
30
+ /** Environment bindings available to Prerender/Static handlers via ctx.env. */
31
+ env: Record<string, unknown>;
32
+ /** Called after build completes to clean up resources (e.g., miniflare). */
33
+ dispose?: () => Promise<void> | void;
34
+ }
35
+
36
+ /**
37
+ * Build-time environment configuration for Prerender and Static handlers.
38
+ *
39
+ * - `false` (default): no build-time env, `ctx.env` throws.
40
+ * - `"auto"`: calls `wrangler.getPlatformProxy()` (cloudflare preset only).
41
+ * - Object: used directly as `ctx.env` during build.
42
+ * - Factory: called once at startup, must return `{ env, dispose? }`.
43
+ */
44
+ export type BuildEnvOption =
45
+ | false
46
+ | "auto"
47
+ | Record<string, unknown>
48
+ | BuildEnvFactory;
49
+
50
+ // -- Plugin options ---------------------------------------------------------
51
+
52
+ /**
53
+ * Base options shared by all presets
54
+ */
55
+ interface RangoBaseOptions {
56
+ /**
57
+ * Show startup banner. Set to false to disable.
58
+ * @default true
59
+ */
60
+ banner?: boolean;
61
+
62
+ /**
63
+ * Environment bindings available to Prerender and Static handlers at build
64
+ * time via `ctx.env`. Applies to both production build and dev on-demand
65
+ * prerender (`/__rsc_prerender`).
66
+ *
67
+ * This is the build-time env supplied by the Vite plugin, not the live
68
+ * request env. It is shared across all prerender invocations for the build.
69
+ *
70
+ * @default false
71
+ */
72
+ buildEnv?: BuildEnvOption;
73
+ }
74
+
75
+ /**
76
+ * Options for Node.js deployment (default)
77
+ */
78
+ export interface RangoNodeOptions extends RangoBaseOptions {
79
+ /**
80
+ * Deployment preset. Defaults to 'node' when not specified.
81
+ */
82
+ preset?: "node";
83
+ }
84
+
85
+ /**
86
+ * Options for Cloudflare Workers deployment
87
+ */
88
+ export interface RangoCloudflareOptions extends RangoBaseOptions {
89
+ /**
90
+ * Deployment preset for Cloudflare Workers.
91
+ * When using cloudflare preset:
92
+ * - @vitejs/plugin-rsc is NOT added (cloudflare plugin adds it)
93
+ * - Your worker entry (e.g., worker.rsc.tsx) imports the router directly
94
+ * - Browser and SSR use virtual entries
95
+ * - Build-time manifest generation is auto-detected from the resolved RSC environment config
96
+ */
97
+ preset: "cloudflare";
98
+ }
99
+
100
+ /**
101
+ * Options for rango() Vite plugin
102
+ */
103
+ export type RangoOptions = RangoNodeOptions | RangoCloudflareOptions;
@@ -0,0 +1,93 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
5
+ * The react-server-dom vendor files are shipped as CJS which doesn't work in browsers.
6
+ */
7
+ export function createCjsToEsmPlugin(): Plugin {
8
+ return {
9
+ name: "@rangojs/router:cjs-to-esm",
10
+ enforce: "pre",
11
+ transform(code, id) {
12
+ const cleanId = id.split("?")[0];
13
+
14
+ // Transform the client.browser.js entry point to re-export from CJS
15
+ if (
16
+ cleanId.includes("vendor/react-server-dom/client.browser.js") ||
17
+ cleanId.includes("vendor\\react-server-dom\\client.browser.js")
18
+ ) {
19
+ const isProd = process.env.NODE_ENV === "production";
20
+ const cjsFile = isProd
21
+ ? "./cjs/react-server-dom-webpack-client.browser.production.js"
22
+ : "./cjs/react-server-dom-webpack-client.browser.development.js";
23
+
24
+ return {
25
+ code: `export * from "${cjsFile}";`,
26
+ map: null,
27
+ };
28
+ }
29
+
30
+ // Transform the actual CJS files to ESM
31
+ if (
32
+ (cleanId.includes("vendor/react-server-dom/cjs/") ||
33
+ cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
34
+ cleanId.includes("client.browser")
35
+ ) {
36
+ let transformed = code;
37
+
38
+ // Extract the license comment to preserve it
39
+ const licenseMatch = transformed.match(/^\/\*\*[\s\S]*?\*\//);
40
+ const license = licenseMatch ? licenseMatch[0] : "";
41
+ if (license) {
42
+ transformed = transformed.slice(license.length);
43
+ }
44
+
45
+ // Remove "use strict" (both dev and prod have this)
46
+ transformed = transformed.replace(/^\s*["']use strict["'];\s*/, "");
47
+
48
+ // Remove the conditional IIFE wrapper (development only)
49
+ transformed = transformed.replace(
50
+ /^\s*["']production["']\s*!==\s*process\.env\.NODE_ENV\s*&&\s*\(function\s*\(\)\s*\{/,
51
+ "",
52
+ );
53
+
54
+ // Remove the closing of the conditional IIFE at the end (development only)
55
+ transformed = transformed.replace(/\}\)\(\);?\s*$/, "");
56
+
57
+ // Replace require('react') and require('react-dom') with imports (development)
58
+ transformed = transformed.replace(
59
+ /var\s+React\s*=\s*require\s*\(\s*["']react["']\s*\)\s*,[\s\n]+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
60
+ 'import React from "react";\nimport ReactDOM from "react-dom";\nvar ',
61
+ );
62
+
63
+ // Replace require('react-dom') only (production - doesn't import React)
64
+ transformed = transformed.replace(
65
+ /var\s+ReactDOM\s*=\s*require\s*\(\s*["']react-dom["']\s*\)\s*,/g,
66
+ 'import ReactDOM from "react-dom";\nvar ',
67
+ );
68
+
69
+ // Transform exports.xyz = function() to export function xyz()
70
+ transformed = transformed.replace(
71
+ /exports\.(\w+)\s*=\s*function\s*\(/g,
72
+ "export function $1(",
73
+ );
74
+
75
+ // Transform exports.xyz = value to export const xyz = value
76
+ transformed = transformed.replace(
77
+ /exports\.(\w+)\s*=/g,
78
+ "export const $1 =",
79
+ );
80
+
81
+ // Reconstruct with license at the top
82
+ transformed = license + "\n" + transformed;
83
+
84
+ return {
85
+ code: transformed,
86
+ map: null,
87
+ };
88
+ }
89
+
90
+ return null;
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,115 @@
1
+ import type { Plugin, ResolvedConfig } from "vite";
2
+
3
+ const CLIENT_IN_SERVER_PROXY_PREFIX =
4
+ "virtual:vite-rsc/client-in-server-package-proxy/";
5
+
6
+ /**
7
+ * Extract the bare package name from an absolute node_modules path.
8
+ * Handles scoped packages (@org/name) and nested node_modules.
9
+ * Returns null if the path doesn't contain a valid package reference.
10
+ *
11
+ * NOTE: This is a lossy transformation. It maps a specific submodule path
12
+ * (e.g., pkg/internal/context.js) to the package root (pkg). The load()
13
+ * hook then re-exports via the bare specifier, which resolves to the
14
+ * package entry point. This works for packages that barrel-export their
15
+ * "use client" symbols from the root, which covers the common case
16
+ * (component libraries like @mantine/core, @chakra-ui/react, etc.).
17
+ * Packages whose client symbols are only available from deep subpaths
18
+ * (not re-exported from the root) would lose those symbols after the
19
+ * rewrite. A more precise approach would resolve through the package's
20
+ * exports map to find the correct entry point, but that adds significant
21
+ * complexity for a rare edge case.
22
+ * See: https://github.com/cloudflare/vinext/pull/413
23
+ */
24
+ export function extractPackageName(absolutePath: string): string | null {
25
+ // Find the last /node_modules/ segment (handles nested node_modules)
26
+ const marker = "/node_modules/";
27
+ const idx = absolutePath.lastIndexOf(marker);
28
+ if (idx === -1) return null;
29
+
30
+ const afterModules = absolutePath.slice(idx + marker.length);
31
+
32
+ if (afterModules.startsWith("@")) {
33
+ // Scoped package: @org/name
34
+ const parts = afterModules.split("/");
35
+ if (parts.length < 2 || !parts[1]) return null;
36
+ return `${parts[0]}/${parts[1]}`;
37
+ }
38
+
39
+ // Unscoped package: name
40
+ const name = afterModules.split("/")[0];
41
+ return name || null;
42
+ }
43
+
44
+ /**
45
+ * Vite plugin that deduplicates client references from third-party packages
46
+ * in dev mode.
47
+ *
48
+ * When @vitejs/plugin-rsc encounters a "use client" submodule inside a
49
+ * package imported from a server component, it creates a
50
+ * client-in-server-package-proxy virtual module that re-exports from the
51
+ * absolute file path. In the client environment, this absolute path bypasses
52
+ * Vite's pre-bundling, while direct client imports of the same package go
53
+ * through .vite/deps/. Two separate module instances are created, breaking
54
+ * React contexts (createContext runs twice, provider/consumer mismatch).
55
+ *
56
+ * This plugin intercepts absolute node_modules imports from proxy modules
57
+ * in the client environment and rewrites them to bare specifier imports
58
+ * that go through pre-bundling, ensuring a single module instance.
59
+ *
60
+ * Dev-only: production builds use the SSR manifest which handles module
61
+ * identity correctly.
62
+ */
63
+ export function clientRefDedup(): Plugin {
64
+ let clientExclude: string[] = [];
65
+
66
+ return {
67
+ name: "@rangojs/router:client-ref-dedup",
68
+ enforce: "pre",
69
+ apply: "serve",
70
+
71
+ configResolved(config: ResolvedConfig) {
72
+ // Respect user's optimizeDeps.exclude — if a package is explicitly
73
+ // excluded from pre-bundling, we shouldn't redirect it there.
74
+ const clientEnv = config.environments?.["client"];
75
+ clientExclude =
76
+ clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
77
+ },
78
+
79
+ resolveId(source, importer, options) {
80
+ // Only intercept in the client environment
81
+ if (this.environment?.name !== "client") return;
82
+
83
+ // Only handle imports from client-in-server-package-proxy virtual modules
84
+ if (!importer?.includes(CLIENT_IN_SERVER_PROXY_PREFIX)) return;
85
+
86
+ // Only handle absolute node_modules paths
87
+ if (!source.includes("/node_modules/")) return;
88
+
89
+ // Must have an importer
90
+ if (!importer) return;
91
+
92
+ const packageName = extractPackageName(source);
93
+ if (!packageName) return;
94
+
95
+ // Don't redirect packages that are excluded from optimization
96
+ if (clientExclude.includes(packageName)) return;
97
+
98
+ // Return a virtual module that re-exports via bare specifier
99
+ return `\0rango:dedup/${packageName}`;
100
+ },
101
+
102
+ load(id) {
103
+ if (!id.startsWith("\0rango:dedup/")) return;
104
+
105
+ const packageName = id.slice("\0rango:dedup/".length);
106
+
107
+ // Re-export via bare specifier so Vite routes through pre-bundling
108
+ return [
109
+ `export * from ${JSON.stringify(packageName)};`,
110
+ `import * as __all__ from ${JSON.stringify(packageName)};`,
111
+ `export default __all__.default;`,
112
+ ].join("\n");
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,105 @@
1
+ import type { Plugin } from "vite";
2
+ import { relative } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ // Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
6
+ const CLIENT_PKG_PROXY_PREFIX =
7
+ "/@id/__x00__virtual:vite-rsc/client-package-proxy/";
8
+ const CLIENT_IN_SERVER_PKG_PROXY_PREFIX =
9
+ "/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/";
10
+ const FS_PREFIX = "/@fs/";
11
+
12
+ /**
13
+ * Compute the production SHA-256 hash for a dev-mode client reference key.
14
+ * Mirrors the hashing logic in @vitejs/plugin-rsc's build mode:
15
+ * - Local files: hashString(toRelativeId(id)) where toRelativeId = relative(root, id)
16
+ * - Package proxies: hashString(packageSource)
17
+ * - client-in-server-package proxies: hashString(relative(root, decodedAbsPath))
18
+ *
19
+ * Returns the input unchanged if it doesn't match a known dev-mode pattern
20
+ * (e.g., already a production hash).
21
+ */
22
+ export function computeProductionHash(
23
+ projectRoot: string,
24
+ refKey: string,
25
+ ): string {
26
+ let toHash: string;
27
+
28
+ if (refKey.startsWith(CLIENT_PKG_PROXY_PREFIX)) {
29
+ // /@id/__x00__virtual:vite-rsc/client-package-proxy/<pkg> -> hash("<pkg>")
30
+ toHash = refKey.slice(CLIENT_PKG_PROXY_PREFIX.length);
31
+ } else if (refKey.startsWith(CLIENT_IN_SERVER_PKG_PROXY_PREFIX)) {
32
+ // /@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/<encodedAbsPath>
33
+ const absPath = decodeURIComponent(
34
+ refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length),
35
+ );
36
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
37
+ } else if (refKey.startsWith(FS_PREFIX)) {
38
+ // /@fs/abs/path.tsx -> hash(relative(root, "/abs/path.tsx"))
39
+ const absPath = refKey.slice(FS_PREFIX.length - 1); // keep leading /
40
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
41
+ } else if (refKey.startsWith("/")) {
42
+ // /src/Button.tsx -> hash("src/Button.tsx")
43
+ toHash = refKey.slice(1);
44
+ } else {
45
+ // Already hashed or unknown format — return unchanged
46
+ return refKey;
47
+ }
48
+
49
+ return createHash("sha256").update(toHash).digest("hex").slice(0, 12);
50
+ }
51
+
52
+ // Regex to match registerClientReference() calls as emitted by @vitejs/plugin-rsc.
53
+ // Captures the reference key (second argument) from the call.
54
+ // Handles two proxy forms: parenthesized expression `(expr)` and arrow-throw `() => { ... }`.
55
+ const REGISTER_CLIENT_REF_RE =
56
+ /registerClientReference\(\s*(?:(?:\([^)]*\))|(?:\(\)[\s\S]*?\}))\s*,\s*"([^"]+)"\s*,\s*"[^"]+"\s*\)/g;
57
+
58
+ /**
59
+ * Transform source code by replacing dev-mode client reference keys with
60
+ * production hashes. Exported for testing; used internally by hashClientRefs.
61
+ * Returns null if no replacements were made.
62
+ */
63
+ export function transformClientRefs(
64
+ code: string,
65
+ projectRoot: string,
66
+ ): string | null {
67
+ if (!code.includes("registerClientReference")) return null;
68
+
69
+ let hasReplacement = false;
70
+ const result = code.replace(
71
+ REGISTER_CLIENT_REF_RE,
72
+ (match, refKey: string) => {
73
+ const hash = computeProductionHash(projectRoot, refKey);
74
+ if (hash === refKey) return match;
75
+ hasReplacement = true;
76
+ return match.replace(`"${refKey}"`, `"${hash}"`);
77
+ },
78
+ );
79
+
80
+ return hasReplacement ? result : null;
81
+ }
82
+
83
+ /**
84
+ * Vite plugin that rewrites registerClientReference() calls in the RSC
85
+ * environment, replacing dev-mode reference keys with production hashes.
86
+ *
87
+ * This runs AFTER the RSC plugin's transform so the Flight serializer
88
+ * naturally emits production IDs, eliminating the need for post-build
89
+ * regex replacement of Flight payloads.
90
+ */
91
+ export function hashClientRefs(projectRoot: string): Plugin {
92
+ return {
93
+ name: "@rangojs/router:hash-client-refs",
94
+ // Run after the RSC plugin's transform (default enforce is normal)
95
+ enforce: "post",
96
+ applyToEnvironment(env) {
97
+ return env.name === "rsc";
98
+ },
99
+ transform(code, _id) {
100
+ const result = transformClientRefs(code, projectRoot);
101
+ if (result === null) return;
102
+ return { code: result, map: null };
103
+ },
104
+ };
105
+ }
@@ -2,6 +2,7 @@ import type { Plugin, ResolvedConfig } from "vite";
2
2
  import MagicString from "magic-string";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
+ import { normalizePath } from "./expose-id-utils.js";
5
6
 
6
7
  /**
7
8
  * Type for the RSC plugin's manager API
@@ -34,12 +35,12 @@ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
34
35
  plugin = config.plugins.find(
35
36
  (p) =>
36
37
  (p.api as RscPluginApi | undefined)?.manager?.serverReferenceMetaMap !==
37
- undefined
38
+ undefined,
38
39
  );
39
40
  if (plugin) {
40
41
  console.warn(
41
42
  `[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
42
- `Consider updating the name lookup if the plugin was renamed.`
43
+ `Consider updating the name lookup if the plugin was renamed.`,
43
44
  );
44
45
  }
45
46
  }
@@ -47,13 +48,6 @@ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
47
48
  return plugin?.api as RscPluginApi | undefined;
48
49
  }
49
50
 
50
- /**
51
- * Normalize path to forward slashes
52
- */
53
- function normalizePath(p: string): string {
54
- return p.split(path.sep).join("/");
55
- }
56
-
57
51
  /**
58
52
  * Check if a file is a "use server" module (has the directive at the module level).
59
53
  * This distinguishes module-level server action files from files with inline actions.
@@ -88,21 +82,27 @@ function isUseServerModule(filePath: string): boolean {
88
82
  * @param sourceId - The source file identifier (for sourcemap)
89
83
  * @param hashToFileMap - Optional mapping from hash to file path (for server bundles)
90
84
  */
91
- function transformServerReferences(
85
+ /**
86
+ * Apply createServerReference wrapping to a MagicString instance.
87
+ * Returns true if any changes were made.
88
+ */
89
+ function applyServerReferenceWrapping(
92
90
  code: string,
93
- sourceId?: string,
94
- hashToFileMap?: Map<string, string>
95
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
91
+ s: MagicString,
92
+ hashToFileMap?: Map<string, string>,
93
+ ): boolean {
96
94
  if (!code.includes("createServerReference(")) {
97
- return null;
95
+ return false;
98
96
  }
99
97
 
100
98
  // Match: createServerReference("hash#actionName", ...) or $$ReactClient.createServerReference(...)
101
- // The RSC plugin uses $$ReactClient namespace in transformed code
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.
102
103
  const pattern =
103
104
  /((?:\$\$\w+\.)?createServerReference)\(("[^"]+#[^"]+")([^)]*)\)/g;
104
105
 
105
- const s = new MagicString(code);
106
106
  let hasChanges = false;
107
107
  let match: RegExpExecArray | null;
108
108
 
@@ -133,7 +133,16 @@ function transformServerReferences(
133
133
  s.overwrite(start, end, replacement);
134
134
  }
135
135
 
136
- if (!hasChanges) {
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)) {
137
146
  return null;
138
147
  }
139
148
 
@@ -159,20 +168,27 @@ function transformServerReferences(
159
168
  * @param sourceId - The source file identifier (for sourcemap)
160
169
  * @param hashToFileMap - Mapping from hash to file path (only module-level "use server" files)
161
170
  */
162
- function transformRegisterServerReference(
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(
163
179
  code: string,
164
- sourceId?: string,
165
- hashToFileMap?: Map<string, string>
166
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
167
- if (!hashToFileMap || !code.includes("registerServerReference(")) {
168
- return null;
180
+ s: MagicString,
181
+ hashToFileMap: Map<string, string>,
182
+ ): boolean {
183
+ if (!code.includes("registerServerReference(")) {
184
+ return false;
169
185
  }
170
186
 
171
187
  // Match: registerServerReference(fn, "hash", "exportName")
172
188
  // The hash is the second argument, exportName is the third
173
- const pattern = /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
189
+ const pattern =
190
+ /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
174
191
 
175
- const s = new MagicString(code);
176
192
  let hasChanges = false;
177
193
  let match: RegExpExecArray | null;
178
194
 
@@ -196,7 +212,18 @@ function transformRegisterServerReference(
196
212
  }
197
213
  }
198
214
 
199
- if (!hasChanges) {
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)) {
200
227
  return null;
201
228
  }
202
229
 
@@ -251,9 +278,7 @@ export function exposeActionId(): Plugin {
251
278
  if (!rscPluginApi) {
252
279
  throw new Error(
253
280
  "[rsc-router] Could not find @vitejs/plugin-rsc. " +
254
- "@rangojs/router requires the Vite RSC plugin.\n" +
255
- "The RSC plugin should be included automatically. If you disabled it with\n" +
256
- "rscRouter({ rsc: false }), add rsc() before rscRouter() in your config."
281
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
257
282
  );
258
283
  }
259
284
 
@@ -263,7 +288,7 @@ export function exposeActionId(): Plugin {
263
288
  const { serverReferenceMetaMap } = rscPluginApi.manager;
264
289
 
265
290
  for (const [absolutePath, meta] of Object.entries(
266
- serverReferenceMetaMap
291
+ serverReferenceMetaMap,
267
292
  )) {
268
293
  // Only include module-level "use server" files
269
294
  // Inline actions (defined in RSC components) should keep hashed IDs for client security
@@ -272,7 +297,7 @@ export function exposeActionId(): Plugin {
272
297
  }
273
298
 
274
299
  const relativePath = normalizePath(
275
- path.relative(config.root, absolutePath)
300
+ path.relative(config.root, absolutePath),
276
301
  );
277
302
 
278
303
  // The referenceKey in build mode is the hash
@@ -281,7 +306,6 @@ export function exposeActionId(): Plugin {
281
306
  }
282
307
  },
283
308
 
284
-
285
309
  // Dev mode only: transform hook runs after RSC plugin creates server references
286
310
  // In dev mode, IDs already contain file paths, not hashes
287
311
  transform(code, id) {
@@ -314,31 +338,26 @@ export function exposeActionId(): Plugin {
314
338
  // Only use file path mapping for RSC environment
315
339
  const effectiveMap = isRscEnv ? hashToFileMap : undefined;
316
340
 
317
- // Transform createServerReference calls (client-side)
318
- const result = transformServerReferences(
319
- code,
320
- chunk.fileName,
321
- effectiveMap
322
- );
323
-
324
- // For RSC bundles, also transform registerServerReference calls
325
- // This replaces hashed IDs with file paths so $id contains the actual path
341
+ // For RSC bundles, both createServerReference and registerServerReference
342
+ // may need transforming. Use a single MagicString for correct sourcemaps.
326
343
  if (isRscEnv && hashToFileMap) {
327
- const codeToTransform = result ? result.code : code;
328
- const registerResult = transformRegisterServerReference(
329
- codeToTransform,
330
- chunk.fileName,
331
- hashToFileMap
332
- );
333
- if (registerResult) {
334
- return { code: registerResult.code, map: registerResult.map };
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
+ };
335
355
  }
356
+ return null;
336
357
  }
337
358
 
338
- if (result) {
339
- return { code: result.code, map: result.map };
340
- }
341
- return null;
359
+ // Non-RSC environments: only transform createServerReference calls
360
+ return transformServerReferences(code, chunk.fileName, effectiveMap);
342
361
  },
343
362
  };
344
363
  }