@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.141

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