@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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 (440) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +301 -797
  3. package/dist/bin/rango.js +603 -145
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3750 -1160
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +96 -24
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +85 -6
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +228 -33
  12. package/skills/caching/SKILL.md +336 -19
  13. package/skills/catalog.json +271 -0
  14. package/skills/comparison/SKILL.md +50 -0
  15. package/skills/comparison/agents/openai.yaml +4 -0
  16. package/skills/comparison/references/framework-comparison.md +837 -0
  17. package/skills/composability/SKILL.md +110 -4
  18. package/skills/css/SKILL.md +76 -0
  19. package/skills/debug-manifest/SKILL.md +5 -3
  20. package/skills/defer-hydration/SKILL.md +235 -0
  21. package/skills/document-cache/SKILL.md +87 -56
  22. package/skills/fonts/SKILL.md +1 -1
  23. package/skills/handler-use/SKILL.md +12 -10
  24. package/skills/hooks/SKILL.md +73 -691
  25. package/skills/hooks/data.md +273 -0
  26. package/skills/hooks/handle-and-actions.md +103 -0
  27. package/skills/hooks/navigation.md +110 -0
  28. package/skills/hooks/outlets.md +41 -0
  29. package/skills/hooks/state.md +228 -0
  30. package/skills/hooks/urls.md +135 -0
  31. package/skills/host-router/SKILL.md +129 -27
  32. package/skills/i18n/SKILL.md +276 -0
  33. package/skills/intercept/SKILL.md +75 -19
  34. package/skills/layout/SKILL.md +40 -19
  35. package/skills/links/SKILL.md +247 -17
  36. package/skills/loader/SKILL.md +248 -10
  37. package/skills/middleware/SKILL.md +25 -13
  38. package/skills/migrate-nextjs/SKILL.md +205 -20
  39. package/skills/migrate-react-router/SKILL.md +59 -670
  40. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  41. package/skills/migrate-react-router/component-migration.md +196 -0
  42. package/skills/migrate-react-router/data-and-actions.md +225 -0
  43. package/skills/migrate-react-router/route-mapping.md +271 -0
  44. package/skills/mime-routes/SKILL.md +29 -2
  45. package/skills/observability/SKILL.md +202 -0
  46. package/skills/parallel/SKILL.md +40 -10
  47. package/skills/ppr/SKILL.md +616 -0
  48. package/skills/prerender/SKILL.md +72 -60
  49. package/skills/rango/SKILL.md +318 -26
  50. package/skills/react-compiler/SKILL.md +168 -0
  51. package/skills/response-routes/SKILL.md +138 -49
  52. package/skills/route/SKILL.md +117 -9
  53. package/skills/router-setup/SKILL.md +44 -9
  54. package/skills/scripts/SKILL.md +179 -0
  55. package/skills/server-actions/SKILL.md +776 -0
  56. package/skills/shell-manifest/SKILL.md +185 -0
  57. package/skills/streams-and-websockets/SKILL.md +283 -0
  58. package/skills/tailwind/SKILL.md +28 -4
  59. package/skills/testing/SKILL.md +130 -0
  60. package/skills/testing/bindings.md +103 -0
  61. package/skills/testing/cache-prerender.md +127 -0
  62. package/skills/testing/client-components.md +124 -0
  63. package/skills/testing/e2e-parity.md +125 -0
  64. package/skills/testing/flight.md +91 -0
  65. package/skills/testing/handles.md +131 -0
  66. package/skills/testing/loader.md +128 -0
  67. package/skills/testing/middleware.md +99 -0
  68. package/skills/testing/render-handler.md +122 -0
  69. package/skills/testing/response-routes.md +95 -0
  70. package/skills/testing/reverse-and-types.md +85 -0
  71. package/skills/testing/server-actions.md +107 -0
  72. package/skills/testing/server-tree.md +128 -0
  73. package/skills/testing/setup.md +123 -0
  74. package/skills/theme/SKILL.md +1 -1
  75. package/skills/typesafety/SKILL.md +45 -626
  76. package/skills/typesafety/env-and-bindings.md +254 -0
  77. package/skills/typesafety/generated-files-and-cli.md +335 -0
  78. package/skills/typesafety/params-and-search.md +153 -0
  79. package/skills/typesafety/route-types.md +209 -0
  80. package/skills/use-cache/SKILL.md +74 -15
  81. package/skills/vercel/SKILL.md +128 -0
  82. package/skills/view-transitions/SKILL.md +337 -0
  83. package/src/__augment-tests__/augment.ts +81 -0
  84. package/src/__augment-tests__/augmented.check.ts +116 -0
  85. package/src/__internal.ts +0 -65
  86. package/src/browser/action-coordinator.ts +53 -36
  87. package/src/browser/action-fence.ts +47 -0
  88. package/src/browser/app-shell.ts +39 -0
  89. package/src/browser/connection-warmup.ts +134 -0
  90. package/src/browser/cookie-name.ts +140 -0
  91. package/src/browser/event-controller.ts +252 -158
  92. package/src/browser/history-state.ts +21 -0
  93. package/src/browser/index.ts +3 -3
  94. package/src/browser/invalidate-client-cache.ts +52 -0
  95. package/src/browser/logging.ts +28 -0
  96. package/src/browser/merge-segment-loaders.ts +6 -4
  97. package/src/browser/navigation-bridge.ts +94 -25
  98. package/src/browser/navigation-client.ts +144 -79
  99. package/src/browser/navigation-store-handle.ts +38 -0
  100. package/src/browser/navigation-store.ts +161 -73
  101. package/src/browser/navigation-transaction.ts +9 -59
  102. package/src/browser/network-error-handler.ts +34 -7
  103. package/src/browser/partial-update.ts +183 -144
  104. package/src/browser/prefetch/cache.ts +242 -77
  105. package/src/browser/prefetch/fetch.ts +325 -69
  106. package/src/browser/prefetch/queue.ts +61 -12
  107. package/src/browser/rango-state.ts +158 -76
  108. package/src/browser/react/Link.tsx +58 -20
  109. package/src/browser/react/NavigationProvider.tsx +202 -120
  110. package/src/browser/react/ScrollRestoration.tsx +10 -6
  111. package/src/browser/react/filter-segment-order.ts +66 -7
  112. package/src/browser/react/index.ts +0 -48
  113. package/src/browser/react/location-state-shared.ts +178 -8
  114. package/src/browser/react/location-state.ts +39 -14
  115. package/src/browser/react/use-action.ts +6 -15
  116. package/src/browser/react/use-handle.ts +17 -14
  117. package/src/browser/react/use-href.tsx +8 -1
  118. package/src/browser/react/use-link-status.ts +33 -8
  119. package/src/browser/react/use-navigation.ts +32 -7
  120. package/src/browser/react/use-params.ts +20 -10
  121. package/src/browser/react/use-reverse.ts +106 -0
  122. package/src/browser/react/use-router.ts +25 -3
  123. package/src/browser/react/use-search-params.ts +0 -5
  124. package/src/browser/react/use-segments.ts +11 -21
  125. package/src/browser/response-adapter.ts +99 -8
  126. package/src/browser/rsc-router.tsx +145 -28
  127. package/src/browser/scroll-restoration.ts +37 -22
  128. package/src/browser/segment-reconciler.ts +31 -21
  129. package/src/browser/segment-structure-assert.ts +2 -2
  130. package/src/browser/server-action-bridge.ts +236 -65
  131. package/src/browser/types.ts +102 -9
  132. package/src/browser/validate-redirect-origin.ts +43 -16
  133. package/src/build/collect-fallback-refs.ts +107 -0
  134. package/src/build/generate-manifest.ts +203 -154
  135. package/src/build/generate-route-types.ts +3 -1
  136. package/src/build/index.ts +11 -3
  137. package/src/build/prefix-tree-utils.ts +123 -0
  138. package/src/build/route-trie.ts +152 -21
  139. package/src/build/route-types/ast-route-extraction.ts +15 -8
  140. package/src/build/route-types/codegen.ts +16 -5
  141. package/src/build/route-types/include-resolution.ts +456 -62
  142. package/src/build/route-types/param-extraction.ts +6 -3
  143. package/src/build/route-types/per-module-writer.ts +22 -6
  144. package/src/build/route-types/router-processing.ts +128 -51
  145. package/src/build/route-types/scan-filter.ts +1 -1
  146. package/src/build/route-types/source-scan.ts +216 -0
  147. package/src/build/runtime-discovery.ts +13 -21
  148. package/src/cache/cache-error.ts +104 -0
  149. package/src/cache/cache-key-utils.ts +58 -13
  150. package/src/cache/cache-policy.ts +108 -34
  151. package/src/cache/cache-runtime.ts +421 -58
  152. package/src/cache/cache-scope.ts +187 -96
  153. package/src/cache/cache-tag.ts +149 -0
  154. package/src/cache/cf/cf-base64.ts +33 -0
  155. package/src/cache/cf/cf-cache-constants.ts +127 -0
  156. package/src/cache/cf/cf-cache-store.ts +2202 -372
  157. package/src/cache/cf/cf-cache-types.ts +349 -0
  158. package/src/cache/cf/cf-kv-utils.ts +46 -0
  159. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  160. package/src/cache/cf/index.ts +6 -16
  161. package/src/cache/document-cache.ts +126 -41
  162. package/src/cache/handle-snapshot.ts +70 -0
  163. package/src/cache/index.ts +23 -20
  164. package/src/cache/memory-segment-store.ts +243 -37
  165. package/src/cache/profile-registry.ts +46 -31
  166. package/src/cache/read-through-swr.ts +56 -12
  167. package/src/cache/segment-codec.ts +13 -21
  168. package/src/cache/shell-snapshot.ts +417 -0
  169. package/src/cache/tag-invalidation.ts +230 -0
  170. package/src/cache/types.ts +180 -99
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1127 -0
  173. package/src/client.rsc.tsx +41 -21
  174. package/src/client.tsx +33 -61
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +108 -0
  177. package/src/component-utils.ts +19 -0
  178. package/src/components/DefaultDocument.tsx +8 -2
  179. package/src/context-var.ts +18 -6
  180. package/src/decode-loader-results.ts +52 -0
  181. package/src/defer.ts +185 -0
  182. package/src/deps/ssr.ts +0 -1
  183. package/src/encode-kv.ts +49 -0
  184. package/src/errors.ts +30 -4
  185. package/src/escape-script.ts +52 -0
  186. package/src/handle.ts +67 -37
  187. package/src/handles/MetaTags.tsx +24 -53
  188. package/src/handles/Scripts.tsx +183 -0
  189. package/src/handles/breadcrumbs.ts +35 -8
  190. package/src/handles/deferred-resolution.ts +127 -0
  191. package/src/handles/is-thenable.ts +18 -0
  192. package/src/handles/meta.ts +14 -40
  193. package/src/handles/script.ts +244 -0
  194. package/src/host/cookie-handler.ts +9 -60
  195. package/src/host/errors.ts +13 -22
  196. package/src/host/index.ts +9 -2
  197. package/src/host/pattern-matcher.ts +23 -52
  198. package/src/host/router.ts +107 -99
  199. package/src/host/testing.ts +40 -27
  200. package/src/host/types.ts +37 -4
  201. package/src/host/utils.ts +1 -1
  202. package/src/href-client.ts +137 -22
  203. package/src/index.rsc.ts +97 -12
  204. package/src/index.ts +98 -14
  205. package/src/internal-debug.ts +11 -10
  206. package/src/loader-store.ts +500 -0
  207. package/src/loader.rsc.ts +20 -13
  208. package/src/loader.ts +12 -11
  209. package/src/missing-id-error.ts +68 -0
  210. package/src/outlet-context.ts +1 -1
  211. package/src/outlet-provider.tsx +1 -5
  212. package/src/prerender/param-hash.ts +16 -16
  213. package/src/prerender/store.ts +32 -37
  214. package/src/prerender.ts +78 -10
  215. package/src/redirect-origin.ts +114 -0
  216. package/src/regex-escape.ts +8 -0
  217. package/src/render-error-thrower.tsx +20 -0
  218. package/src/response-utils.ts +62 -0
  219. package/src/reverse.ts +65 -39
  220. package/src/root-error-boundary.tsx +1 -19
  221. package/src/route-content-wrapper.tsx +19 -77
  222. package/src/route-definition/dsl-helpers.ts +304 -309
  223. package/src/route-definition/helper-factories.ts +28 -140
  224. package/src/route-definition/helpers-types.ts +87 -59
  225. package/src/route-definition/index.ts +1 -2
  226. package/src/route-definition/redirect.ts +44 -11
  227. package/src/route-definition/resolve-handler-use.ts +12 -1
  228. package/src/route-definition/use-item-types.ts +29 -0
  229. package/src/route-map-builder.ts +41 -20
  230. package/src/route-types.ts +19 -46
  231. package/src/router/basename.ts +14 -0
  232. package/src/router/content-negotiation.ts +73 -25
  233. package/src/router/error-handling.ts +45 -18
  234. package/src/router/find-match.ts +129 -30
  235. package/src/router/handler-context.ts +27 -42
  236. package/src/router/instrument.ts +355 -0
  237. package/src/router/intercept-resolution.ts +39 -20
  238. package/src/router/lazy-includes.ts +82 -59
  239. package/src/router/loader-resolution.ts +167 -72
  240. package/src/router/logging.ts +0 -6
  241. package/src/router/manifest.ts +74 -40
  242. package/src/router/match-api.ts +80 -55
  243. package/src/router/match-context.ts +0 -22
  244. package/src/router/match-handlers.ts +211 -165
  245. package/src/router/match-middleware/background-revalidation.ts +40 -24
  246. package/src/router/match-middleware/cache-lookup.ts +159 -285
  247. package/src/router/match-middleware/cache-store.ts +64 -52
  248. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  249. package/src/router/match-middleware/segment-resolution.ts +0 -22
  250. package/src/router/match-pipelines.ts +1 -42
  251. package/src/router/match-result.ts +69 -79
  252. package/src/router/metrics.ts +0 -34
  253. package/src/router/middleware-types.ts +7 -134
  254. package/src/router/middleware.ts +298 -172
  255. package/src/router/navigation-snapshot.ts +7 -56
  256. package/src/router/params-util.ts +23 -0
  257. package/src/router/parse-pattern.ts +115 -0
  258. package/src/router/pattern-matching.ts +181 -150
  259. package/src/router/prefetch-cache-ttl.ts +51 -0
  260. package/src/router/prefetch-limits.ts +37 -0
  261. package/src/router/prerender-match.ts +112 -67
  262. package/src/router/preview-match.ts +6 -2
  263. package/src/router/request-classification.ts +50 -69
  264. package/src/router/revalidation.ts +123 -73
  265. package/src/router/route-snapshot.ts +14 -3
  266. package/src/router/router-context.ts +6 -29
  267. package/src/router/router-interfaces.ts +115 -36
  268. package/src/router/router-options.ts +166 -5
  269. package/src/router/router-registry.ts +2 -5
  270. package/src/router/segment-resolution/fresh.ts +131 -86
  271. package/src/router/segment-resolution/helpers.ts +86 -6
  272. package/src/router/segment-resolution/loader-cache.ts +139 -39
  273. package/src/router/segment-resolution/loader-mask.ts +67 -0
  274. package/src/router/segment-resolution/loader-snapshot.ts +251 -0
  275. package/src/router/segment-resolution/revalidation.ts +272 -320
  276. package/src/router/segment-resolution/static-store.ts +19 -5
  277. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  278. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  279. package/src/router/segment-resolution.ts +5 -1
  280. package/src/router/segment-wrappers.ts +6 -5
  281. package/src/router/state-cookie-name.ts +33 -0
  282. package/src/router/substitute-pattern-params.ts +75 -0
  283. package/src/router/telemetry-otel.ts +160 -200
  284. package/src/router/telemetry.ts +105 -20
  285. package/src/router/timeout.ts +0 -20
  286. package/src/router/tracing.ts +215 -0
  287. package/src/router/trie-matching.ts +171 -59
  288. package/src/router/types.ts +9 -63
  289. package/src/router/url-params.ts +57 -0
  290. package/src/router.ts +157 -71
  291. package/src/rsc/full-payload.ts +70 -0
  292. package/src/rsc/handler-context.ts +3 -2
  293. package/src/rsc/handler.ts +291 -217
  294. package/src/rsc/helpers.ts +168 -46
  295. package/src/rsc/index.ts +2 -5
  296. package/src/rsc/json-route-result.ts +38 -0
  297. package/src/rsc/loader-fetch.ts +114 -38
  298. package/src/rsc/manifest-init.ts +29 -42
  299. package/src/rsc/nonce.ts +10 -1
  300. package/src/rsc/origin-guard.ts +39 -25
  301. package/src/rsc/progressive-enhancement.ts +124 -13
  302. package/src/rsc/redirect-guard.ts +100 -0
  303. package/src/rsc/response-cache-serve.ts +238 -0
  304. package/src/rsc/response-error.ts +79 -12
  305. package/src/rsc/response-route-handler.ts +99 -189
  306. package/src/rsc/rsc-rendering.ts +421 -76
  307. package/src/rsc/runtime-warnings.ts +23 -10
  308. package/src/rsc/server-action.ts +282 -116
  309. package/src/rsc/shell-capture.ts +1158 -0
  310. package/src/rsc/shell-serve.ts +150 -0
  311. package/src/rsc/ssr-setup.ts +16 -0
  312. package/src/rsc/transition-gate.ts +89 -0
  313. package/src/rsc/types.ts +53 -5
  314. package/src/runtime-env.ts +18 -0
  315. package/src/search-params.ts +35 -30
  316. package/src/segment-loader-promise.ts +49 -4
  317. package/src/segment-system.tsx +350 -149
  318. package/src/serialize.ts +243 -0
  319. package/src/server/context.ts +208 -51
  320. package/src/server/cookie-parse.ts +32 -0
  321. package/src/server/cookie-store.ts +152 -5
  322. package/src/server/handle-store.ts +21 -38
  323. package/src/server/loader-registry.ts +33 -42
  324. package/src/server/request-context.ts +395 -176
  325. package/src/ssr/index.tsx +458 -178
  326. package/src/ssr/ssr-root.tsx +228 -0
  327. package/src/static-handler.ts +10 -13
  328. package/src/testing/cache-status.ts +162 -0
  329. package/src/testing/collect-handle.ts +46 -0
  330. package/src/testing/dispatch.ts +813 -0
  331. package/src/testing/dom.entry.ts +22 -0
  332. package/src/testing/e2e/fixture.ts +188 -0
  333. package/src/testing/e2e/index.ts +128 -0
  334. package/src/testing/e2e/matchers.ts +35 -0
  335. package/src/testing/e2e/page-helpers.ts +272 -0
  336. package/src/testing/e2e/parity.ts +387 -0
  337. package/src/testing/e2e/server.ts +195 -0
  338. package/src/testing/flight-matchers.ts +97 -0
  339. package/src/testing/flight-normalize.ts +11 -0
  340. package/src/testing/flight-runtime.d.ts +57 -0
  341. package/src/testing/flight-tree.ts +682 -0
  342. package/src/testing/flight.entry.ts +52 -0
  343. package/src/testing/flight.ts +257 -0
  344. package/src/testing/generated-routes.ts +199 -0
  345. package/src/testing/index.ts +105 -0
  346. package/src/testing/internal/context.ts +371 -0
  347. package/src/testing/internal/flight-client-globals.ts +30 -0
  348. package/src/testing/internal/seed-vars.ts +54 -0
  349. package/src/testing/render-handler.ts +357 -0
  350. package/src/testing/render-route.tsx +584 -0
  351. package/src/testing/run-loader.ts +385 -0
  352. package/src/testing/run-middleware.ts +205 -0
  353. package/src/testing/run-transition-when.ts +164 -0
  354. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  355. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  356. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  357. package/src/testing/vitest-stubs/version.ts +5 -0
  358. package/src/testing/vitest.ts +305 -0
  359. package/src/theme/ThemeProvider.tsx +56 -84
  360. package/src/theme/ThemeScript.tsx +7 -9
  361. package/src/theme/constants.ts +52 -13
  362. package/src/theme/index.ts +0 -7
  363. package/src/theme/theme-context.ts +1 -5
  364. package/src/theme/theme-script.ts +22 -21
  365. package/src/theme/use-theme.ts +0 -3
  366. package/src/types/boundaries.ts +0 -35
  367. package/src/types/cache-types.ts +13 -4
  368. package/src/types/error-types.ts +30 -90
  369. package/src/types/global-namespace.ts +54 -41
  370. package/src/types/handler-context.ts +110 -62
  371. package/src/types/index.ts +3 -10
  372. package/src/types/loader-types.ts +11 -9
  373. package/src/types/request-scope.ts +112 -0
  374. package/src/types/route-config.ts +20 -52
  375. package/src/types/route-entry.ts +0 -6
  376. package/src/types/segments.ts +135 -14
  377. package/src/urls/include-helper.ts +19 -64
  378. package/src/urls/include-provider.ts +71 -0
  379. package/src/urls/index.ts +2 -11
  380. package/src/urls/path-helper-types.ts +63 -17
  381. package/src/urls/path-helper.ts +22 -106
  382. package/src/urls/pattern-types.ts +72 -19
  383. package/src/urls/response-types.ts +22 -29
  384. package/src/urls/type-extraction.ts +98 -154
  385. package/src/urls/urls-function.ts +1 -19
  386. package/src/use-loader.tsx +292 -107
  387. package/src/vercel/index.ts +11 -0
  388. package/src/vercel/tracing.ts +88 -0
  389. package/src/vite/debug.ts +185 -0
  390. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  391. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  392. package/src/vite/discovery/discover-routers.ts +127 -86
  393. package/src/vite/discovery/discovery-errors.ts +255 -0
  394. package/src/vite/discovery/gate-state.ts +171 -0
  395. package/src/vite/discovery/prerender-collection.ts +96 -68
  396. package/src/vite/discovery/route-types-writer.ts +40 -84
  397. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  398. package/src/vite/discovery/state.ts +45 -1
  399. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  400. package/src/vite/index.ts +4 -0
  401. package/src/vite/inject-client-debug.ts +88 -0
  402. package/src/vite/plugin-types.ts +210 -10
  403. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  404. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  405. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  406. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  407. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  408. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  409. package/src/vite/plugins/expose-action-id.ts +48 -95
  410. package/src/vite/plugins/expose-id-utils.ts +88 -55
  411. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  412. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  413. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  414. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  415. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  416. package/src/vite/plugins/performance-tracks.ts +26 -25
  417. package/src/vite/plugins/refresh-cmd.ts +1 -1
  418. package/src/vite/plugins/use-cache-transform.ts +73 -83
  419. package/src/vite/plugins/vercel-output.ts +384 -0
  420. package/src/vite/plugins/version-injector.ts +40 -29
  421. package/src/vite/plugins/version-plugin.ts +37 -40
  422. package/src/vite/plugins/virtual-entries.ts +138 -27
  423. package/src/vite/rango.ts +236 -138
  424. package/src/vite/router-discovery.ts +927 -136
  425. package/src/vite/utils/ast-handler-extract.ts +26 -35
  426. package/src/vite/utils/banner.ts +1 -1
  427. package/src/vite/utils/bundle-analysis.ts +10 -15
  428. package/src/vite/utils/client-chunks.ts +184 -0
  429. package/src/vite/utils/directive-prologue.ts +40 -0
  430. package/src/vite/utils/forward-user-plugins.ts +171 -0
  431. package/src/vite/utils/manifest-utils.ts +4 -59
  432. package/src/vite/utils/package-resolution.ts +20 -52
  433. package/src/vite/utils/prerender-utils.ts +71 -43
  434. package/src/vite/utils/shared-utils.ts +142 -43
  435. package/src/browser/action-response-classifier.ts +0 -99
  436. package/src/browser/react/use-client-cache.ts +0 -58
  437. package/src/browser/shallow.ts +0 -40
  438. package/src/handles/index.ts +0 -7
  439. package/src/network-error-thrower.tsx +0 -23
  440. package/src/router/middleware-cookies.ts +0 -55
