@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -9,15 +9,15 @@
9
9
  import {
10
10
  requireRequestContext,
11
11
  setRequestContextParams,
12
- getLocationState,
13
12
  } from "../server/request-context.js";
14
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
15
13
  import { appendMetric } from "../router/metrics.js";
16
- import { getSSRSetup } from "./ssr-setup.js";
14
+ import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
17
15
  import type { RscPayload } from "./types.js";
16
+ import type { MatchResult } from "../types.js";
18
17
  import {
19
18
  createResponseWithMergedHeaders,
20
19
  createSimpleRedirectResponse,
20
+ attachLocationStateIfPresent,
21
21
  } from "./helpers.js";
22
22
  import type { HandlerContext } from "./handler-context.js";
23
23
 
@@ -35,6 +35,29 @@ export async function handleRscRendering<TEnv>(
35
35
  let payload: RscPayload;
36
36
  let hasInterceptSlots = false;
37
37
 
38
+ // Shared by the partial-fallback and full-render paths. The partial-success
39
+ // payload below is intentionally different (omits rootLayout/theme, adds slots).
40
+ const buildFullPayload = (m: MatchResult): RscPayload => ({
41
+ metadata: {
42
+ pathname: url.pathname,
43
+ routerId: ctx.router.id,
44
+ basename: ctx.router.basename,
45
+ segments: m.segments,
46
+ matched: m.matched,
47
+ diff: m.diff,
48
+ resolvedIds: m.resolvedIds,
49
+ params: m.params,
50
+ isPartial: false,
51
+ rootLayout: ctx.router.rootLayout,
52
+ handles: handleStore.stream(),
53
+ version: ctx.version,
54
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
55
+ stateCookieName: ctx.router.resolvedStateCookieName,
56
+ themeConfig: ctx.router.themeConfig,
57
+ initialTheme: reqCtx.theme,
58
+ },
59
+ });
60
+
38
61
  if (isPartial) {
39
62
  // Partial render (navigation)
40
63
  const result = await ctx.router.matchPartial(request, { env });
@@ -51,22 +74,7 @@ export async function handleRscRendering<TEnv>(
51
74
  return createSimpleRedirectResponse(match.redirect);
52
75
  }
53
76
 
54
- payload = {
55
- metadata: {
56
- pathname: url.pathname,
57
- segments: match.segments,
58
- matched: match.matched,
59
- diff: match.diff,
60
- params: match.params,
61
- isPartial: false,
62
- rootLayout: ctx.router.rootLayout,
63
- handles: handleStore.stream(),
64
- version: ctx.version,
65
- prefetchCacheTTL: ctx.router.prefetchCacheTTL,
66
- themeConfig: ctx.router.themeConfig,
67
- initialTheme: reqCtx.theme,
68
- },
69
- };
77
+ payload = buildFullPayload(match);
70
78
  } else {
71
79
  setRequestContextParams(result.params, result.routeName);
72
80
 
@@ -75,14 +83,23 @@ export async function handleRscRendering<TEnv>(
75
83
  payload = {
76
84
  metadata: {
77
85
  pathname: url.pathname,
86
+ // routerId is serialized on every payload (including within-session
87
+ // ones) so the frontend can read the current app/router identity. It
88
+ // always equals the current app's id: a cross-app navigation is
89
+ // intercepted server-side (X-RSC-Reload) and never delivers a
90
+ // different-router payload to the client.
91
+ routerId: ctx.router.id,
78
92
  segments: result.segments,
79
93
  matched: result.matched,
80
94
  diff: result.diff,
95
+ resolvedIds: result.resolvedIds,
81
96
  params: result.params,
82
97
  isPartial: true,
83
98
  slots: result.slots,
84
99
  handles: handleStore.stream(),
85
100
  version: ctx.version,
101
+ prefetchCacheTTL: ctx.router.prefetchCacheTTL,
102
+ stateCookieName: ctx.router.resolvedStateCookieName,
86
103
  },
87
104
  };
88
105
  }
@@ -129,24 +146,7 @@ export async function handleRscRendering<TEnv>(
129
146
  { headers: { "Content-Type": "application/json" } },
130
147
  );
131
148
  } else {
132
- payload = {
133
- // Initial SSR can reconstruct the tree from segments + rootLayout,
134
- // so we omit root to avoid sending the same structure twice.
135
-
136
- metadata: {
137
- pathname: url.pathname,
138
- segments: match.segments,
139
- matched: match.matched,
140
- diff: match.diff,
141
- params: match.params,
142
- isPartial: false,
143
- rootLayout: ctx.router.rootLayout,
144
- handles: handleStore.stream(),
145
- version: ctx.version,
146
- themeConfig: ctx.router.themeConfig,
147
- initialTheme: reqCtx.theme,
148
- },
149
- };
149
+ payload = buildFullPayload(match);
150
150
  }
151
151
  }
