@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9

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 (293) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +242 -55
  3. package/dist/bin/rango.js +277 -99
  4. package/dist/vite/index.js +2929 -1132
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +68 -21
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +159 -10
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +262 -51
  16. package/skills/host-router/SKILL.md +243 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +291 -31
  22. package/skills/middleware/SKILL.md +49 -12
  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 +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +125 -102
  29. package/skills/rango/SKILL.md +242 -23
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +91 -8
  33. package/skills/router-setup/SKILL.md +98 -8
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +511 -188
  37. package/skills/typesafety/SKILL.md +354 -50
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +92 -0
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/link-interceptor.ts +4 -0
  50. package/src/browser/navigation-bridge.ts +183 -18
  51. package/src/browser/navigation-client.ts +187 -57
  52. package/src/browser/navigation-store.ts +75 -17
  53. package/src/browser/navigation-transaction.ts +21 -37
  54. package/src/browser/partial-update.ts +143 -40
  55. package/src/browser/prefetch/cache.ts +275 -28
  56. package/src/browser/prefetch/fetch.ts +191 -46
  57. package/src/browser/prefetch/policy.ts +6 -0
  58. package/src/browser/prefetch/queue.ts +123 -20
  59. package/src/browser/prefetch/resource-ready.ts +77 -0
  60. package/src/browser/rango-state.ts +53 -13
  61. package/src/browser/react/Link.tsx +98 -14
  62. package/src/browser/react/NavigationProvider.tsx +110 -33
  63. package/src/browser/react/context.ts +7 -2
  64. package/src/browser/react/filter-segment-order.ts +51 -7
  65. package/src/browser/react/index.ts +3 -0
  66. package/src/browser/react/location-state-shared.ts +175 -4
  67. package/src/browser/react/location-state.ts +39 -13
  68. package/src/browser/react/use-handle.ts +23 -64
  69. package/src/browser/react/use-navigation.ts +22 -2
  70. package/src/browser/react/use-params.ts +20 -8
  71. package/src/browser/react/use-reverse.ts +106 -0
  72. package/src/browser/react/use-router.ts +43 -10
  73. package/src/browser/react/use-segments.ts +11 -8
  74. package/src/browser/response-adapter.ts +25 -0
  75. package/src/browser/rsc-router.tsx +200 -75
  76. package/src/browser/scroll-restoration.ts +46 -39
  77. package/src/browser/segment-reconciler.ts +36 -9
  78. package/src/browser/segment-structure-assert.ts +2 -2
  79. package/src/browser/server-action-bridge.ts +31 -36
  80. package/src/browser/types.ts +81 -5
  81. package/src/build/collect-fallback-refs.ts +107 -0
  82. package/src/build/generate-manifest.ts +65 -40
  83. package/src/build/generate-route-types.ts +5 -0
  84. package/src/build/index.ts +2 -0
  85. package/src/build/route-trie.ts +69 -26
  86. package/src/build/route-types/codegen.ts +4 -4
  87. package/src/build/route-types/include-resolution.ts +9 -2
  88. package/src/build/route-types/per-module-writer.ts +7 -4
  89. package/src/build/route-types/router-processing.ts +278 -88
  90. package/src/build/route-types/scan-filter.ts +9 -2
  91. package/src/build/route-types/source-scan.ts +118 -0
  92. package/src/build/runtime-discovery.ts +9 -20
  93. package/src/cache/cache-runtime.ts +15 -11
  94. package/src/cache/cache-scope.ts +76 -49
  95. package/src/cache/cf/cf-cache-store.ts +501 -18
  96. package/src/cache/cf/index.ts +5 -1
  97. package/src/cache/document-cache.ts +17 -7
  98. package/src/cache/index.ts +1 -0
  99. package/src/cache/taint.ts +55 -0
  100. package/src/client.rsc.tsx +5 -1
  101. package/src/client.tsx +95 -284
  102. package/src/context-var.ts +72 -2
  103. package/src/debug.ts +2 -2
  104. package/src/decode-loader-results.ts +36 -0
  105. package/src/errors.ts +30 -1
  106. package/src/handle.ts +65 -12
  107. package/src/handles/breadcrumbs.ts +66 -0
  108. package/src/handles/index.ts +1 -0
  109. package/src/host/index.ts +2 -5
  110. package/src/host/router.ts +129 -57
  111. package/src/host/types.ts +31 -2
  112. package/src/host/utils.ts +1 -1
  113. package/src/href-client.ts +140 -20
  114. package/src/index.rsc.ts +15 -40
  115. package/src/index.ts +92 -76
  116. package/src/loader-store.ts +500 -0
  117. package/src/loader.rsc.ts +2 -5
  118. package/src/loader.ts +3 -10
  119. package/src/missing-id-error.ts +68 -0
  120. package/src/outlet-context.ts +1 -1
  121. package/src/prerender/store.ts +57 -15
  122. package/src/prerender.ts +141 -80
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -15
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +435 -260
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +110 -34
  129. package/src/route-definition/index.ts +3 -3
  130. package/src/route-definition/redirect.ts +11 -3
  131. package/src/route-definition/resolve-handler-use.ts +155 -0
  132. package/src/route-definition/use-item-types.ts +32 -0
  133. package/src/route-map-builder.ts +7 -1
  134. package/src/route-types.ts +37 -41
  135. package/src/router/basename.ts +14 -0
  136. package/src/router/content-negotiation.ts +113 -1
  137. package/src/router/error-handling.ts +1 -1
  138. package/src/router/find-match.ts +4 -2
  139. package/src/router/handler-context.ts +105 -39
  140. package/src/router/intercept-resolution.ts +15 -22
  141. package/src/router/lazy-includes.ts +12 -9
  142. package/src/router/loader-resolution.ts +175 -23
  143. package/src/router/logging.ts +5 -2
  144. package/src/router/manifest.ts +31 -16
  145. package/src/router/match-api.ts +129 -193
  146. package/src/router/match-handlers.ts +63 -20
  147. package/src/router/match-middleware/background-revalidation.ts +30 -2
  148. package/src/router/match-middleware/cache-lookup.ts +136 -106
  149. package/src/router/match-middleware/cache-store.ts +54 -10
  150. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  151. package/src/router/match-middleware/segment-resolution.ts +61 -5
  152. package/src/router/match-result.ts +124 -18
  153. package/src/router/metrics.ts +239 -14
  154. package/src/router/middleware-types.ts +61 -31
  155. package/src/router/middleware.ts +226 -124
  156. package/src/router/navigation-snapshot.ts +182 -0
  157. package/src/router/pattern-matching.ts +118 -19
  158. package/src/router/prerender-match.ts +114 -10
  159. package/src/router/preview-match.ts +32 -102
  160. package/src/router/request-classification.ts +286 -0
  161. package/src/router/revalidation.ts +85 -9
  162. package/src/router/route-snapshot.ts +245 -0
  163. package/src/router/router-context.ts +6 -1
  164. package/src/router/router-interfaces.ts +91 -29
  165. package/src/router/router-options.ts +89 -19
  166. package/src/router/router-registry.ts +2 -5
  167. package/src/router/segment-resolution/fresh.ts +240 -23
  168. package/src/router/segment-resolution/helpers.ts +30 -25
  169. package/src/router/segment-resolution/loader-cache.ts +1 -0
  170. package/src/router/segment-resolution/revalidation.ts +483 -289
  171. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  172. package/src/router/segment-wrappers.ts +2 -0
  173. package/src/router/substitute-pattern-params.ts +56 -0
  174. package/src/router/telemetry.ts +99 -0
  175. package/src/router/trie-matching.ts +38 -15
  176. package/src/router/types.ts +9 -0
  177. package/src/router/url-params.ts +49 -0
  178. package/src/router.ts +120 -32
  179. package/src/rsc/handler-context.ts +2 -2
  180. package/src/rsc/handler.ts +524 -370
  181. package/src/rsc/helpers.ts +91 -43
  182. package/src/rsc/index.ts +1 -21
  183. package/src/rsc/loader-fetch.ts +23 -3
  184. package/src/rsc/manifest-init.ts +5 -1
  185. package/src/rsc/origin-guard.ts +28 -10
  186. package/src/rsc/progressive-enhancement.ts +39 -10
  187. package/src/rsc/response-route-handler.ts +46 -53
  188. package/src/rsc/rsc-rendering.ts +69 -89
  189. package/src/rsc/runtime-warnings.ts +9 -10
  190. package/src/rsc/server-action.ts +39 -47
  191. package/src/rsc/ssr-setup.ts +144 -0
  192. package/src/rsc/types.ts +19 -3
  193. package/src/search-params.ts +20 -17
  194. package/src/segment-content-promise.ts +67 -0
  195. package/src/segment-loader-promise.ts +122 -0
  196. package/src/segment-system.tsx +219 -67
  197. package/src/serialize.ts +243 -0
  198. package/src/server/context.ts +285 -63
  199. package/src/server/cookie-store.ts +28 -4
  200. package/src/server/handle-store.ts +19 -0
  201. package/src/server/loader-registry.ts +9 -8
  202. package/src/server/request-context.ts +228 -65
  203. package/src/server.ts +6 -0
  204. package/src/ssr/index.tsx +9 -1
  205. package/src/static-handler.ts +19 -7
  206. package/src/testing/cache-status.ts +166 -0
  207. package/src/testing/collect-handle.ts +63 -0
  208. package/src/testing/dispatch.ts +440 -0
  209. package/src/testing/dom.entry.ts +22 -0
  210. package/src/testing/e2e/fixture.ts +154 -0
  211. package/src/testing/e2e/index.ts +149 -0
  212. package/src/testing/e2e/matchers.ts +51 -0
  213. package/src/testing/e2e/page-helpers.ts +272 -0
  214. package/src/testing/e2e/parity.ts +306 -0
  215. package/src/testing/e2e/server.ts +183 -0
  216. package/src/testing/flight-matchers.ts +104 -0
  217. package/src/testing/flight-runtime.d.ts +21 -0
  218. package/src/testing/flight.entry.ts +22 -0
  219. package/src/testing/flight.ts +182 -0
  220. package/src/testing/generated-routes.ts +223 -0
  221. package/src/testing/index.ts +98 -0
  222. package/src/testing/internal/context.ts +151 -0
  223. package/src/testing/render-route.tsx +536 -0
  224. package/src/testing/run-loader.ts +296 -0
  225. package/src/testing/run-middleware.ts +170 -0
  226. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  227. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  228. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  229. package/src/testing/vitest-stubs/version.ts +5 -0
  230. package/src/testing/vitest.ts +112 -0
  231. package/src/theme/index.ts +4 -13
  232. package/src/types/cache-types.ts +4 -4
  233. package/src/types/global-namespace.ts +39 -26
  234. package/src/types/handler-context.ts +197 -79
  235. package/src/types/index.ts +1 -0
  236. package/src/types/loader-types.ts +41 -15
  237. package/src/types/request-scope.ts +126 -0
  238. package/src/types/route-config.ts +17 -8
  239. package/src/types/route-entry.ts +19 -1
  240. package/src/types/segments.ts +37 -6
  241. package/src/urls/include-helper.ts +34 -67
  242. package/src/urls/index.ts +0 -3
  243. package/src/urls/path-helper-types.ts +50 -9
  244. package/src/urls/path-helper.ts +63 -63
  245. package/src/urls/pattern-types.ts +48 -19
  246. package/src/urls/response-types.ts +25 -22
  247. package/src/urls/type-extraction.ts +26 -116
  248. package/src/urls/urls-function.ts +1 -5
  249. package/src/use-loader.tsx +487 -44
  250. package/src/vite/debug.ts +185 -0
  251. package/src/vite/discovery/bundle-postprocess.ts +63 -91
  252. package/src/vite/discovery/discover-routers.ts +106 -53
  253. package/src/vite/discovery/discovery-errors.ts +194 -0
  254. package/src/vite/discovery/gate-state.ts +171 -0
  255. package/src/vite/discovery/prerender-collection.ts +222 -107
  256. package/src/vite/discovery/route-types-writer.ts +40 -84
  257. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  258. package/src/vite/discovery/state.ts +50 -13
  259. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  260. package/src/vite/index.ts +10 -3
  261. package/src/vite/plugin-types.ts +111 -72
  262. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  263. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  265. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  266. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  267. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  268. package/src/vite/plugins/expose-action-id.ts +55 -33
  269. package/src/vite/plugins/expose-id-utils.ts +24 -8
  270. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  271. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  272. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  273. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  274. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  275. package/src/vite/plugins/performance-tracks.ts +92 -0
  276. package/src/vite/plugins/refresh-cmd.ts +127 -0
  277. package/src/vite/plugins/use-cache-transform.ts +65 -50
  278. package/src/vite/plugins/version-injector.ts +39 -23
  279. package/src/vite/plugins/version-plugin.ts +72 -3
  280. package/src/vite/plugins/virtual-entries.ts +2 -2
  281. package/src/vite/rango.ts +265 -226
  282. package/src/vite/router-discovery.ts +924 -137
  283. package/src/vite/utils/ast-handler-extract.ts +15 -15
  284. package/src/vite/utils/banner.ts +4 -4
  285. package/src/vite/utils/bundle-analysis.ts +4 -2
  286. package/src/vite/utils/client-chunks.ts +190 -0
  287. package/src/vite/utils/forward-user-plugins.ts +193 -0
  288. package/src/vite/utils/manifest-utils.ts +21 -5
  289. package/src/vite/utils/package-resolution.ts +41 -1
  290. package/src/vite/utils/prerender-utils.ts +98 -5
  291. package/src/vite/utils/shared-utils.ts +109 -27
  292. package/src/browser/action-response-classifier.ts +0 -99
  293. package/src/route-definition/route-function.ts +0 -119