package/src/ssr/index.tsx CHANGED
@@ -1,18 +1,6 @@
1
1
  import React from "react";
2
- import { renderSegments } from "../segment-system.js";
3
- import { filterSegmentOrder } from "../browser/react/filter-segment-order.js";
4
- import { ThemeProvider } from "../theme/ThemeProvider.js";
5
- import { NonceContext } from "../browser/react/nonce-context.js";
6
- import { NavigationStoreContext } from "../browser/react/context.js";
7
- import type { NavigationStoreContextValue } from "../browser/react/context.js";
8
- import type { HandleData } from "../browser/types.js";
2
+ import { createSsrRootComponent } from "./ssr-root.js";
9
3
  import type { ErrorPhase } from "../types.js";
10
- import type { ResolvedSegment } from "../types.js";
11
- import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
12
- import type {
13
- EventController,
14
- DerivedNavigationState,
15
- } from "../browser/event-controller.js";
16
4
 
17
5
  /**
18
6
  * Options for injectRSCPayload
@@ -40,6 +28,51 @@ interface ReactDOMReadableStream extends ReadableStream<Uint8Array> {
40
28
  allReady: Promise<void>;
41
29
  }
42
30
 
31
+ /**
32
+ * Options for prerender from react-dom/static.edge
33
+ */
34
+ interface PrerenderOptions {
35
+ signal?: AbortSignal;
36
+ bootstrapScriptContent?: string;
37
+ onError?: (error: unknown) => void;
38
+ }
39
+
40
+ /**
41
+ * Result of prerender from react-dom/static.edge. `postponed` is React's
42
+ * opaque resume state — non-null when the render was aborted with pending
43
+ * holes, null when the shell completed with nothing left to stream.
44
+ */
45
+ interface PrerenderResult {
46
+ prelude: ReadableStream<Uint8Array>;
47
+ postponed: unknown;
48
+ }
49
+
50
+ /**
51
+ * prerender from react-dom/static.edge
52
+ */
53
+ type PrerenderFn = (
54
+ element: React.ReactNode,
55
+ options?: PrerenderOptions,
56
+ ) => Promise<PrerenderResult>;
57
+
58
+ /**
59
+ * Options for resume from react-dom/server.edge
60
+ */
61
+ interface ResumeOptions {
62
+ onError?: (error: unknown) => void;
63
+ nonce?: string;
64
+ }
65
+
66
+ /**
67
+ * resume from react-dom/server.edge — continues a prerendered render, emitting
68
+ * only the postponed holes.
69
+ */
70
+ type ResumeFn = (
71
+ element: React.ReactNode,
72
+ postponedState: unknown,
73
+ options?: ResumeOptions,
74
+ ) => Promise<ReactDOMReadableStream>;
75
+
43
76
  /**
44
77
  * Options for the renderHTML function
45
78
  */