152
152
 
@@ -154,11 +154,7 @@ export async function handleRscRendering<TEnv>(
154
154
  // SSR (full page) requests ignore location state since there's no history.state
155
155
  // to write to on a fresh page load.
156
156
  if (isPartial && payload.metadata) {
157
- const locationState = getLocationState();
158
- if (locationState) {
159
- payload.metadata.locationState =
160
- resolveLocationStateEntries(locationState);
161
- }
157
+ attachLocationStateIfPresent(payload);
162
158
  }
163
159
 
164
160
  const metricsStore = reqCtx._metricsStore;
@@ -166,7 +162,11 @@ export async function handleRscRendering<TEnv>(
166
162
 
167
163
  // Serialize to RSC stream
168
164
  const rscSerializeStart = performance.now();
169
- const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
165
+ const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
166
+ onError: (error: unknown) => {
167
+ ctx.callOnError(error, "rendering", { request, url, env });
168
+ },
169
+ });
170
170
  const rscSerializeDur = performance.now() - rscSerializeStart;
171
171
  // This measures synchronous stream creation, not end-to-end stream consumption.
172
172
  appendMetric(
@@ -176,23 +176,25 @@ export async function handleRscRendering<TEnv>(
176
176
  rscSerializeDur,
177
177
  );
178
178
 
179
- // Determine if this is an RSC request or HTML request.
180
- // Partial requests (_rsc_partial) are always RSC -- they come from client-side
181
- // navigation or prefetch fetch(). We cannot rely on Accept alone since some
182
- // browsers may send Accept: text/html for non-HTML requests.
183
- const isRscRequest =
184
- isPartial ||
185
- (!request.headers.get("accept")?.includes("text/html") &&
186
- !url.searchParams.has("__html")) ||
187
- url.searchParams.has("__rsc");
188
-
189
- if (isRscRequest) {
179
+ if (isRscRequest(request, url, isPartial)) {
190
180
  const renderDur = performance.now() - renderStart;
191
181
  appendMetric(metricsStore, "render:total", renderStart, renderDur);
192
182
  const rscHeaders: Record<string, string> = {
193
183
  "content-type": "text/x-component;charset=utf-8",
194
184
  vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
185
+ // Router identity, so the client can verify pre-decode (before importing
186
+ // chunks) that this content payload belongs to its app and refuse a
187
+ // foreign one (cache/proxy/bug). Control-only reload/redirect responses
188
+ // are deliberately NOT stamped. See browser/response-adapter.ts.
189
+ "X-RSC-Router-Id": ctx.router.id,
195
190
  };
191
+ // Tell the client's prefetch cache to scope this response to its source
192
+ // URL (instead of the default source-agnostic wildcard). Intercept
193
+ // responses depend on the source page matching an intercept rule, so
194
+ // they must not be reused for navigations from other sources.
195
+ if (hasInterceptSlots) {
196
+ rscHeaders["x-rsc-prefetch-scope"] = "source";
197
+ }
196
198
  // Enable browser HTTP caching for prefetch responses only.
197
199
  // Requires X-Rango-Prefetch header (sent by Link prefetch fetch),
198
200
  // non-intercept context (intercept responses depend on source page),
@@ -8,6 +8,7 @@ import {
8
8
  createResponseWithMergedHeaders,
9
9
  carryOverRedirectHeaders,
10
10
  } from "./helpers.js";
11
+ import { isRedirectResponse } from "../response-utils.js";
11
12
 
12
13
  // W3 -----------------------------------------------------------------------
13
14
 
@@ -18,16 +19,14 @@ import {
18
19
  */
19
20
  export function extractRedirectResponse(value: unknown): Response | null {
20
21
  if (!(value instanceof Response)) return null;
21
- const location = value.headers.get("Location");
22
- if (value.status >= 300 && value.status < 400 && location) {
23
- const redirect = createResponseWithMergedHeaders(null, {
24
- status: value.status,
25
- headers: { Location: location },
26
- });
27
- carryOverRedirectHeaders(value, redirect);
28
- return redirect;
29
- }
30
- return null;
22
+ if (!isRedirectResponse(value)) return null;
23
+ const location = value.headers.get("Location")!;
24
+ const redirect = createResponseWithMergedHeaders(null, {
25
+ status: value.status,
26
+ headers: { Location: location },
27
+ });
28
+ carryOverRedirectHeaders(value, redirect);
29
+ return redirect;
31
30
  }
32
31
 
33
32
  /**
@@ -40,3 +39,17 @@ export function warnNonRedirectPeResponse(): void {
40
39
  `ignored — the page will re-render at the current URL instead.`,
41
40
  );
42
41
  }
42
+
43
+ /**
44
+ * Warn when a non-redirect Response is returned (not thrown) from an action
45
+ * on the JS (fetch) path. A raw Response cannot be serialized into Flight, so
46
+ * it is discarded — mirroring the PE path. Use `throw redirect('/path')` for
47
+ * redirects.
48
+ */
49
+ export function warnNonRedirectActionResponse(actionId: string): void {
50
+ console.warn(
51
+ `[@rangojs/router] Server action "${actionId}" returned a Response ` +
52
+ `that is not a redirect. Non-redirect Responses cannot be serialized ` +
53
+ `and are ignored. Use \`throw redirect('/path')\` for redirects.`,
54
+ );
55
+ }
@@ -18,31 +18,19 @@
18
18
  import {
19
19
  requireRequestContext,
20
20
  setRequestContextParams,
21
- getLocationState,
22
21
  } from "../server/request-context.js";
23
- import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
24
22
  import { appendMetric } from "../router/metrics.js";
25
23
  import type { RscPayload } from "./types.js";
26
24
  import {
27
25
  hasBodyContent,
28
26
  createResponseWithMergedHeaders,
29
27
  createSimpleRedirectResponse,
30
- carryOverRedirectHeaders,
28
+ interceptRedirectForPartial,
29
+ attachLocationStateIfPresent,
31
30
  } from "./helpers.js";
31
+ import { warnNonRedirectActionResponse } from "./runtime-warnings.js";
32
32
  import type { HandlerContext } from "./handler-context.js";
33
33
 
34
- /**
35
- * Attach location state set during the action to a payload's metadata.
36
- * No-op if no location state was set.
37
- */
38
- function attachLocationState(payload: RscPayload): void {
39
- const locationState = getLocationState();
40
- if (locationState) {
41
- payload.metadata!.locationState =
42
- resolveLocationStateEntries(locationState);
43
- }
44
- }
45
-
46
34
  /**
47
35
  * Data flowing from action execution to the revalidation phase.
48
36
  * When the action completes without redirect/error-boundary, the handler
@@ -97,7 +85,10 @@ export async function executeServerAction<TEnv>(
97
85
  args = await ctx.decodeReply(body, { temporaryReferences });
98
86
  }
99
87
  } catch (error) {
100
- throw new Error(`Failed to decode action arguments: ${error}`, {
88
+ // Keep the original error as `cause` for server-side logging, but do not
89
+ // interpolate it into the message: that string can surface to the client
90
+ // and may leak decode internals.
91
+ throw new Error("Failed to decode action arguments", {
101
92
  cause: error,
102
93
  });
103
94
  }
@@ -109,51 +100,35 @@ export async function executeServerAction<TEnv>(
109
100
 
110
101
  try {
111
102
  loadedAction = await ctx.loadServerAction(actionId);
112
- const data = await loadedAction!.apply(null, args);
103
+ let data = await loadedAction!.apply(null, args);
113
104
 
114
- // Intercept redirect responses from actions. Without this, the redirect
115
- // Response would be serialized as the action returnValue (which fails)
116
- // and the revalidation step would run unnecessarily.
105
+ // Intercept redirect Responses: serializing one as the action returnValue
106
+ // would fail, and revalidation would run needlessly.
117
107
  if (data instanceof Response) {
118
- const redirectUrl = data.headers.get("Location");
119
- const isRedirect = data.status >= 300 && data.status < 400 && redirectUrl;
120
- if (isRedirect) {
121
- const locationState = getLocationState();
122
- let redirect: Response;
123
- if (locationState) {
124
- redirect = ctx.createRedirectFlightResponse(
125
- redirectUrl,
126
- resolveLocationStateEntries(locationState),
127
- );
128
- } else {
129
- redirect = createSimpleRedirectResponse(redirectUrl);
130
- }
131
- carryOverRedirectHeaders(data, redirect);
132
- return redirect;
108
+ const intercepted = interceptRedirectForPartial(
109
+ data,
110
+ ctx.createRedirectFlightResponse,
111
+ );
112
+ if (intercepted) return intercepted;
113
+
114
+ // Non-redirect Response returned (not thrown): a raw Response cannot be
115
+ // serialized into Flight. Discard it and re-render — mirroring the PE
116
+ // path (progressive-enhancement.ts) so JS and no-JS behave identically.
117
+ if (process.env.NODE_ENV !== "production") {
118
+ warnNonRedirectActionResponse(actionId);
133
119
  }
120
+ data = undefined;
134
121
  }
135
122
 
136
123
  returnValue = { ok: true, data };
137
124
  } catch (error) {
138
125
  // Handle thrown redirect (e.g., throw redirect('/path'))
139
126
  if (error instanceof Response) {
140
- const redirectUrl = error.headers.get("Location");
141
- const isRedirect =
142
- error.status >= 300 && error.status < 400 && redirectUrl;
143
- if (isRedirect) {
144
- const locationState = getLocationState();
145
- let redirect: Response;
146
- if (locationState) {
147
- redirect = ctx.createRedirectFlightResponse(
148
- redirectUrl,
149
- resolveLocationStateEntries(locationState),
150
- );
151
- } else {
152
- redirect = createSimpleRedirectResponse(redirectUrl);
153
- }
154
- carryOverRedirectHeaders(error, redirect);
155
- return redirect;
156
- }
127
+ const intercepted = interceptRedirectForPartial(
128
+ error,
129
+ ctx.createRedirectFlightResponse,
130
+ );
131
+ if (intercepted) return intercepted;
157
132
 
158
133
  // Non-redirect Response thrown from action — this will be treated
159
134
  // as a regular error and routed to the error boundary. Warn in dev
@@ -208,10 +183,15 @@ export async function executeServerAction<TEnv>(
208
183
  const payload: RscPayload = {
209
184
  metadata: {
210
185
  pathname: url.pathname,
186
+ // routerId exposed for the frontend (current app identity); see
187
+ // rsc-rendering.ts partial branch.
188
+ routerId: ctx.router.id,
211
189
  segments: errorResult.segments,
212
190
  isPartial: true,
213
191
  matched: errorResult.matched,
214
192
  diff: errorResult.diff,
193
+ resolvedIds: errorResult.resolvedIds,
194
+ params: errorResult.params,
215
195
  isError: true,
216
196
  handles: handleStore.stream(),
217
197
  version: ctx.version,
@@ -225,28 +205,39 @@ export async function executeServerAction<TEnv>(
225
205
 
226
206
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
227
207
  temporaryReferences,
208
+ onError: (error: unknown) => {
209
+ ctx.callOnError(error, "rendering", { request, url, env });
210
+ },
228
211
  });
229
212
 
230
213
  return createResponseWithMergedHeaders(rscStream, {
231
214
  status: actionStatus,
232
- headers: { "content-type": "text/x-component;charset=utf-8" },
215
+ headers: {
216
+ "content-type": "text/x-component;charset=utf-8",
217
+ // Router identity for the client's pre-decode integrity check (the
218
+ // action apply path has no post-decode guard). See response-adapter.
219
+ "X-RSC-Router-Id": ctx.router.id,
220
+ },
233
221
  });
234
222
  }
235
223
  }
236
224
 
237
225
  // Build continuation for the revalidation phase
238
- const resolvedActionId =
239
- (loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
240
- (loadedAction as { $$id?: string } | undefined)?.$$id ??
241
- actionId;
226
+ const actionMeta = loadedAction as
227
+ | { $id?: string; $$id?: string }
228
+ | undefined;
229
+ const resolvedActionId = actionMeta?.$id ?? actionMeta?.$$id ?? actionId;
242
230
 
243
231
  return {
244
232
  returnValue,
245
233
  actionStatus,
246
234
  temporaryReferences,
247
235
  actionContext: {
236
+ // Defensive copy of the already-parsed url (avoids re-parsing
237
+ // request.url). actionUrl is persisted into the continuation and later
238
+ // flows into matchPartial, so it must not alias the handler's live url.
248
239
  actionId: resolvedActionId,
249
- actionUrl: new URL(request.url),
240
+ actionUrl: new URL(url),
250
241
  actionResult: returnValue.data,
251
242
  formData: actionFormData,
252
243
  },
@@ -285,8 +276,8 @@ export async function revalidateAfterAction<TEnv>(
285
276
  );
286
277
 
287
278
  if (!matchResult) {
288
- // matchPartial returns null when the route is a redirect or the request
289
- // is missing required headers (previousUrl). Check for redirect first.
279
+ // matchPartial returns null when the route is a redirect or no previous-URL
280
+ // context could be resolved. Check for redirect first.
290
281
  const fullMatch = await ctx.router.match(request, { env });
291
282
  setRequestContextParams(fullMatch.params, fullMatch.routeName);
292
283
 
@@ -297,14 +288,17 @@ export async function revalidateAfterAction<TEnv>(
297
288
  return createSimpleRedirectResponse(fullMatch.redirect);
298
289
  }
299
290
 
300
- // Non-redirect: this branch is only reachable when the action request
301
- // is missing the X-RSC-Router-Client-Path header (defensive). The
302
- // client requires isPartial for action responses, so producing a full
303
- // payload here would be rejected. Return 500 instead.
291
+ // Non-redirect: this branch is only reachable when no previous URL could
292
+ // be resolved (neither X-RSC-Router-Client-Path nor a usable Referer), or
293
+ // the previous URL was unparseable (defensive). The client requires
294
+ // isPartial for action responses, so producing a full payload here would
295
+ // be rejected. Return 500 instead.
304
296
  throw new Error(
305
297
  `[RSC] matchPartial returned null for a non-redirect route ` +
306
298
  `during action revalidation (${url.pathname}). This indicates ` +
307
- `a malformed action request (missing X-RSC-Router-Client-Path header).`,
299
+ `a malformed action request: no previous-URL context could be ` +
300
+ `resolved (neither X-RSC-Router-Client-Path nor a usable Referer), ` +
301
+ `or the previous URL was unparseable.`,
308
302
  );
309
303
  }
310
304
 
@@ -314,10 +308,15 @@ export async function revalidateAfterAction<TEnv>(
314
308
  const payload: RscPayload = {
315
309
  metadata: {
316
310
  pathname: url.pathname,
311
+ // routerId exposed for the frontend (current app identity); see
312
+ // rsc-rendering.ts partial branch.
313
+ routerId: ctx.router.id,
317
314
  segments: matchResult.segments,
318
315
  isPartial: true,
319
316
  matched: matchResult.matched,
320
317
  diff: matchResult.diff,
318
+ resolvedIds: matchResult.resolvedIds,
319
+ params: matchResult.params,
321
320
  slots: matchResult.slots,
322
321
  handles: handleStore.stream(),
323
322
  version: ctx.version,
@@ -325,11 +324,14 @@ export async function revalidateAfterAction<TEnv>(
325
324
  returnValue,
326
325
  };
327
326
 
328
- attachLocationState(payload);
327
+ attachLocationStateIfPresent(payload);
329
328
 
330
329
  const renderStart = performance.now();
331
330
  const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
332
331
  temporaryReferences,
332
+ onError: (error: unknown) => {
333
+ ctx.callOnError(error, "rendering", { request, url, env });
334
+ },
333
335
  });
334
336
  const rscSerializeDur = performance.now() - renderStart;
335
337
  // This measures synchronous stream creation, not end-to-end stream consumption.
@@ -343,6 +345,11 @@ export async function revalidateAfterAction<TEnv>(
343
345
 
344
346
  return createResponseWithMergedHeaders(rscStream, {
345
347
  status: actionStatus,
346
- headers: { "content-type": "text/x-component;charset=utf-8" },
348
+ headers: {
349
+ "content-type": "text/x-component;charset=utf-8",
350
+ // Router identity for the client's pre-decode integrity check (the action
351
+ // apply path has no post-decode guard). See response-adapter.
352
+ "X-RSC-Router-Id": ctx.router.id,
353
+ },
347
354
  });
348
355
  }
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
77
77
  url: URL,
78
78
  metricsStore: MetricsStore | undefined,
79
79
  ): Promise<SSRSetup> {
80
- const early = _getRequestContext()?.var?.[SSR_SETUP_VAR] as
80
+ const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
81
81
  | Promise<SSRSetup>
82
82
  | undefined;
83
83
  if (early) return early;
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
98
98
  * the isRscRequest decision in rsc-rendering.ts.
99
99
  *
100
100
  * Note: response/mime routes are excluded by the caller — this function
101
- * runs after previewMatch() classifies the route type.
101
+ * runs after classifyRequest() determines the request mode.
102
102
  */
103
103
  export function mayNeedSSR(request: Request, url: URL): boolean {
104
104
  if (
@@ -126,3 +126,19 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
126
126
 
127
127
  return true;
128
128
  }
129
+
130
+ // Final render-time decision: is the response an RSC stream (vs HTML)? Distinct
131
+ // from mayNeedSSR, which is a conservative pre-classifier (it treats a missing
132
+ // Accept header as needing SSR; this treats it as RSC).
133
+ export function isRscRequest(
134
+ request: Request,
135
+ url: URL,
136
+ isPartial: boolean,
137
+ ): boolean {
138
+ return (
139
+ isPartial ||
140
+ (!request.headers.get("accept")?.includes("text/html") &&
141
+ !url.searchParams.has("__html")) ||
142
+ url.searchParams.has("__rsc")
143
+ );
144
+ }
package/src/rsc/types.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ResolvedSegment, SlotState } from "../types.js";
9
9
  import type { HandleData } from "../server/handle-store.js";
10
- import type { RSCRouterInternal } from "../router/router-interfaces.js";
10
+ import type { RangoInternal } from "../router/router-interfaces.js";
11
11
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
12
12
 
13
13
  /**
@@ -19,10 +19,19 @@ export interface RscPayload {
19
19
  metadata?: {
20
20
  pathname: string;
21
21
  segments: ResolvedSegment[];
22
+ /** Router instance ID. When this changes between navigations, the client
23
+ * discards cached segments and does a full tree replacement (app switch). */
24
+ routerId?: string;
22
25
  isPartial?: boolean;
23
26
  isError?: boolean;
24
27
  matched?: string[];
25
28
  diff?: string[];
29
+ /**
30
+ * All segment ids re-resolved on the server, including null-component
31
+ * ones excluded from `segments`/`diff`. Drives client-side handle-bucket
32
+ * cleanup. Superset of `diff`. See MatchResult.resolvedIds.
33
+ */
34
+ resolvedIds?: string[];
26
35
  /** Merged route params from the matched route */
27
36
  params?: Record<string, string>;
28
37
  slots?: Record<string, SlotState>;
@@ -34,19 +43,26 @@ export interface RscPayload {
34
43
  version?: string;
35
44
  /** TTL in milliseconds for the client-side in-memory prefetch cache */
36
45
  prefetchCacheTTL?: number;
46
+ /** Server-resolved rango state cookie name; the client reads it verbatim. */
47
+ stateCookieName?: string;
37
48
  /** Theme configuration for FOUC prevention */
38
49
  themeConfig?: ResolvedThemeConfig | null;
39
50
  /** Initial theme from cookie (for SSR hydration) */
40
51
  initialTheme?: Theme;
52
+ /** URL prefix for all routes (from createRouter({ basename })). */
53
+ basename?: string;
41
54
  /** Whether connection warmup is enabled */
42
55
  warmupEnabled?: boolean;
43
- /** Server-side redirect with optional state (for partial requests) */
44
- redirect?: { url: string };
56
+ /**
57
+ * Server-side redirect with optional state (for partial requests).
58
+ * `external: true` (from redirect(url, { external: true })) tells the client
59
+ * to hard-navigate to an off-host target instead of validating same-origin.
60
+ */
61
+ redirect?: { url: string; external?: boolean };
45
62
  /** Server-set location state to include in history.pushState */
46
63
  locationState?: Record<string, unknown>;
47
64
  };
48
65
  returnValue?: { ok: boolean; data: unknown };
49
- formState?: unknown;
50
66
  }
51
67
 
52
68
  /**
@@ -63,7 +79,10 @@ export interface RSCDependencies {
63
79
  */
64
80
  renderToReadableStream: <T>(
65
81
  payload: T,
66
- options?: { temporaryReferences?: unknown },
82
+ options?: {
83
+ temporaryReferences?: unknown;
84
+ onError?: (error: unknown) => string | void;
85
+ },
67
86
  ) => ReadableStream<Uint8Array>;
68
87
 
69
88
  /**
@@ -171,7 +190,7 @@ export interface CreateRSCHandlerOptions<
171
190
  /**
172
191
  * The RSC router instance
173
192
  */
174
- router: RSCRouterInternal<TEnv, TRoutes>;
193
+ router: RangoInternal<TEnv, TRoutes>;
175
194
 
176
195
  /**
177
196
  * RSC dependencies from @vitejs/plugin-rsc/rsc.
@@ -0,0 +1,18 @@
1
+ // Runtime-safe detection of a test runner (Vitest), used to decide whether a
2
+ // create*() call with no plugin-injected $$id may fall back to a synthetic id (a
3
+ // bare test) or must fail loud (dev / a real build).
4
+ //
5
+ // `process` is absent in some target runtimes (the browser, certain edge/worker
6
+ // RSC environments), so probe it through `globalThis` with optional chaining —
7
+ // NEVER a bare `process.env.VITEST`, which would ReferenceError before the
8
+ // intended error is thrown. Unlike `process.env.NODE_ENV` (folded by the app's
9
+ // build `define`), `VITEST` is not folded, so this stays a small runtime check;
10
+ // it lives only on the create*() error path (id missing), which never runs in a
11
+ // correct production build.
12
+ //
13
+ // Vitest sets `VITEST` in every test process — the node project and the
14
+ // react-server forks alike (the RSC project forces NODE_ENV=production, so NODE_ENV
15
+ // cannot distinguish it from a real build; `VITEST` can). A real build never sets it.
16
+ export function isUnderTestRunner(): boolean {
17
+ return !!globalThis.process?.env?.VITEST;
18
+ }