@@ -1,4 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
+ import { createRangoDebugger, NS } from "../debug.js";
3
+
4
+ const debug = createRangoDebugger(NS.transform);
2
5
 
3
6
  /**
4
7
  * Transform CJS vendor files from @vitejs/plugin-rsc to ESM for browser compatibility.
@@ -9,18 +12,16 @@ export function createCjsToEsmPlugin(): Plugin {
9
12
  name: "@rangojs/router:cjs-to-esm",
10
13
  enforce: "pre",
11
14
  transform(code, id) {
12
- const cleanId = id.split("?")[0];
15
+ const cleanId = id.split("?")[0].replaceAll("\\", "/");
13
16
 
14
17
  // 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
- ) {
18
+ if (cleanId.includes("vendor/react-server-dom/client.browser.js")) {
19
19
  const isProd = process.env.NODE_ENV === "production";
20
20
  const cjsFile = isProd
21
21
  ? "./cjs/react-server-dom-webpack-client.browser.production.js"
22
22
  : "./cjs/react-server-dom-webpack-client.browser.development.js";
23
23
 
24
+ debug?.("cjs-to-esm entry redirect %s", id);
24
25
  return {
25
26
  code: `export * from "${cjsFile}";`,
26
27
  map: null,
@@ -29,8 +30,7 @@ export function createCjsToEsmPlugin(): Plugin {
29
30
 
30
31
  // Transform the actual CJS files to ESM
31
32
  if (
32
- (cleanId.includes("vendor/react-server-dom/cjs/") ||
33
- cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
33
+ cleanId.includes("vendor/react-server-dom/cjs/") &&
34
34
  cleanId.includes("client.browser")
35
35
  ) {
36
36
  let transformed = code;
@@ -81,6 +81,7 @@ export function createCjsToEsmPlugin(): Plugin {
81
81
  // Reconstruct with license at the top
82
82
  transformed = license + "\n" + transformed;
83
83
 
84
+ debug?.("cjs-to-esm body rewrite %s", id);
84
85
  return {
85
86
  code: transformed,
86
87
  map: null,
@@ -1,4 +1,7 @@
1
1
  import type { Plugin, ResolvedConfig } from "vite";
2
+ import { createRangoDebugger, NS } from "../debug.js";
3
+
4
+ const debug = createRangoDebugger(NS.transform);
2
5
 
3
6
  const CLIENT_IN_SERVER_PROXY_PREFIX =
4
7
  "virtual:vite-rsc/client-in-server-package-proxy/";
@@ -62,6 +65,7 @@ export function extractPackageName(absolutePath: string): string | null {
62
65
  */