@@ -71,7 +104,7 @@ export interface SSRRenderOptions {
71
104
  */
72
105
  export interface SSRDependencies<TEnv = unknown> {
73
106
  /**
74
- * createFromReadableStream from @vitejs/plugin-rsc/ssr
107
+ * createFromReadableStream from @rangojs/router/internal/deps/ssr
75
108
  */
76
109
  createFromReadableStream: <T>(
77
110
  stream: ReadableStream<Uint8Array>,
@@ -86,7 +119,7 @@ export interface SSRDependencies<TEnv = unknown> {
86
119
  ) => Promise<ReactDOMReadableStream>;
87
120
 
88
121
  /**
89
- * injectRSCPayload from rsc-html-stream/server
122
+ * injectRSCPayload from @rangojs/router/internal/deps/html-stream-server
90
123
  */
91
124
  injectRSCPayload: (
92
125
  rscStream: ReadableStream<Uint8Array>,
@@ -99,6 +132,18 @@ export interface SSRDependencies<TEnv = unknown> {
99
132
  */
100
133
  loadBootstrapScriptContent: () => Promise<string>;
101
134
 
135
+ /**
136
+ * prerender from react-dom/static.edge. Optional; required only by
137
+ * {@link createShellCaptureHandler} for PPR shell capture.
138
+ */
139
+ prerender?: PrerenderFn;
140
+
141
+ /**
142
+ * resume from react-dom/server.edge. Optional; required only by
143
+ * {@link createShellResumeHandler} for resuming a postponed shell.
144
+ */
145
+ resume?: ResumeFn;
146
+
102
147
  /**
103
148
  * Optional callback invoked when an error occurs during SSR rendering.
104
149
  *
@@ -119,94 +164,165 @@ export interface SSRDependencies<TEnv = unknown> {
119
164
  }
120
165
 
121
166
  /**
122
- * RSC payload type (minimal interface for SSR)
167
+ * Default guard for how long capture waits on the caller's `quiesce` signal
168
+ * before forcing the abort that freezes the shell. This is the ONLY wall-clock
169
+ * on the capture path and it is a pathological guard — it should never fire once
170
+ * the caller's `quiesce` is a task-quantized, frozen-byte signal (the capture
171
+ * gate in shell-capture.ts). See docs/design/ppr-shell-resume.md.
123
172
  */
124
- interface RscPayload {
125
- metadata?: {
126
- segments?: ResolvedSegment[];
127
- rootLayout?: React.ComponentType<{ children: React.ReactNode }>;
128
- handles?: AsyncGenerator<HandleData, void, unknown>;
129
- matched?: string[];
130
- pathname?: string;
131
- params?: Record<string, string>;
132
- basename?: string;
133
- themeConfig?: ResolvedThemeConfig | null;
134
- initialTheme?: Theme;
135
- version?: string;
136
- };
137
- }
173
+ const DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS = 5000;
174
+
175
+ /**
176
+ * Fixed number of macrotask hops between `quiesce` resolving and the abort. These
177
+ * give React's fizz worker turns to flush the settled shell into the prelude and
178
+ * mark still-pending boundaries as POSTPONED (rather than errored) before
179
+ * controller.abort() lands. Not a wall-clock wait.
180
+ *
181
+ * Why 16 and not the original 2: under the REPLAY-ONLY capture model
182
+ * (docs/design/ppr-shell-resume.md), the capture Flight render serializes ring-3
183
+ * cached segments that are ALREADY serialized, so it emits the whole shell payload
184
+ * in the first tick and the gate declares quiesce almost immediately (~a few ms).
185
+ * On the old fresh-execution path the Flight dribbled out as handlers ran, so
186
+ * Flight-quiet effectively meant "the shell has rendered" and 2 hops sufficed. Under
187
+ * replay, Flight-quiet fires BEFORE the fizz side has consumed the instant payload
188
+ * and rendered the shell to `<body>`, so the fizz needs a real buffer of turns after
189
+ * quiesce — otherwise the abort lands on an unrendered tree (empty prelude, root
190
+ * postpone) and the sanity gate refuses. Still task-based (masked loaders never
191
+ * emit, so more hops never lets a hole settle); a cold worker whose first
192
+ * attempt still under-renders heals on the in-place retry. Bounded by maxWaitMs.
193
+ */
194
+ const POST_QUIESCE_TASK_HOPS = 16;
138
195
 
139
196
  /**
140
- * Consume an async generator and return a Promise that resolves with the final value.
141
- * Used for SSR where we need to await all handle data before rendering.
197
+ * Route an SSR error through the deps.onError notification callback with the
198
+ * "rendering" phase. Swallows callback failures so a broken reporter never
199
+ * masks the original error. Shared by renderHTML, capture, and resume so the
200
+ * onError contract is identical across all three handlers.
142
201
  */
143
- async function consumeAsyncGenerator(
144
- generator: AsyncGenerator<HandleData, void, unknown>,
145
- ): Promise<HandleData> {
146
- let lastData: HandleData = {};
147
- for await (const data of generator) {
148
- lastData = data;
202
+ function reportRenderError(
203
+ onError: SSRDependencies["onError"],
204
+ error: unknown,
205
+ ): void {
206
+ if (onError) {
207
+ const errorObj = error instanceof Error ? error : new Error(String(error));
208
+ try {
209
+ onError(errorObj, { phase: "rendering" });
210
+ } catch (callbackError) {
211
+ console.error("[SSRHandler.onError] Callback error:", callbackError);
212
+ }
149
213
  }
150
- return lastData;
151
214
  }
152
215
 
153
216
  /**
154
- * Create a minimal event controller for SSR.
155
- * This provides the correct pathname so useNavigation returns the right value during SSR.
217
+ * Yield one macrotask. Used by capture to let React's fizz worker flush the
218
+ * shell and mark still-pending boundaries as postponed before the abort.
156
219
  */
157
- function createSsrEventController(opts: {
158
- pathname: string;
159
- params?: Record<string, string>;
160
- handleData?: HandleData;
161
- matched?: string[];
162
- }): EventController {
163
- const location = new URL(opts.pathname, "http://localhost");
164
- let params = opts.params ?? {};
165
- const handleState = {
166
- data: opts.handleData ?? {},
167
- segmentOrder: filterSegmentOrder(opts.matched ?? []),
168
- };
169
- const state: DerivedNavigationState = {
170
- state: "idle",
171
- isStreaming: false,
172
- isNavigating: false,
173
- location,
174
- pendingUrl: null,
175
- inflightActions: [],
176
- };
220
+ function macrotask(): Promise<void> {
221
+ return new Promise((resolve) => setTimeout(resolve, 0));
222
+ }
177
223
 
178
- return {
179
- getState: () => state,
180
- getLocation: () => location,
181
- subscribe: () => () => {},
182
- getActionState: () => ({
183
- state: "idle",
184
- actionId: null,
185
- payload: null,
186
- error: null,
187
- result: null,
188
- }),
189
- subscribeToAction: () => () => {},
190
- subscribeToHandles: () => () => {},
191
- setHandleData: () => {},
192
- getHandleState: () => handleState,
193
- setParams: (nextParams) => {
194
- params = nextParams;
195
- },
196
- getParams: () => params,
197
- setLocation: () => {},
198
- startNavigation: () => {
199
- throw new Error("Navigation not supported during SSR");
200
- },
201
- abortNavigation: () => {},
202
- startAction: () => {
203
- throw new Error("Actions not supported during SSR");
224
+ /**
225
+ * A timeout promise paired with a cancel() so the pending timer is cleared once
226
+ * the race is decided — otherwise the maxWait timer keeps the event loop alive
227
+ * for the full duration even after `quiesce` won.
228
+ */
229
+ function createCancelableTimeout(ms: number): {
230
+ promise: Promise<void>;
231
+ cancel: () => void;
232
+ } {
233
+ let id: ReturnType<typeof setTimeout> | undefined;
234
+ const promise = new Promise<void>((resolve) => {
235
+ id = setTimeout(resolve, ms);
236
+ });
237
+ return { promise, cancel: () => clearTimeout(id) };
238
+ }
239
+
240
+ /**
241
+ * Drain a ReadableStream fully into a single Uint8Array. Capture buffers the
242
+ * whole prelude so it can be stored and later prepended byte-for-byte.
243
+ */
244
+ async function readStreamToUint8Array(
245
+ stream: ReadableStream<Uint8Array>,
246
+ ): Promise<Uint8Array> {
247
+ const reader = stream.getReader();
248
+ const chunks: Uint8Array[] = [];
249
+ let total = 0;
250
+ try {
251
+ while (true) {
252
+ const { done, value } = await reader.read();
253
+ if (done) break;
254
+ chunks.push(value);
255
+ total += value.length;
256
+ }
257
+ } catch (error) {
258
+ // Mid-read abort path (documented): the prelude stream errors with our
259
+ // abort reason while we are still reading it. Cancel the source before
260
+ // rethrowing so it is not left uncancelled; releaseLock always runs in
261
+ // finally. Mirrors src/rsc/rsc-rendering.ts's serve-side reader cleanup.
262
+ reader.cancel(error).catch(() => {});
263
+ throw error;
264
+ } finally {
265
+ reader.releaseLock();
266
+ }
267
+ const out = new Uint8Array(total);
268
+ let offset = 0;
269
+ for (const chunk of chunks) {
270
+ out.set(chunk, offset);
271
+ offset += chunk.length;
272
+ }
273
+ return out;
274
+ }
275
+
276
+ /**
277
+ * A minimal HTML stream for the resume DATA variant: one empty chunk, then
278
+ * close.
279
+ *
280
+ * injectRSCPayload only resolves its internal flight-data promise (and thus
281
+ * only writes the Flight payload <script> pushes) from inside transform()'s
282
+ * scheduled callback. A stream that closes without ever emitting a chunk never
283
+ * runs transform, so its flush() awaits a promise that is never resolved and
284
+ * the output deadlocks. Emitting a single empty chunk runs transform once,
285
+ * which is enough for the payload to be written and the trailer appended.
286
+ */
287
+ function createDataVariantHtmlStream(): ReadableStream<Uint8Array> {
288
+ return new ReadableStream({
289
+ start(controller) {
290
+ controller.enqueue(new Uint8Array(0));
291
+ controller.close();
204
292
  },
205
- abortAllActions: () => {},
206
- getCurrentNavigation: () => null,
207
- getInflightActions: () => new Map(),
208
- hadAnyConcurrentActions: () => false,
209
- };
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Options for the captureShellHTML function returned by
298
+ * {@link createShellCaptureHandler}.
299
+ */
300
+ interface ShellCaptureOptions {
301
+ /** Caller-provided promise that resolves once the cached content settled. */
302
+ quiesce: Promise<void>;
303
+ /** Upper bound on how long to wait for `quiesce`. Default 5000ms. */
304
+ maxWaitMs?: number;
305
+ }
306
+
307
+ /**
308
+ * Result of a successful shell capture. `prelude` is the raw prelude bytes;
309
+ * `postponed` is React's resume state serialized to JSON, or null when the
310
+ * shell completed with no holes (the DATA variant).
311
+ */
312
+ interface ShellCaptureResult {
313
+ prelude: Uint8Array;
314
+ postponed: string | null;
315
+ }
316
+
317
+ /**
318
+ * Options for the resumeShellHTML function returned by
319
+ * {@link createShellResumeHandler}.
320
+ */
321
+ interface ShellResumeOptions {
322
+ /** JSON from capture; null selects the DATA variant (no fizz). */
323
+ postponed: string | null;
324
+ /** Nonce for CSP. */
325
+ nonce?: string;
210
326
  }
211
327
 
212
328
  /**
@@ -214,10 +330,10 @@ function createSsrEventController(opts: {
214
330
  *
215
331
  * @example
216
332
  * ```tsx
217
- * import { createSSRHandler } from "rsc-router/ssr";
218
- * import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
333
+ * import { createSSRHandler } from "@rangojs/router/ssr";
334
+ * import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
219
335
  * import { renderToReadableStream } from "react-dom/server.edge";
220
- * import { injectRSCPayload } from "rsc-html-stream/server";
336
+ * import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
221
337
  *
222
338
  * export const renderHTML = createSSRHandler({
223
339
  * createFromReadableStream,
@@ -255,81 +371,11 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
255
371
  // - rscStream2: For browser hydration (inject as __FLIGHT_DATA__)
256
372
  const [rscStream1, rscStream2] = rscStream.tee();
257
373
 
258
- // Deserialize RSC stream to React tree
259
- let payload: Promise<RscPayload> | undefined;
260
- let handlesPromise: Promise<HandleData> | undefined;
261
- let ssrContextValue: NavigationStoreContextValue | undefined;
262
- function SsrRoot() {
263
- payload ??= createFromReadableStream<RscPayload>(rscStream1);
264
- const resolved = React.use(payload);
265
-
266
- const themeConfig = resolved.metadata?.themeConfig ?? null;
267
- const pathname = resolved.metadata?.pathname ?? "/";
268
-
269
- // Await handles before creating SSR event controller so hooks can
270
- // read request-local handle data via NavigationStoreContext.
271
- // The handles property is an async generator that yields on each push
272
- // Memoize the promise since async generators can only be iterated once
273
- let handleData: HandleData = {};
274
- if (resolved.metadata?.handles) {
275
- handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
276
- handleData = React.use(handlesPromise);
277
- }
278
-
279
- // Create SSR context with request-local pathname/params/handles.
280
- ssrContextValue ??= {
281
- store: null as any,
282
- eventController: createSsrEventController({
283
- pathname,
284
- params: resolved.metadata?.params,
285
- handleData,
286
- matched: resolved.metadata?.matched,
287
- }),
288
- navigate: async () => {},
289
- refresh: async () => {},
290
- version: resolved.metadata?.version,
291
- basename: resolved.metadata?.basename,
292
- };
293
-
294
- // Build content tree from segments.
295
- // Order must match NavigationProvider: NavigationStoreContext > ThemeProvider > content
296
- const reconstructedRoot = renderSegments(
297
- resolved.metadata?.segments ?? [],
298
- {
299
- rootLayout: resolved.metadata?.rootLayout,
300
- },
301
- );
302
- let content: React.ReactNode =
303
- reconstructedRoot instanceof Promise
304
- ? React.use(reconstructedRoot)
305
- : reconstructedRoot;
306
-
307
- // Wrap content with ThemeProvider if theme is enabled
308
- if (themeConfig) {
309
- content = (
310
- <ThemeProvider
311
- config={themeConfig}
312
- initialTheme={resolved.metadata?.initialTheme}
313
- >
314
- {content}
315
- </ThemeProvider>
316
- );
317
- }
318
-
319
- // Wrap with NonceContext so client components (e.g. MetaTags) can
320
- // apply CSP nonces to inline scripts during SSR. Always present to
321
- // match the browser-side NavigationProvider tree shape for hydration.
322
- content = (
323
- <NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
324
- );
325
-
326
- // Wrap with NavigationStoreContext for useNavigation hook
327
- return (
328
- <NavigationStoreContext.Provider value={ssrContextValue!}>
329
- {content}
330
- </NavigationStoreContext.Provider>
331
- );
332
- }
374
+ const SsrRoot = createSsrRootComponent({
375
+ createFromReadableStream,
376
+ rscStream: rscStream1,
377
+ nonce,
378
+ });
333
379
 
334
380
  // Get bootstrap script content
335
381
  const bootstrapScriptContent = await loadBootstrapScriptContent();
@@ -353,16 +399,250 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
353
399
  // Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
354
400
  return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
355
401
  } catch (error) {
356
- // Invoke onError callback if provided
357
- if (onError) {
358
- const errorObj =
359
- error instanceof Error ? error : new Error(String(error));
360
- try {
361
- onError(errorObj, { phase: "rendering" });
362
- } catch (callbackError) {
363
- console.error("[SSRHandler.onError] Callback error:", callbackError);
402
+ reportRenderError(onError, error);
403
+ throw error;
404
+ }
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Create the PPR shell capture handler.
410
+ *
411
+ * captureShellHTML prerenders the shell over the (cached, loader-masked) Flight
412
+ * stream, aborts once the shell settles, and returns the prelude bytes plus the
413
+ * postponed resume state for storage. The stored pair is later served by
414
+ * {@link createShellResumeHandler}. See docs/design/ppr-shell-resume.md.
415
+ *
416
+ * Throws at creation if `deps.prerender` is missing — capture cannot run
417
+ * without react-dom/static.edge's prerender.
418
+ */
419
+ export function createShellCaptureHandler<TEnv = unknown>(
420
+ deps: SSRDependencies<TEnv>,
421
+ ) {
422
+ const { createFromReadableStream, loadBootstrapScriptContent, prerender } =
423
+ deps;
424
+ const onError = deps.onError;
425
+
426
+ if (!prerender) {
427
+ throw new Error(
428
+ "[createShellCaptureHandler] Missing `prerender` dependency (react-dom/static.edge). " +
429
+ "PPR shell capture requires the prerender export; wire it in the SSR virtual entry.",
430
+ );
431
+ }
432
+
433
+ /**
434
+ * Prerender the shell and return the stored artifacts, or null when the
435
+ * shell degraded (root postpone / hung handles) and must not be cached.
436
+ *
437
+ * @param rscStream - Flight stream to render the shell over. Not teed and not
438
+ * piped through injectRSCPayload: the hydration payload is produced fresh
439
+ * per request by the resume/serve pass.
440
+ * @param opts - quiesce signal and maxWait guard.
441
+ */
442
+ return async function captureShellHTML(
443
+ rscStream: ReadableStream<Uint8Array>,
444
+ opts: ShellCaptureOptions,
445
+ ): Promise<ShellCaptureResult | null> {
446
+ const maxWaitMs = opts.maxWaitMs ?? DEFAULT_SHELL_CAPTURE_MAX_WAIT_MS;
447
+
448
+ // Arm the maxWaitMs deadline BEFORE the first await so it bounds the ENTIRE
449
+ // capture, the bootstrap-script load included. loadBootstrapScriptContent()
450
+ // used to run before the timer, so a hung/slow bootstrap load hung
451
+ // captureShellHTML with no upper bound and held the background capture task
452
+ // open. One deadline, shared by the bootstrap race below and the quiesce
453
+ // race, keeps the whole path "bounded by maxWaitMs like every quiesce input".
454
+ const deadline = createCancelableTimeout(maxWaitMs);
455
+ try {
456
+ // No nonce (nonce'd requests never reach capture); no formState.
457
+ const SsrRoot = createSsrRootComponent({
458
+ createFromReadableStream,
459
+ rscStream,
460
+ });
461
+
462
+ // Bootstrap load raced against the deadline. A load that never resolves
463
+ // within maxWaitMs is the same bounded no-shell degrade as a shell that
464
+ // never goes quiet: return null, do not hang. A load that REJECTS is a
465
+ // genuine error and still propagates (it is not the deadline). `null` is
466
+ // the deadline sentinel — disjoint from the load's `Promise<string>`, so
467
+ // the race narrows to `string | null` with no wrapper. The no-op catch
468
+ // keeps a late rejection off the unhandledRejection path when the deadline
469
+ // already won; a rejection that lands first still propagates out.
470
+ const load = loadBootstrapScriptContent();
471
+ load.catch(() => {});
472
+ const bootstrapScriptContent = await Promise.race([
473
+ load,
474
+ deadline.promise.then(() => null),
475
+ ]);
476
+ if (bootstrapScriptContent === null) {
477
+ return null;
478
+ }
479
+
480
+ // Start prerender first, then run the abort schedule concurrently. When
481
+ // holes are pending, prerender's promise settles only after abort(); when
482
+ // the shell completes with no holes it settles on its own and the later
483
+ // abort() is a harmless no-op (the DATA variant).
484
+ const controller = new AbortController();
485
+ // Private reason object: the deliberate abort is identified by object
486
+ // IDENTITY in both the onError below and the post-await catch. React
487
+ // propagates this EXACT object to onError for every still-pending boundary
488
+ // (verified identity-preserving), and rejects/errors the prelude with it.
489
+ const abortReason = { rangoShellCaptureAbort: true };
490
+ const prerenderPromise = prerender(<SsrRoot />, {
491
+ signal: controller.signal,
492
+ bootstrapScriptContent,
493
+ // Abort is how capture WORKS: once the shell is quiet we abort() to
494
+ // freeze the prelude and let the still-pending holes postpone. React
495
+ // reports the abort reason for each pending boundary through onError.
496
+ // Without an onError here React falls back to console.error, so every
497
+ // capture that still has a live hole at abort time (the normal case)
498
+ // dumps a stack once per pending boundary. That is EXPECTED degradation,
499
+ // so swallow OUR abort — matched by IDENTITY (error === abortReason).
500
+ // Discriminate by identity, NOT error.name: capture aborts before
501
+ // awaiting, so signal.aborted is unconditionally true and a name check
502
+ // swallowed genuine AbortError-named throws (a component's own
503
+ // fetch/AbortController cancellation) as if they were our abort. Genuine
504
+ // render errors are NOT our sentinel and still surface through
505
+ // deps.onError, the same channel renderHTML uses. See
506
+ // docs/design/ppr-shell-resume.md.
507
+ onError: (error: unknown) => {
508
+ if (error === abortReason) {
509
+ return;
510
+ }
511
+ reportRenderError(onError, error);
512
+ },
513
+ });
514
+ // Pre-attach a no-op catch: the real await sits AFTER quiesce + the
515
+ // post-quiesce hops, so an early prerender rejection (e.g. a bake-lane
516
+ // loader tripping the identity guard within milliseconds) would otherwise
517
+ // spend several turns handler-less and crash the worker as an unhandled
518
+ // rejection. The actual rejection handling still happens at the await
519
+ // below; this parallel handler only keeps the gap crash-free.
520
+ prerenderPromise.catch(() => {});
521
+
522
+ // Wait for the caller's quiesce signal, bounded by the SAME deadline. By
523
+ // the time it resolves the Flight input is byte-quiet and FROZEN by the
524
+ // capture gate (shell-capture.ts gateFlightForCapture), so there is no
525
+ // wall-clock debounce here — maxWaitMs is only the pathological guard for a
526
+ // shell that never goes quiet (a root postpone / hung handle).
527
+ await Promise.race([opts.quiesce, deadline.promise]);
528
+ // Fixed task hops before the abort: give React's fizz worker turns to flush
529
+ // the now-complete shell and mark the still-pending boundaries as POSTPONED
530
+ // rather than errored. Deterministic (the byte set is already frozen), so a
531
+ // fixed count of turns suffices — no wall-clock.
532
+ for (let i = 0; i < POST_QUIESCE_TASK_HOPS; i++) {
533
+ await macrotask();
534
+ }
535
+ controller.abort(abortReason);
536
+
537
+ // A hard prerender rejection (fatal shell error) propagates. Expected
538
+ // degradation surfaces three ways and all return null: a trivial prelude
539
+ // (sanity gate below), the prerender REJECTING with our abort reason, or
540
+ // the prelude STREAM erroring with our abort reason mid-read — both abort
541
+ // shapes happen when our own abort lands before the shell completed (seen
542
+ // on dev cold paths, where module transform / first-render latency
543
+ // outlasts flight quiesce; a later request re-captures against warm
544
+ // modules and succeeds).
545
+ let prelude: Uint8Array;
546
+ let postponed: unknown;
547
+ try {
548
+ const result = await prerenderPromise;
549
+ prelude = await readStreamToUint8Array(result.prelude);
550
+ postponed = result.postponed;
551
+ } catch (error) {
552
+ // Identity match: swallow ONLY our own deliberate abort
553
+ // (error === abortReason). Not error.name — capture aborts before this
554
+ // await, so signal.aborted is always true, and a name check let a
555
+ // genuine AbortError-named throw masquerade as our abort and degrade
556
+ // into a retryable no-shell, hiding real failures from reportCacheError.
557
+ if (error === abortReason) {
558
+ return null;
364
559
  }
560
+ throw error;
561
+ }
562
+
563
+ // Sanity gate: a prelude with no `<body` is the no-shell failure mode.
564
+ // Return null and store nothing; the request falls back to axis 1 and a
565
+ // later request re-captures. The dominant real-world cause is a loader
566
+ // route WITHOUT a route-level loading() boundary: renderSegments' loading-
567
+ // less branch awaits loader data at TREE-BUILD, so the masked loader pins
568
+ // the whole tree above <body> (root postpone). Root-postponing layouts and
569
+ // hung handles degrade the same way. shell-capture.ts logs a once-per-key
570
+ // warning so the eternal-MISS shape is diagnosable.
571
+ if (!new TextDecoder().decode(prelude).includes("<body")) {
572
+ return null;
573
+ }
574
+
575
+ return {
576
+ prelude,
577
+ postponed: postponed == null ? null : JSON.stringify(postponed),
578
+ };
579
+ } finally {
580
+ deadline.cancel();
581
+ }
582
+ };
583
+ }
584
+
585
+ /**
586
+ * Create the PPR shell resume handler.
587
+ *
588
+ * resumeShellHTML produces the per-request live portion of the document: for a
589
+ * postponed shell it resumes fizz over a fresh SsrRoot to emit only the holes;
590
+ * for the DATA variant it emits only the fresh Flight payload scripts. The
591
+ * caller (shell-cache middleware) prepends the stored prelude bytes to form the
592
+ * composite response. See docs/design/ppr-shell-resume.md.
593
+ */
594
+ export function createShellResumeHandler<TEnv = unknown>(
595
+ deps: SSRDependencies<TEnv>,
596
+ ) {
597
+ const { createFromReadableStream, injectRSCPayload, resume, onError } = deps;
598
+
599
+ /**
600
+ * @param rscStream - Fresh full Flight stream for this request.
601
+ * @param opts - postponed state (null = DATA variant) and optional nonce.
602
+ */
603
+ return async function resumeShellHTML(
604
+ rscStream: ReadableStream<Uint8Array>,
605
+ opts: ShellResumeOptions,
606
+ ): Promise<ReadableStream<Uint8Array>> {
607
+ const { postponed, nonce } = opts;
608
+
609
+ try {
610
+ if (postponed === null) {
611
+ // DATA variant: the stored prelude is the complete shell. No fizz runs;
612
+ // feed injectRSCPayload a minimal HTML stream so its flush appends the
613
+ // fresh Flight payload scripts after the shell. The stream must emit at
614
+ // least one chunk — see createDataVariantHtmlStream.
615
+ return createDataVariantHtmlStream().pipeThrough(
616
+ injectRSCPayload(rscStream, { nonce }),
617
+ );
365
618
  }
619
+
620
+ if (!resume) {
621
+ throw new Error(
622
+ "[createShellResumeHandler] Missing `resume` dependency (react-dom/server.edge). " +
623
+ "Resuming a postponed shell requires the resume export; wire it in the SSR virtual entry.",
624
+ );
625
+ }
626
+
627
+ // Tee: one branch deserializes into the SsrRoot VDOM that resume() replays
628
+ // (a fresh instance is fine — replay matches structure, not identity), the
629
+ // other feeds the fresh hydration payload to injectRSCPayload.
630
+ const [rscStream1, rscStream2] = rscStream.tee();
631
+
632
+ const SsrRoot = createSsrRootComponent({
633
+ createFromReadableStream,
634
+ rscStream: rscStream1,
635
+ nonce,
636
+ });
637
+
638
+ const resumed = await resume(<SsrRoot />, JSON.parse(postponed), {
639
+ onError: (error) => reportRenderError(onError, error),
640
+ nonce,
641
+ });
642
+
643
+ return resumed.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
644
+ } catch (error) {
645
+ reportRenderError(onError, error);
366
646
  throw error;
367
647
  }
368
648
  };