63
66
  export function clientRefDedup(): Plugin {
64
67
  let clientExclude: string[] = [];
68
+ const dedupedPackages = new Set<string>();
65
69
 
66
70
  return {
67
71
  name: "@rangojs/router:client-ref-dedup",
@@ -76,6 +80,16 @@ export function clientRefDedup(): Plugin {
76
80
  clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
77
81
  },
78
82
 
83
+ buildEnd() {
84
+ if (debug && dedupedPackages.size > 0) {
85
+ debug(
86
+ "client-ref-dedup: redirected %d package(s) (%s)",
87
+ dedupedPackages.size,
88
+ [...dedupedPackages].join(","),
89
+ );
90
+ }
91
+ },
92
+
79
93
  resolveId(source, importer, options) {
80
94
  // Only intercept in the client environment
81
95
  if (this.environment?.name !== "client") return;
@@ -95,6 +109,8 @@ export function clientRefDedup(): Plugin {
95
109
  // Don't redirect packages that are excluded from optimization
96
110
  if (clientExclude.includes(packageName)) return;
97
111
 
112
+ if (debug) dedupedPackages.add(packageName);
113
+
98
114
  // Return a virtual module that re-exports via bare specifier
99
115
  return `\0rango:dedup/${packageName}`;
100
116
  },
@@ -1,6 +1,9 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { relative } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
5
+
6
+ const debug = createRangoDebugger(NS.transform);
4
7
 
5
8
  // Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
6
9
  const CLIENT_PKG_PROXY_PREFIX =
@@ -19,6 +22,17 @@ const FS_PREFIX = "/@fs/";
19
22
  * Returns the input unchanged if it doesn't match a known dev-mode pattern
20
23
  * (e.g., already a production hash).
21
24
  */
25
+ /**
26
+ * The production client-reference key hash: `sha256(relativeId).slice(0,12)`,
27
+ * matching @vitejs/plugin-rsc's `hashString`. Exported so the client-chunks
28
+ * strategy can hash a `clientChunks` callback's `meta.normalizedId` (already the
29
+ * project-root-relative id) and compare it against fallback hashes collected
30
+ * during discovery.
31
+ */
32
+ export function hashRefKey(relativeId: string): string {
33
+ return createHash("sha256").update(relativeId).digest("hex").slice(0, 12);
34
+ }
35
+
22
36
  export function computeProductionHash(
23
37
  projectRoot: string,
24
38
  refKey: string,
@@ -46,7 +60,7 @@ export function computeProductionHash(
46
60
  return refKey;
47
61
  }
48
62
 
49
- return createHash("sha256").update(toHash).digest("hex").slice(0, 12);
63
+ return hashRefKey(toHash);
50
64
  }
51
65
 
52
66
  // Regex to match registerClientReference() calls as emitted by @vitejs/plugin-rsc.
@@ -89,6 +103,7 @@ export function transformClientRefs(
89
103
  * regex replacement of Flight payloads.
90
104
  */
91
105
  export function hashClientRefs(projectRoot: string): Plugin {
106
+ const counter = createCounter(debug, "hash-client-refs");
92
107
  return {
93
108
  name: "@rangojs/router:hash-client-refs",
94
109
  // Run after the RSC plugin's transform (default enforce is normal)
@@ -96,10 +111,18 @@ export function hashClientRefs(projectRoot: string): Plugin {
96
111
  applyToEnvironment(env) {
97
112
  return env.name === "rsc";
98
113
  },
99
- transform(code, _id) {
100
- const result = transformClientRefs(code, projectRoot);
101
- if (result === null) return;
102
- return { code: result, map: null };
114
+ buildEnd() {
115
+ counter?.flush();
116
+ },
117
+ transform(code, id) {
118
+ const start = counter ? performance.now() : 0;
119
+ try {
120
+ const result = transformClientRefs(code, projectRoot);
121
+ if (result === null) return;
122
+ return { code: result, map: null };
123
+ } finally {
124
+ counter?.record(id, performance.now() - start);
125
+ }
103
126
  },
104
127
  };
105
128
  }
@@ -0,0 +1,23 @@
1
+ export interface LoaderResolveContext {
2
+ parentURL?: string;
3
+ conditions?: readonly string[];
4
+ importAttributes?: Record<string, string>;
5
+ }
6
+
7
+ export interface LoaderResolveResult {
8
+ shortCircuit?: boolean;
9
+ url: string;
10
+ format?: "module" | "commonjs" | "json" | "wasm" | null;
11
+ importAttributes?: Record<string, string>;
12
+ }
13
+
14
+ export type NextResolve = (
15
+ specifier: string,
16
+ context?: LoaderResolveContext,
17
+ ) => Promise<LoaderResolveResult>;
18
+
19
+ export function resolve(
20
+ specifier: string,
21
+ context: LoaderResolveContext,
22
+ nextResolve: NextResolve,
23
+ ): Promise<LoaderResolveResult>;
@@ -0,0 +1,76 @@
1
+ // Node ESM loader hook that resolves `cloudflare:*` imports to the same
2
+ // stub ESM the Vite transform produces for rewritten specifiers.
3
+ //
4
+ // Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
5
+ // imports in modules that flow through Vite's plugin pipeline — covers
6
+ // user source and any node_modules package Vite fetches and transforms.
7
+ // But Vite/Rollup externalize certain packages (e.g. `partyserver`,
8
+ // which has `import { DurableObject, env } from "cloudflare:workers"`
9
+ // at its top level, and similar "workerd-native" libraries). Externalized
10
+ // modules bypass the transform: Rollup hands their resolution to Node's
11
+ // native ESM loader, which rejects URL-scheme specifiers. This loader
12
+ // hook registers via `module.register()` from `createTempRscServer` and
13
+ // intercepts `cloudflare:*` at Node's resolve layer — before the default
14
+ // loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
15
+ //
16
+ // Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
17
+ // architecture) with its own globalThis. It cannot see the main thread's
18
+ // `__rango_build_env__` bridge, so the `env` export here is always `{}`.
19
+ // That's fine in practice — externalized libraries don't typically touch
20
+ // `env` at module top level; they read it at request time in workerd
21
+ // where the real module exists. Build-time prerender handlers in user
22
+ // source DO read `env`, but they flow through the Vite transform (which
23
+ // does bridge `env` from `getPlatformProxy()`), not through this loader.
24
+ //
25
+ // Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
26
+ // to hand out the same base classes.
27
+
28
+ const CF_PREFIX = "cloudflare:";
29
+
30
+ const STUBS = {
31
+ "cloudflare:workers": `
32
+ export class DurableObject { constructor(_ctx, _env) {} }
33
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
34
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
35
+ export class RpcTarget {}
36
+ export const env = {};
37
+ export default {};
38
+ `,
39
+ "cloudflare:email": `
40
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
41
+ export default {};
42
+ `,
43
+ "cloudflare:sockets": `
44
+ export function connect() { return {}; }
45
+ export default {};
46
+ `,
47
+ "cloudflare:workflows": `
48
+ export class NonRetryableError extends Error {
49
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
50
+ }
51
+ export default {};
52
+ `,
53
+ };
54
+
55
+ // Policy: unknown `cloudflare:*` specifiers resolve permissively to an
56
+ // empty default export rather than throwing. Same reasoning as
57
+ // cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
58
+ // dependency-graph resilience over strict validation, because third-party
59
+ // packages can pull `cloudflare:*` modules we haven't curated.
60
+ const FALLBACK_STUB = `export default {};\n`;
61
+
62
+ function dataUrlFor(specifier) {
63
+ const body = STUBS[specifier] ?? FALLBACK_STUB;
64
+ return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
65
+ }
66
+
67
+ export async function resolve(specifier, context, nextResolve) {
68
+ if (specifier.startsWith(CF_PREFIX)) {
69
+ return {
70
+ shortCircuit: true,
71
+ url: dataUrlFor(specifier),
72
+ format: "module",
73
+ };
74
+ }
75
+ return nextResolve(specifier, context);
76
+ }
@@ -0,0 +1,214 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ const VIRTUAL_PREFIX = "virtual:rango-cloudflare-stub-";
4
+ const NULL_PREFIX = "\0" + VIRTUAL_PREFIX;
5
+ const CF_PREFIX = "cloudflare:";
6
+
7
+ /**
8
+ * `globalThis` key the `cloudflare:workers` stub reads to populate its
9
+ * `env` export. Router discovery sets this to the resolved `buildEnv`
10
+ * proxy (from `wrangler.getPlatformProxy()` when `buildEnv: "auto"` is
11
+ * configured, or a user-supplied object otherwise) before importing the
12
+ * worker entry, and clears it after discovery disposes the proxy. When
13
+ * unset, the stub's `env` falls back to `{}`.
14
+ *
15
+ * Using `globalThis` is the only cross-module bridge that works here:
16
+ * the stub's `load` hook returns source text, not a live closure, but
17
+ * the stub module is evaluated in the same Node process as the
18
+ * discovery plugin — so reading a global at module-evaluation time
19
+ * reaches whatever the plugin assigned there. A symbol key would be
20
+ * cleaner in-process but awkward to name from the stub source.
21
+ *
22
+ * @internal
23
+ */
24
+ export const BUILD_ENV_GLOBAL_KEY = "__rango_build_env__";
25
+
26
+ const SOURCE_EXT_RE = /\.[mc]?[jt]sx?$/;
27
+
28
+ const IMPORT_NODE_TYPES = new Set([
29
+ "ImportDeclaration",
30
+ "ImportExpression",
31
+ "ExportNamedDeclaration",
32
+ "ExportAllDeclaration",
33
+ ]);
34
+
35
+ // Keep in sync with `STUBS` in cloudflare-protocol-loader-hook.mjs —
36
+ // both paths (Vite transform and Node loader) need to hand out the same
37
+ // classes. Unknown `cloudflare:*` modules fall back to an empty default
38
+ // export so third-party packages (e.g. the Cloudflare Agents SDK) can
39
+ // pull them into the graph without crashing discovery. Discovery only
40
+ // evaluates module top-level code — no handlers run — so missing named
41
+ // exports only fail if something does `class X extends Missing {}` at
42
+ // module scope, which is rare outside the already-stubbed classes.
43
+ const STUBS: Record<string, string> = {
44
+ "cloudflare:workers": `
45
+ export class DurableObject { constructor(_ctx, _env) {} }
46
+ export class WorkerEntrypoint { constructor(_ctx, _env) {} }
47
+ export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
48
+ export class RpcTarget {}
49
+ export const env = globalThis[${JSON.stringify(BUILD_ENV_GLOBAL_KEY)}] ?? {};
50
+ export default {};
51
+ `,
52
+ "cloudflare:email": `
53
+ export class EmailMessage { constructor(_from, _to, _raw) {} }
54
+ export default {};
55
+ `,
56
+ "cloudflare:sockets": `
57
+ export function connect() { return {}; }
58
+ export default {};
59
+ `,
60
+ "cloudflare:workflows": `
61
+ export class NonRetryableError extends Error {
62
+ constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
63
+ }
64
+ export default {};
65
+ `,
66
+ };
67
+
68
+ // Policy: unknown `cloudflare:*` specifiers resolve permissively (empty
69
+ // default export) rather than throwing. We prioritize dependency-graph
70
+ // resilience over strict validation of user imports because third-party
71
+ // packages can pull `cloudflare:*` modules we haven't curated, and
72
+ // discovery should not fail just because those modules appear in the graph.
73
+ // Tradeoff: unsupported user-authored `cloudflare:*` imports may fail later
74
+ // with a generic JS/module error instead of a tailored rango-branded hint.
75
+ // The test below pins this behavior so dependency compatibility is not
76
+ // regressed accidentally.
77
+ const FALLBACK_STUB = `export default {};\n`;
78
+
79
+ interface AstNode {
80
+ type: string;
81
+ start?: number;
82
+ end?: number;
83
+ source?: AstNode | null;
84
+ value?: unknown;
85
+ [key: string]: unknown;
86
+ }
87
+
88
+ /**
89
+ * Stubs `cloudflare:*` imports for the discovery-time Node Vite server.
90
+ *
91
+ * Discovery only evaluates user module top-level code — it never invokes
92
+ * DurableObject / WorkerEntrypoint / Workflow handlers — so empty base
93
+ * classes are enough for `class X extends DurableObject {}` declarations
94
+ * to load in Node, where `cloudflare:*` is otherwise unresolvable.
95
+ *
96
+ * Interception point: a transform hook parses source with Rollup's
97
+ * plugin-context parser (`this.parse`) and rewrites only real import
98
+ * specifier spans (`import ... from "cloudflare:xxx"`,
99
+ * `import("cloudflare:xxx")`, `export ... from "cloudflare:xxx"`) to a
100
+ * plain virtual module name (`virtual:rango-cloudflare-stub-xxx`).
101
+ * This must be done in transform because Vite's module runner routes
102
+ * URL-scheme specifiers straight to Node's native ESM loader without
103
+ * consulting plugin `resolveId` hooks. Using the AST (instead of a
104
+ * text regex or a permissive lexer) guarantees that strings,
105
+ * comments, and template literals that merely contain import-like
106
+ * text are never mutated — the walker only looks at the four import
107
+ * node types.
108
+ *
109
+ * The transform runs on user source AND on compiled node_modules
110
+ * output: real-world CF packages (e.g. the Cloudflare Agents SDK)
111
+ * ship compiled JS that contains `import ... from "cloudflare:email"`
112
+ * and similar, so excluding node_modules would leave those imports
113
+ * unrewritten. Cost is small because the early exit (`code.includes`)
114
+ * skips files with no cloudflare: mention.
115
+ *
116
+ * The plugin intentionally runs at Vite's default ordering (no
117
+ * `enforce: "pre"`) so TS/JSX has already been compiled to plain JS
118
+ * by the time `this.parse` runs — acorn doesn't understand
119
+ * non-standard syntax.
120
+ *
121
+ * `cloudflare:workers`, `cloudflare:email`, `cloudflare:sockets`, and
122
+ * `cloudflare:workflows` each get curated stubs with the well-known
123
+ * symbols that appear in top-level `extends` positions. Any other
124
+ * `cloudflare:*` specifier falls back to an empty default export —
125
+ * discovery never executes the handlers, so an empty module is safe
126
+ * for anything the graph pulls in transitively.
127
+ *
128
+ * Only registered in the discovery temp server, not the user's runtime
129
+ * config.
130
+ * @internal
131
+ */
132
+ export function createCloudflareProtocolStubPlugin(): Plugin {
133
+ return {
134
+ name: "@rangojs/router:cloudflare-protocol-stub",
135
+ transform(code, id) {
136
+ const cleanId = id.split("?")[0] ?? id;
137
+ if (!SOURCE_EXT_RE.test(cleanId)) return null;
138
+ if (!code.includes(CF_PREFIX)) return null;
139
+
140
+ let ast: AstNode;
141
+ try {
142
+ ast = this.parse(code, { lang: "tsx" }) as unknown as AstNode;
143
+ } catch {
144
+ // Malformed source — let a downstream plugin surface the parse error.
145
+ return null;
146
+ }
147
+
148
+ const hits: Array<{ start: number; end: number; value: string }> = [];
149
+ walk(ast, (node) => {
150
+ if (!IMPORT_NODE_TYPES.has(node.type)) return;
151
+ const source = node.source;
152
+ if (!source || source.type !== "Literal") return;
153
+ if (typeof source.value !== "string") return;
154
+ if (!source.value.startsWith(CF_PREFIX)) return;
155
+ if (typeof source.start !== "number" || typeof source.end !== "number")
156
+ return;
157
+ hits.push({
158
+ start: source.start,
159
+ end: source.end,
160
+ value: source.value,
161
+ });
162
+ });
163
+
164
+ if (hits.length === 0) return null;
165
+
166
+ // Rewrite from last to first so earlier offsets stay valid. `start`/
167
+ // `end` span the full literal including quotes, so we re-emit the
168
+ // same quote character around the new specifier.
169
+ hits.sort((a, b) => b.start - a.start);
170
+ let out = code;
171
+ for (const hit of hits) {
172
+ const submodule = hit.value.slice(CF_PREFIX.length);
173
+ const quote = code[hit.start] === "'" ? "'" : '"';
174
+ out =
175
+ out.slice(0, hit.start) +
176
+ quote +
177
+ VIRTUAL_PREFIX +
178
+ submodule +
179
+ quote +
180
+ out.slice(hit.end);
181
+ }
182
+ return { code: out, map: null };
183
+ },
184
+ resolveId(id) {
185
+ if (id.startsWith(VIRTUAL_PREFIX)) {
186
+ return "\0" + id;
187
+ }
188
+ return null;
189
+ },
190
+ load(id) {
191
+ if (!id.startsWith(NULL_PREFIX)) return null;
192
+ const submodule = id.slice(NULL_PREFIX.length);
193
+ const specifier = CF_PREFIX + submodule;
194
+ return STUBS[specifier] ?? FALLBACK_STUB;
195
+ },
196
+ };
197
+ }
198
+
199
+ function walk(node: unknown, visit: (n: AstNode) => void): void {
200
+ if (!node || typeof node !== "object") return;
201
+ if (Array.isArray(node)) {
202
+ for (const child of node) walk(child, visit);
203
+ return;
204
+ }
205
+ const n = node as AstNode;
206
+ if (typeof n.type !== "string") return;
207
+ visit(n);
208
+ for (const key in n) {
209
+ if (key === "loc" || key === "start" || key === "end" || key === "range") {
210
+ continue;
211
+ }
212
+ walk(n[key], visit);
213
+ }
214
+ }
@@ -3,6 +3,9 @@ import MagicString from "magic-string";
3
3
  import path from "node:path";
4
4
  import fs from "node:fs";
5
5
  import { normalizePath } from "./expose-id-utils.js";
6
+ import { createRangoDebugger, createCounter, NS } from "../debug.js";
7
+
8
+ const debug = createRangoDebugger(NS.transform);
6
9
 
7
10
  /**
8
11
  * Type for the RSC plugin's manager API
@@ -39,7 +42,7 @@ function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
39
42
  );
40
43
  if (plugin) {
41
44
  console.warn(
42
- `[rsc-router:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
45
+ `[rango:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
43
46
  `Consider updating the name lookup if the plugin was renamed.`,
44
47
  );
45
48
  }
@@ -254,6 +257,8 @@ export function exposeActionId(): Plugin {
254
257
  let isBuild = false;
255
258
  let hashToFileMap: Map<string, string> | undefined;
256
259
  let rscPluginApi: RscPluginApi | undefined;
260
+ const counterTransform = createCounter(debug, "expose-action-id transform");
261
+ const counterRender = createCounter(debug, "expose-action-id renderChunk");
257
262
 
258
263
  return {
259
264
  name: "@rangojs/router:expose-action-id",
@@ -268,6 +273,11 @@ export function exposeActionId(): Plugin {
268
273
  rscPluginApi = getRscPluginApi(config);
269
274
  },
270
275
 
276
+ buildEnd() {
277
+ counterTransform?.flush();
278
+ counterRender?.flush();
279
+ },
280
+
271
281
  buildStart() {
272
282
  // Verify RSC plugin is present at build start (after all config hooks have run)
273
283
  // This allows rsc-router:rsc-integration to dynamically add the RSC plugin
@@ -277,10 +287,8 @@ export function exposeActionId(): Plugin {
277
287
 
278
288
  if (!rscPluginApi) {
279
289
  throw new Error(
280
- "[rsc-router] Could not find @vitejs/plugin-rsc. " +
281
- "@rangojs/router requires the Vite RSC plugin.\n" +
282
- "The RSC plugin should be included automatically. If you disabled it with\n" +
283
- "rango({ rsc: false }), add rsc() before rango() in your config.",
290
+ "[rango] Could not find @vitejs/plugin-rsc. " +
291
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
284
292
  );
285
293
  }
286
294
 
@@ -326,40 +334,54 @@ export function exposeActionId(): Plugin {
326
334
  return;
327
335
  }
328
336
 
329
- // Dev mode: no hash-to-file mapping needed (IDs are already file paths)
330
- return transformServerReferences(code, id);
337
+ const start = counterTransform ? performance.now() : 0;
338
+ try {
339
+ // Dev mode: no hash-to-file mapping needed (IDs are already file paths)
340
+ return transformServerReferences(code, id);
341
+ } finally {
342
+ counterTransform?.record(id, performance.now() - start);
343
+ }
331
344
  },
332
345
 
333
346
  // Build mode: renderChunk runs after all transforms and bundling complete
334
347
  renderChunk(code, chunk) {
335
- // Only RSC bundle should get file paths for revalidation matching
336
- // SSR bundle must NOT use file paths because client components run there
337
- // and need to match the client bundle during hydration (otherwise: error #418)
338
- const isRscEnv = this.environment?.name === "rsc";
339
-
340
- // Only use file path mapping for RSC environment
341
- const effectiveMap = isRscEnv ? hashToFileMap : undefined;
342
-
343
- // For RSC bundles, both createServerReference and registerServerReference
344
- // may need transforming. Use a single MagicString for correct sourcemaps.
345
- if (isRscEnv && hashToFileMap) {
346
- const s = new MagicString(code);
347
- const changed1 = applyServerReferenceWrapping(code, s, effectiveMap);
348
- const changed2 = applyRegisterReferenceWrapping(code, s, hashToFileMap);
349
- if (changed1 || changed2) {
350
- return {
351
- code: s.toString(),
352
- map: s.generateMap({
353
- source: chunk.fileName,
354
- includeContent: true,
355
- }),
356
- };
348
+ const start = counterRender ? performance.now() : 0;
349
+ try {
350
+ // Only RSC bundle should get file paths for revalidation matching
351
+ // SSR bundle must NOT use file paths because client components run there
352
+ // and need to match the client bundle during hydration (otherwise: error #418)
353
+ const isRscEnv = this.environment?.name === "rsc";
354
+
355
+ // Only use file path mapping for RSC environment
356
+ const effectiveMap = isRscEnv ? hashToFileMap : undefined;
357
+
358
+ // For RSC bundles, both createServerReference and registerServerReference
359
+ // may need transforming. Use a single MagicString for correct sourcemaps.
360
+ if (isRscEnv && hashToFileMap) {
361
+ const s = new MagicString(code);
362
+ const changed1 = applyServerReferenceWrapping(code, s, effectiveMap);
363
+ const changed2 = applyRegisterReferenceWrapping(
364
+ code,
365
+ s,
366
+ hashToFileMap,
367
+ );
368
+ if (changed1 || changed2) {
369
+ return {
370
+ code: s.toString(),
371
+ map: s.generateMap({
372
+ source: chunk.fileName,
373
+ includeContent: true,
374
+ }),
375
+ };
376
+ }
377
+ return null;
357
378
  }
358
- return null;
359
- }
360
379
 
361
- // Non-RSC environments: only transform createServerReference calls
362
- return transformServerReferences(code, chunk.fileName, effectiveMap);
380
+ // Non-RSC environments: only transform createServerReference calls
381
+ return transformServerReferences(code, chunk.fileName, effectiveMap);
382
+ } finally {
383
+ counterRender?.record(chunk.fileName, performance.now() - start);
384
+ }
363
385
  },
364
386
  };
365
387
  }
@@ -20,18 +20,34 @@ export function hashId(filePath: string, exportName: string): string {
20
20
  }
21
21
 
22
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).
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 handler call site.
36
+ *
37
+ * Keyed on the source-order INDEX of the call (the Nth inline `fnName(...)` in
38
+ * the file), NOT its line number. Line numbers shift between the prerender
39
+ * build context and the production build context (preceding transforms differ,
40
+ * e.g. plugin-react boilerplate), which would desync the prerender manifest key
41
+ * from the runtime handler id and break prerender/static freezing. The
42
+ * source-order index is invariant to line shifts; `fnName` keeps Static and
43
+ * Prerender inline ids from colliding at the same index.
25
44
  */
26
45
  export function hashInlineId(
27
46
  filePath: string,
28
- lineNumber: number,
29
- index?: number,
47
+ fnName: string,
48
+ index: number,
30
49
  ): string {
31
- const input =
32
- index !== undefined && index > 0
33
- ? `${filePath}:${lineNumber}:${index}`
34
- : `${filePath}:${lineNumber}`;
50
+ const input = `${filePath}:${fnName}:${index}`;
35
51
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, 8);
36
52
  }
37
53