@wular/pnext 0.0.1

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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
package/src/ppr.ts ADDED
@@ -0,0 +1,784 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ // Partial Prerendering (Next.js-compatible "mixed" model).
4
+ //
5
+ // At build time a PPR route is rendered in a "prerender" scope where there is
6
+ // no request. Any access to request data (cookies()/headers()/...) throws a
7
+ // PostponeError instead of a hard failure. The renderer catches that error at
8
+ // the nearest <Suspense> boundary, keeps the fallback in the static shell, and
9
+ // records the boundary as a dynamic "hole". At request time the shell is served
10
+ // immediately and only the recorded holes are re-rendered with the real request
11
+ // and streamed in.
12
+ //
13
+ // Stage C (cacheComponents) generalizes this: request APIs become HANGING
14
+ // PROMISES during prerender rather than throwing synchronously at the call
15
+ // site. The postpone fires at the first `await` of the hanging promise inside a
16
+ // Suspense subtree (dynamic-at-await-site), matching Next's semantic where
17
+ // passing `cookies()` as an un-awaited promise to a child does NOT make the
18
+ // parent dynamic. Awaiting it does.
19
+
20
+ // PostponeError + isPostpone live in a client-safe module (no node:async_hooks)
21
+ // so client control-flow code can import isPostpone without pulling this module
22
+ // (and its AsyncLocalStorage) into the browser bundle. Re-exported here so
23
+ // every server caller keeps importing them from ppr.
24
+ export { PostponeError, isPostpone } from './ppr-postpone';
25
+ import { isPostpone as isPostponeError, PostponeError } from './ppr-postpone';
26
+
27
+ // The prerender scope carries an AbortController so a hanging promise whose
28
+ // settlement microtask has not yet run stays pending once the prerender has
29
+ // produced its shell (see hangingPromise's `scope.signal.aborted` guard).
30
+ interface PrerenderScope {
31
+ /** Fires when the prerender pass has produced its shell. */
32
+ readonly signal: AbortSignal;
33
+ abort: () => void;
34
+ /**
35
+ * A RUNTIME-PREFETCH prerender (`unstable_instant` full prefetch): the render runs against a real sampled
36
+ * request, so request APIs RESOLVE from it instead of hanging - only connection()-gated content stays
37
+ * dynamic, its promise hanging so the boundary records a hole. Nothing this request-sampled render
38
+ * produces may be persisted into build artifacts.
39
+ */
40
+ readonly runtimePrefetch?: boolean;
41
+ /**
42
+ * Runtime-prefetch prerenders only: a runtime API (cookies/headers/params/searchParams, or a `use cache`
43
+ * read) has been AWAITED somewhere in this render. From that point on, synchronous platform IO aborts its
44
+ * boundary - see runWithRuntimePrefetchSyncIoAbort.
45
+ */
46
+ awaitedRuntimeApi?: boolean;
47
+ /**
48
+ * A prerender whose output becomes a PREFETCH shell (the body segment the client's segment cache stores),
49
+ * NOT the document served for a direct request. Short-lived `use cache` scopes are omitted from it exactly
50
+ * as they are from a runtime prefetch - the client's stale window for the prefetch would otherwise be
51
+ * governed by a cache that expires before it.
52
+ */
53
+ readonly prefetchShell?: boolean;
54
+ /**
55
+ * A BUILD prerender running under Next's task-boundary semantics (see
56
+ * awaitAtTaskBoundary). Only set for a cacheComponents build shell render.
57
+ */
58
+ readonly taskBoundary?: boolean;
59
+ }
60
+
61
+ // Anchored on globalThis (like requestStorage in request/context.ts) so a
62
+ // compiled compat copy of this module shares the same prerender-scope ALS —
63
+ // unanchored, a second module instance splits prerender detection.
64
+ const PRERENDER_STORAGE = Symbol.for('pnext.prerenderStorage');
65
+ const prerenderStorage = ((globalThis as Record<PropertyKey, unknown>)[PRERENDER_STORAGE] ??=
66
+ new AsyncLocalStorage<PrerenderScope>()) as AsyncLocalStorage<PrerenderScope>;
67
+
68
+ interface PrerenderDynamicTracker {
69
+ dynamic: boolean;
70
+ }
71
+
72
+ const prerenderDynamicTrackerStorage = new AsyncLocalStorage<PrerenderDynamicTracker>();
73
+
74
+ export async function trackPrerenderDynamic<T>(callback: () => Promise<T>): Promise<{
75
+ value: T;
76
+ dynamic: boolean;
77
+ }> {
78
+ const tracker: PrerenderDynamicTracker = { dynamic: false };
79
+ const value = await prerenderDynamicTrackerStorage.run(tracker, callback);
80
+ return { value, dynamic: tracker.dynamic };
81
+ }
82
+
83
+ function markPrerenderDynamic(): void {
84
+ const tracker = prerenderDynamicTrackerStorage.getStore();
85
+ if (tracker) tracker.dynamic = true;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Build-shell dynamic-source tracking.
90
+ //
91
+ // A cacheComponents route whose only dynamic access is `params` inside a
92
+ // <Suspense> boundary produces a hole during the partial-shell prerender, yet
93
+ // must NOT be served as a postponed fallback shell: params are part of the URL
94
+ // and always available at request time, so the route is served with a blocking
95
+ // dynamic render (Next's fallback-shells "without IO should not postpone").
96
+ // A hole caused by cached IO (`use cache`/cached fetch) or by request data
97
+ // other than params (cookies()/headers()/connection()/searchParams) is a
98
+ // genuine partial-shell postpone and keeps the fallback-shell path.
99
+ //
100
+ // Anchored on globalThis because the built compat-server bundle loads its own
101
+ // copy of this module (same reason revalidate.ts / cache-fill state anchor
102
+ // there): the cache runtime recording a fill lives in a different module copy
103
+ // from the build driver that begins/reads the window.
104
+ const SHELL_SOURCES = Symbol.for('pnext.shellDynamicSources');
105
+ interface ShellSourceState {
106
+ active: boolean;
107
+ cacheIO: boolean;
108
+ nonParamsRequest: boolean;
109
+ }
110
+ function shellSourceState(): ShellSourceState {
111
+ const root = globalThis as Record<PropertyKey, unknown>;
112
+ return (root[SHELL_SOURCES] ??= {
113
+ active: false,
114
+ cacheIO: false,
115
+ nonParamsRequest: false,
116
+ }) as ShellSourceState;
117
+ }
118
+
119
+ /** Arm shell-source tracking for one partial-shell prerender (resets counters). */
120
+ export function beginShellSourceTracking(): void {
121
+ const state = shellSourceState();
122
+ state.active = true;
123
+ state.cacheIO = false;
124
+ state.nonParamsRequest = false;
125
+ }
126
+
127
+ /** Read + disarm the dynamic sources observed during the last shell prerender. */
128
+ export function endShellSourceTracking(): { cacheIO: boolean; nonParamsRequest: boolean } {
129
+ const state = shellSourceState();
130
+ const result = { cacheIO: state.cacheIO, nonParamsRequest: state.nonParamsRequest };
131
+ state.active = false;
132
+ return result;
133
+ }
134
+
135
+ /** Record a request-API postpone by name (params is URL-derived, everything else is real request IO). */
136
+ function recordShellRequestApi(api: string): void {
137
+ const state = shellSourceState();
138
+ if (state.active && api !== 'params') state.nonParamsRequest = true;
139
+ }
140
+
141
+ /** Record that cached IO (a `use cache`/fetch/unstable_cache fill) ran during the shell. */
142
+ function recordShellCacheIO(): void {
143
+ const state = shellSourceState();
144
+ if (state.active) state.cacheIO = true;
145
+ }
146
+
147
+ /** Render `callback` in prerender mode so request APIs postpone instead of throwing. */
148
+ export function runInPrerender<T>(
149
+ callback: () => T,
150
+ options: { runtimePrefetch?: boolean; prefetchShell?: boolean; taskBoundary?: boolean } = {},
151
+ ): T {
152
+ const controller = new AbortController();
153
+ activePrerenderControllers().add(controller);
154
+ const scope: PrerenderScope = {
155
+ signal: controller.signal,
156
+ abort: () => controller.abort(),
157
+ ...(options.runtimePrefetch ? { runtimePrefetch: true, awaitedRuntimeApi: false } : {}),
158
+ ...(options.prefetchShell ? { prefetchShell: true } : {}),
159
+ ...(options.taskBoundary ? { taskBoundary: true } : {}),
160
+ };
161
+ return prerenderStorage.run(scope, callback);
162
+ }
163
+
164
+ // The build process force-aborts every prerender AbortController created since
165
+ // the last sweep as a belt-and-braces backstop: completePrerender already aborts
166
+ // each scope on its own exit path, but a scope whose callback wired a timer to
167
+ // React.cacheSignal() (a slow cache component polling `setTimeout(..., { signal:
168
+ // React.cacheSignal() })`) and then escaped in a way that skipped its abort would
169
+ // keep the event loop alive and hang the whole build. Anchored on globalThis for
170
+ // the usual dual-module-copy reason (build driver vs built compat-server bundle).
171
+ const ACTIVE_PRERENDER_CONTROLLERS = Symbol.for('pnext.activePrerenderControllers');
172
+ function activePrerenderControllers(): Set<AbortController> {
173
+ const root = globalThis as Record<PropertyKey, unknown>;
174
+ return (root[ACTIVE_PRERENDER_CONTROLLERS] ??= new Set<AbortController>()) as Set<AbortController>;
175
+ }
176
+
177
+ /**
178
+ * Force-abort (and forget) every prerender AbortController created since the last
179
+ * call. Call once per built route after its shell render returns so no route's
180
+ * stray cacheSignal-bound timer can outlive its render and hang the build.
181
+ */
182
+ export function abortActivePrerenderScopes(): void {
183
+ const controllers = activePrerenderControllers();
184
+ for (const controller of controllers) controller.abort();
185
+ controllers.clear();
186
+ }
187
+
188
+ export function isPrerendering() {
189
+ return prerenderStorage.getStore() !== undefined;
190
+ }
191
+
192
+ /** True inside a runtime-prefetch prerender (see PrerenderScope.runtimePrefetch). */
193
+ export function isRuntimePrefetchPrerender(): boolean {
194
+ return prerenderStorage.getStore()?.runtimePrefetch === true;
195
+ }
196
+
197
+ /** True inside a prefetch-shell prerender (see PrerenderScope.prefetchShell). */
198
+ export function isPrefetchShellPrerender(): boolean {
199
+ return prerenderStorage.getStore()?.prefetchShell === true;
200
+ }
201
+
202
+ /** The active prerender scope (undefined outside a prerender). */
203
+ export function currentPrerenderScope(): PrerenderScope | undefined {
204
+ return prerenderStorage.getStore();
205
+ }
206
+
207
+ /**
208
+ * Complete the active prerender. Called after a shell has been produced: aborts
209
+ * the scope so any hanging promise whose settlement microtask has not yet run
210
+ * stays pending (see hangingPromise's `scope.signal.aborted` guard) rather than
211
+ * rejecting post-shell. Idempotent.
212
+ */
213
+ export function completePrerender(scope: PrerenderScope | undefined): void {
214
+ if (!scope) return;
215
+ scope.abort();
216
+ }
217
+
218
+ // Task-boundary prerender (cacheComponents build parity).
219
+ //
220
+ // Next ends a build prerender at the first TASK boundary: work that settles while the microtask queue
221
+ // drains lands in the static shell, while anything needing a fresh macrotask (setTimeout, setImmediate,
222
+ // real I/O) does NOT - the surrounding <Suspense> keeps its fallback and the content is filled in at request
223
+ // time. Upstream that falls out of React's `prerender` being started in one task and aborted in the next.
224
+ //
225
+ // pnext resolves the server tree itself rather than through React's prerender, so the boundary is modelled
226
+ // per server-component invocation: the component's promise races a deadline that fires once the microtask
227
+ // queue has drained. A microtask-settled component always wins the race; a task-settled one loses and
228
+ // postpones.
229
+ //
230
+ // The drain is counted in microtask turns rather than armed with `setImmediate`, because `setImmediate` is
231
+ // only ordered against the CHECK phase of the loop: a component's already-expired `setTimeout` sitting in
232
+ // the TIMERS phase beats it roughly half the time whenever a loop turn runs long, which baked Suspense
233
+ // content into the static shell instead of postponing. Microtasks cannot be preempted by any timer or I/O
234
+ // callback, so the verdict is the same on a quiet machine and under a saturated CPU.
235
+ //
236
+ // Arming the deadline per invocation, rather than once for the whole render, is what keeps a SIBLING static
237
+ // subtree renderable after another subtree hit the boundary - the parallel-routes case, where the `@slot`
238
+ // becomes a hole but `children` must still bake into the shell.
239
+
240
+ const taskPostponeSymbol = Symbol.for('pnext.taskPostpone');
241
+
242
+ /**
243
+ * A postpone raised by the task boundary rather than by a request API. Escaping
244
+ * every <Suspense> boundary means the route cannot produce a static shell at
245
+ * all, which Next reports as a blocking-prerender build error.
246
+ */
247
+ export function isTaskPostpone(error: unknown): boolean {
248
+ return isPostponeError(error) && taskPostponeSymbol in error;
249
+ }
250
+
251
+ function taskPostponeError(): PostponeError {
252
+ const error = new PostponeError('task');
253
+ Object.defineProperty(error, taskPostponeSymbol, { value: true });
254
+ return error;
255
+ }
256
+
257
+ /** True inside a build prerender running under task-boundary semantics. */
258
+ export function isTaskBoundaryPrerender(): boolean {
259
+ return prerenderStorage.getStore()?.taskBoundary === true;
260
+ }
261
+
262
+ const taskBoundaryArmedStorage = new AsyncLocalStorage<true>();
263
+ const taskBoundarySuspendedStorage = new AsyncLocalStorage<true>();
264
+
265
+ /**
266
+ * Render `callback` with the task boundary ARMED. The renderer arms it only while resolving a <Suspense>
267
+ * subtree, because that is the only place a task postpone can be safely absorbed - the boundary records a
268
+ * hole and keeps its fallback. Task work ABOVE every boundary is left unbounded so it still bakes into the
269
+ * shell, matching Next, where such work escaping the root is a separate blocking-prerender concern.
270
+ */
271
+ export function runWithTaskBoundaryArmed<T>(callback: () => T): T {
272
+ return taskBoundaryArmedStorage.run(true, callback);
273
+ }
274
+
275
+ /**
276
+ * Render `callback` with the task boundary suspended. Used for a `'use cache'` component's subtree: upstream
277
+ * the whole subtree renders inside the cache fill, so its (possibly task-settled) IO belongs in the static
278
+ * shell just like the fill's own - see the module-level `'use cache'` page shape, where the cached component
279
+ * returns immediately and its children do the IO.
280
+ */
281
+ export function runWithTaskBoundarySuspended<T>(callback: () => T): T {
282
+ return taskBoundarySuspendedStorage.run(true, callback);
283
+ }
284
+
285
+ // compat/cache/use-cache tags both shapes of "renders inside a cache scope" with globally-registered
286
+ // symbols, so reading them keeps core free of a compat import: the `'use cache'` wrapper itself, and the
287
+ // render-once memo wrapper it puts on every server component inside a produced (cached) tree.
288
+ const useCacheWrapperSymbol = Symbol.for('pnext.compat.useCacheWrapper');
289
+ const useCacheMemoWrappedSymbol = Symbol.for('pnext.compat.useCacheMemoWrapped');
290
+
291
+ /** Whether `component` renders inside a compat `'use cache'` scope. */
292
+ export function isCachedComponent(component: unknown): boolean {
293
+ if (typeof component !== 'function') return false;
294
+ const tagged = component as unknown as Record<symbol, unknown>;
295
+ return tagged[useCacheWrapperSymbol] === true || tagged[useCacheMemoWrappedSymbol] === true;
296
+ }
297
+
298
+ /**
299
+ * Bound a server-component invocation by the prerender's task boundary. Outside
300
+ * a task-boundary prerender (request renders, non-cacheComponents builds) the
301
+ * value is returned untouched, so this is a no-op on every serving path.
302
+ */
303
+ export function awaitAtTaskBoundary<T>(value: T): T {
304
+ if (!isTaskBoundaryPrerender()) return value;
305
+ // Only armed inside a <Suspense> subtree (see runWithTaskBoundaryArmed), and
306
+ // never inside a `'use cache'` subtree (runWithTaskBoundarySuspended).
307
+ if (taskBoundaryArmedStorage.getStore() !== true) return value;
308
+ if (taskBoundarySuspendedStorage.getStore() === true) return value;
309
+ if (typeof (value as { then?: unknown } | null)?.then !== 'function') return value;
310
+ const deadline = taskDeadline();
311
+ // Cancel on settle so a component that wins the race does not leave its
312
+ // deadline burning through the rest of its microtask budget.
313
+ return Promise.race([value as PromiseLike<unknown>, deadline.promise]).finally(
314
+ deadline.cancel,
315
+ ) as T;
316
+ }
317
+
318
+ // Cache fills are the one kind of task-queue work a build prerender must WAIT for rather than cut off.
319
+ // Upstream this falls out of Next's two-pass model: a warmup render fills every cache,
320
+ // `cacheSignal().cacheReady()` waits for them, and only then does the real prerender run with its
321
+ // task-boundary abort, so by then every cached read settles from memory in a microtask. pnext renders once,
322
+ // so instead the deadline yields while a fill is still in flight and re-arms when the last one settles.
323
+ // Net effect matches: cached IO lands in the shell, uncached task work does not.
324
+ //
325
+ // A fill that never settles must not hang the build (the E236 fill-timeout guard covers 'use cache', but
326
+ // nothing bounds an unstable_cache producer), so the deadline only ever defers to fills a bounded number of
327
+ // times before firing anyway.
328
+ const MAX_CACHE_FILL_DEFERRALS = 50;
329
+
330
+ interface CacheFillState {
331
+ pending: number;
332
+ waiters: (() => void)[];
333
+ }
334
+
335
+ // The build process and the built compat-server bundle load their own copies of
336
+ // this module (same reason revalidate.ts / build-prerender-errors.ts anchor
337
+ // their state), and the cache runtime registering a fill lives in the other
338
+ // copy from the renderer arming the deadline. Anchor the counter on globalThis
339
+ // so both copies share one window.
340
+ const CACHE_FILL_STATE = Symbol.for('pnext.prerenderCacheFills');
341
+
342
+ function cacheFillState(): CacheFillState {
343
+ const root = globalThis as Record<PropertyKey, unknown>;
344
+ return (root[CACHE_FILL_STATE] ??= { pending: 0, waiters: [] }) as CacheFillState;
345
+ }
346
+
347
+ /**
348
+ * Register an in-flight cache fill so a prerender's task boundary waits for it.
349
+ * Cheap enough to run unconditionally (a counter plus one `.then`), which keeps
350
+ * it correct even where the caller's module copy cannot see the prerender ALS.
351
+ * Returns `fill` for chaining.
352
+ */
353
+ export function trackPrerenderCacheFill<T>(fill: Promise<T>): Promise<T> {
354
+ recordShellCacheIO();
355
+ const state = cacheFillState();
356
+ state.pending += 1;
357
+ const settled = () => {
358
+ state.pending -= 1;
359
+ if (state.pending > 0) return;
360
+ const waiters = state.waiters;
361
+ state.waiters = [];
362
+ for (const waiter of waiters) waiter();
363
+ };
364
+ fill.then(settled, settled);
365
+ return fill;
366
+ }
367
+
368
+ // How many microtask turns count as "the queue drained". Generous enough that no
369
+ // realistic chain of already-resolved awaits (in this component or in the
370
+ // siblings interleaving with it) runs out of budget, and still free: the whole
371
+ // drain happens inside one event-loop turn, and it is cancelled the moment the
372
+ // component it guards settles.
373
+ const MICROTASK_DRAIN_TURNS = 1000;
374
+
375
+ function taskDeadline(): { promise: Promise<never>; cancel: () => void } {
376
+ let cancelled = false;
377
+ const promise = new Promise<never>((_resolve, reject) => {
378
+ let deferrals = 0;
379
+ const drain = (remaining: number) => {
380
+ if (cancelled) return;
381
+ if (remaining === 0) {
382
+ const state = cacheFillState();
383
+ if (state.pending > 0 && deferrals < MAX_CACHE_FILL_DEFERRALS) {
384
+ // Cache fills are the one kind of task work the boundary waits for:
385
+ // yield until the last one settles, then drain again.
386
+ deferrals += 1;
387
+ state.waiters.push(() => drain(MICROTASK_DRAIN_TURNS));
388
+ return;
389
+ }
390
+ reject(taskPostponeError());
391
+ return;
392
+ }
393
+ // Yield through BOTH pre-macrotask queues each turn: the nextTick queue
394
+ // ahead of us runs first (a component awaiting `process.nextTick` must
395
+ // still land in the shell), then a microtask hop. Neither queue can be
396
+ // preempted by a timer or an I/O callback, which is what makes the verdict
397
+ // load-independent.
398
+ const next = () => void Promise.resolve().then(() => drain(remaining - 1));
399
+ if (typeof process?.nextTick === 'function') process.nextTick(next);
400
+ else next();
401
+ };
402
+ drain(MICROTASK_DRAIN_TURNS);
403
+ });
404
+ // The losing copy of a raced deadline must never reach the unhandled-rejection
405
+ // channel (the race attaches its own handler to the winning one).
406
+ promise.catch(() => undefined);
407
+ return {
408
+ promise,
409
+ cancel: () => {
410
+ cancelled = true;
411
+ },
412
+ };
413
+ }
414
+
415
+ /**
416
+ * A promise that never resolves during prerender. Its settlement depends on how the render observes it,
417
+ * matching Next's two cacheComponents contracts:
418
+ * - AWAITED (a `.then` with a fulfillment handler - the dynamic-at-await-site signal): rejects with
419
+ * PostponeError so the await throws and the nearest <Suspense> records a hole.
420
+ * - Only observed for rejection, or passed around and never awaited - the "stash the promise and observe
421
+ * its rejection later" pattern: rejects instead with the "During prerendering, <api> rejects when the
422
+ * prerender is complete" message so a subscribed rejection handler observes it.
423
+ * Either way the internal `.catch` below keeps an un-subscribed copy off the unhandled-rejection channel.
424
+ */
425
+ /**
426
+ * The Error a hanging request-API promise rejects with once the prerender completes. Next names the API in
427
+ * backticks and its Node runtime prints the error as `Error: <message>`. Bun's console inspector would
428
+ * otherwise render an Error as lowercase `error:` plus a code frame, so a Node-style `util.inspect` custom
429
+ * hook returns the canonical form - the request-apis e2e suite substring-matches exactly that.
430
+ */
431
+ function prerenderCompleteError(api: string): Error {
432
+ const message = `During prerendering, \`${api}\` rejects when the prerender is complete.`;
433
+ const error = new Error(message);
434
+ Object.defineProperty(error, Symbol.for('nodejs.util.inspect.custom'), {
435
+ value: () => {
436
+ const stackTail = (error.stack ?? '').split('\n').slice(1).join('\n');
437
+ return stackTail ? `Error: ${message}\n${stackTail}` : `Error: ${message}`;
438
+ },
439
+ enumerable: false,
440
+ configurable: true,
441
+ });
442
+ return error;
443
+ }
444
+
445
+ /**
446
+ * Tag read off a hanging promise's Proxy. A consumer that only wants to KNOW a promise is a hanging
447
+ * request-API promise, rather than consume its value, must be able to ask without marking the prerender
448
+ * dynamic - so the tag arm sits ahead of the then/catch/finally arms in the `get` trap.
449
+ */
450
+ const HANGING_PROMISE = Symbol.for('pnext.hangingPromise');
451
+
452
+ /**
453
+ * True for the promise `hangingPromise` returns. Used by the island-props
454
+ * serializer: awaiting a fallback-shell `params`/`searchParams` promise on the
455
+ * way to a client component would postpone OUTSIDE every <Suspense> and destroy
456
+ * the whole partial shell, when the island's props are not the consumer that
457
+ * should postpone anything.
458
+ */
459
+ export function isHangingPromise(value: unknown): boolean {
460
+ return (
461
+ typeof value === 'object' &&
462
+ value !== null &&
463
+ (value as Record<symbol, unknown>)[HANGING_PROMISE] === true
464
+ );
465
+ }
466
+
467
+ export function hangingPromise<T = never>(api: string): Promise<T> {
468
+ const scope = prerenderStorage.getStore();
469
+ // Outside a prerender scope this should not be called; return a caught
470
+ // rejection defensively so a stray await surfaces rather than hangs forever.
471
+ if (!scope) {
472
+ const stray = Promise.reject<T>(new PostponeError(api));
473
+ stray.catch(() => undefined);
474
+ return stray;
475
+ }
476
+
477
+ // The promise settles exactly once, via whichever observation happens first: a consumer awaiting it (a
478
+ // `.then` with a fulfillment handler) rejects with PostponeError so the await throws and the boundary
479
+ // records a hole; the prerender completing (scope abort) rejects with the "rejects when the prerender is
480
+ // complete" Error so stashed-promise subscribers observe it. It must stay PENDING until one of those
481
+ // happens - settling on a timer or microtask races late awaiters into the wrong rejection.
482
+ let settle: ((error: Error) => void) | undefined;
483
+ const promise = new Promise<T>((_resolve, reject) => {
484
+ settle = (error: Error) => reject(error);
485
+ });
486
+ const rejectAwaited = () => settle?.(new PostponeError(api));
487
+ if (scope.signal.aborted) {
488
+ settle?.(prerenderCompleteError(api));
489
+ } else {
490
+ scope.signal.addEventListener('abort', () => settle?.(prerenderCompleteError(api)), {
491
+ once: true,
492
+ });
493
+ }
494
+ // Swallow the rejection for the un-subscribed copy so a hanging promise nobody
495
+ // observed never surfaces as an unhandled rejection; an actual awaiter/handler
496
+ // still observes the throw (it subscribes to the same promise).
497
+ promise.catch(() => undefined);
498
+ return new Proxy(promise, {
499
+ get(target, property) {
500
+ // Before every observation arm: reading the tag must not mark dynamic.
501
+ if (property === HANGING_PROMISE) return true;
502
+ if (property === 'then') {
503
+ return (...args: Parameters<typeof target.then>) => {
504
+ // A fulfillment handler means the value is being consumed (an await or
505
+ // value-taking `.then`): mark the boundary dynamic and postpone.
506
+ if (typeof args[0] === 'function') rejectAwaited();
507
+ markPrerenderDynamic();
508
+ recordShellRequestApi(api);
509
+ return target.then(...args);
510
+ };
511
+ }
512
+ if (property === 'catch') {
513
+ return (...args: Parameters<typeof target.catch>) => {
514
+ markPrerenderDynamic();
515
+ recordShellRequestApi(api);
516
+ return target.catch(...args);
517
+ };
518
+ }
519
+ if (property === 'finally') {
520
+ return (...args: Parameters<typeof target.finally>) => {
521
+ markPrerenderDynamic();
522
+ recordShellRequestApi(api);
523
+ return target.finally(...args);
524
+ };
525
+ }
526
+ const value: unknown = Reflect.get(target, property, target);
527
+ return value;
528
+ },
529
+ });
530
+ }
531
+
532
+ // ---------------------------------------------------------------------------
533
+ // Global cacheComponents flag (Stage C).
534
+ //
535
+ // Next 16 gates PPR-by-default on `nextConfig.cacheComponents: true` rather
536
+ // than the per-route `experimental_ppr` opt-in. Core owns the flag storage
537
+ // (a boolean cell); compat sets it from getNextConfig() during registration.
538
+ // The renderer/routing/build read it to decide whether every route is a PPR
539
+ // candidate and whether request APIs hang.
540
+ // ---------------------------------------------------------------------------
541
+
542
+ let cacheComponentsEnabled = false;
543
+
544
+ export function setCacheComponents(enabled: boolean): void {
545
+ cacheComponentsEnabled = enabled;
546
+ }
547
+
548
+ export function cacheComponents(): boolean {
549
+ return cacheComponentsEnabled;
550
+ }
551
+
552
+ // ---------------------------------------------------------------------------
553
+ // Prerender determinism (Stage C-5).
554
+ //
555
+ // During a build prerender, incidental Math.random()/Date.now()/new Date() must
556
+ // not vary or fail the build. We patch them to deterministic values for the
557
+ // duration of a build prerender pass and restore afterwards. Build-only and
558
+ // single-route-at-a-time, so global patching is safe; at request time the real
559
+ // implementations run (random varies across requests). Applied only under
560
+ // cacheComponents so the legacy path is byte-identical.
561
+ // ---------------------------------------------------------------------------
562
+
563
+ // Real wall-clock that survives the determinism patch below. Cache bookkeeping
564
+ // (use-cache storedAt) must never be stamped with the frozen build clock — a
565
+ // `storedAt: 0` entry reads as perpetually expired at runtime.
566
+ const REAL_NOW = Symbol.for('pnext.realNow');
567
+ export function realNow(): number {
568
+ const stored = (globalThis as Record<PropertyKey, unknown>)[REAL_NOW];
569
+ return typeof stored === 'function' ? (stored as () => number)() : Date.now();
570
+ }
571
+
572
+ // A `use cache` body is allowed to be non-deterministic - its output is what gets cached, and Next's
573
+ // work-unit store inside a cache scope is `cache`, not `prerender-runtime`, so
574
+ // abortOnSynchronousPlatformIOAccess never applies there. The scope ALS is read off its globalThis symbol
575
+ // rather than imported: ppr.ts is core and must not depend on compat/cache.
576
+ const USE_CACHE_SCOPE_STORAGE = Symbol.for('pnext.compat.useCacheScopeStorage');
577
+
578
+ function insideUseCacheScope(): boolean {
579
+ const storage = (globalThis as Record<PropertyKey, unknown>)[USE_CACHE_SCOPE_STORAGE] as
580
+ | { getStore(): unknown }
581
+ | undefined;
582
+ return storage?.getStore() !== undefined;
583
+ }
584
+
585
+ export async function runWithPrerenderDeterminism<T>(fn: () => Promise<T>): Promise<T> {
586
+ if (!cacheComponentsEnabled) return fn();
587
+ const deterministicTime = 1;
588
+ const realRandom = Math.random;
589
+ const RealDate = Date;
590
+ (globalThis as Record<PropertyKey, unknown>)[REAL_NOW] = RealDate.now.bind(RealDate);
591
+ let counter = 0;
592
+ Math.random = () => {
593
+ counter = (counter * 1103515245 + 12345) & 0x7fffffff;
594
+ return (counter % 1000000) / 1000000;
595
+ };
596
+ const PatchedDate = function (this: unknown, ...args: unknown[]) {
597
+ if (!new.target) return new RealDate(deterministicTime).toString();
598
+ if (args.length === 0) return new RealDate(deterministicTime);
599
+ return Reflect.construct(RealDate, args) as Date;
600
+ } as unknown as DateConstructor;
601
+ Object.setPrototypeOf(PatchedDate, RealDate);
602
+ Object.defineProperty(PatchedDate, 'prototype', { value: RealDate.prototype });
603
+ PatchedDate.now = () => deterministicTime;
604
+ // eslint-disable-next-line no-global-assign
605
+ Date = PatchedDate;
606
+ try {
607
+ return await fn();
608
+ } finally {
609
+ Math.random = realRandom;
610
+ // eslint-disable-next-line no-global-assign
611
+ Date = RealDate;
612
+ }
613
+ }
614
+
615
+ // Sync-IO abort during a runtime-prefetch prerender.
616
+ //
617
+ // A runtime prefetch renders against a REAL sampled request, so cookies()/headers()/params/`use cache`
618
+ // resolve - which means sync IO hidden BEHIND one of them (the classic `await cookies(); return
619
+ // <div>{Date.now()}</div>`) is only reachable at runtime, never at build time where validation would catch
620
+ // it. Next handles it with abortOnSynchronousPlatformIOAccess: once a runtime API has resolved, the next
621
+ // synchronous platform IO call aborts the boundary, silently, because the value is simply left for the
622
+ // navigation to render.
623
+ //
624
+ // pnext mirrors that by shimming the platform globals for the duration of the runtime-prefetch render and
625
+ // throwing a postpone (exactly what connection() does) when the scope's `awaitedRuntimeApi` flag is set.
626
+ // The shims are gated on the prerender ALS, so a concurrent request render on the same server sees the real
627
+ // implementations.
628
+
629
+ /**
630
+ * Mark a runtime API's promise so AWAITING it arms the sync-IO abort for the rest of the runtime-prefetch
631
+ * render. Outside a runtime-prefetch prerender the promise is returned untouched.
632
+ *
633
+ * Await-precise rather than creation-precise: merely creating (or passing around) `cookies()` must not arm
634
+ * the abort - the same dynamic-at-await-site rule hangingPromise implements, and via the same Proxy trick,
635
+ * since `await` on a native promise bypasses a monkey-patched `then`.
636
+ */
637
+ export function markRuntimeApiOnAwait<T>(promise: Promise<T>): Promise<T> {
638
+ const scope = prerenderStorage.getStore();
639
+ if (scope?.runtimePrefetch !== true) return promise;
640
+ return new Proxy(promise, {
641
+ get(target, property) {
642
+ if (property === 'then') {
643
+ return (...args: Parameters<typeof target.then>) => {
644
+ // A fulfillment handler means the value is being consumed (an await or
645
+ // a value-taking `.then`) — the point Next considers the runtime API
646
+ // resolved into the render.
647
+ if (typeof args[0] === 'function') scope.awaitedRuntimeApi = true;
648
+ return target.then(...args);
649
+ };
650
+ }
651
+ return Reflect.get(target, property, target) as unknown;
652
+ },
653
+ });
654
+ }
655
+
656
+ /** Throw the boundary-aborting postpone when sync IO happens post-runtime-API. */
657
+ function abortOnSynchronousPlatformIOAccess(): void {
658
+ const scope = prerenderStorage.getStore();
659
+ if (scope?.runtimePrefetch !== true || scope.awaitedRuntimeApi !== true) return;
660
+ // Sync IO inside a `use cache` body is legal — see insideUseCacheScope above.
661
+ if (insideUseCacheScope()) return;
662
+ throw new PostponeError('sync IO');
663
+ }
664
+
665
+ /** Install `value` as an own property of `owner`, returning a restore thunk. */
666
+ function patchMethod(owner: object, key: string, value: unknown): () => void {
667
+ const hadOwn = Object.prototype.hasOwnProperty.call(owner, key);
668
+ const descriptor = Object.getOwnPropertyDescriptor(owner, key);
669
+ try {
670
+ Object.defineProperty(owner, key, {
671
+ value,
672
+ configurable: true,
673
+ writable: true,
674
+ enumerable: descriptor?.enumerable ?? false,
675
+ });
676
+ } catch {
677
+ return () => undefined;
678
+ }
679
+ return () => {
680
+ try {
681
+ if (hadOwn && descriptor) Object.defineProperty(owner, key, descriptor);
682
+ else delete (owner as Record<string, unknown>)[key];
683
+ } catch {
684
+ /* nothing sensible to do if the global refuses to be restored */
685
+ }
686
+ };
687
+ }
688
+
689
+ // Two runtime prefetches can be in flight at once, so installation is
690
+ // refcounted: the shims go in on 0→1 and come out on 1→0. Anchored on
691
+ // globalThis for the usual dual-module-copy reason.
692
+ const SYNC_IO_SHIMS = Symbol.for('pnext.runtimePrefetchSyncIoShims');
693
+ interface SyncIoShimState {
694
+ depth: number;
695
+ restore: (() => void)[];
696
+ }
697
+ function syncIoShimState(): SyncIoShimState {
698
+ const root = globalThis as Record<PropertyKey, unknown>;
699
+ return (root[SYNC_IO_SHIMS] ??= { depth: 0, restore: [] }) as SyncIoShimState;
700
+ }
701
+
702
+ function installSyncIoShims(): void {
703
+ const state = syncIoShimState();
704
+ if (++state.depth > 1) return;
705
+ const root = globalThis as Record<PropertyKey, unknown>;
706
+ const RealDate = Date;
707
+ const realRandom = Math.random;
708
+ // realNow() must keep reporting the true wall clock while Date.now is shimmed
709
+ // — cache bookkeeping (storedAt/expiry) reads it during the render.
710
+ const previousRealNow = root[REAL_NOW];
711
+ root[REAL_NOW] = RealDate.now.bind(RealDate);
712
+ const PatchedDate = function (this: unknown, ...args: unknown[]) {
713
+ if (!new.target) {
714
+ abortOnSynchronousPlatformIOAccess();
715
+ return new RealDate().toString();
716
+ }
717
+ if (args.length === 0) {
718
+ abortOnSynchronousPlatformIOAccess();
719
+ return new RealDate();
720
+ }
721
+ return Reflect.construct(RealDate, args) as Date;
722
+ } as unknown as DateConstructor;
723
+ Object.setPrototypeOf(PatchedDate, RealDate);
724
+ Object.defineProperty(PatchedDate, 'prototype', { value: RealDate.prototype });
725
+ PatchedDate.now = () => {
726
+ abortOnSynchronousPlatformIOAccess();
727
+ return RealDate.now();
728
+ };
729
+ // eslint-disable-next-line no-global-assign
730
+ Date = PatchedDate;
731
+ Math.random = () => {
732
+ abortOnSynchronousPlatformIOAccess();
733
+ return realRandom();
734
+ };
735
+ state.restore.push(() => {
736
+ // eslint-disable-next-line no-global-assign
737
+ Date = RealDate;
738
+ Math.random = realRandom;
739
+ root[REAL_NOW] = previousRealNow;
740
+ });
741
+ const perf = globalThis.performance as Performance | undefined;
742
+ if (perf) {
743
+ const realPerfNow = perf.now.bind(perf);
744
+ state.restore.push(
745
+ patchMethod(perf, 'now', () => {
746
+ abortOnSynchronousPlatformIOAccess();
747
+ return realPerfNow();
748
+ }),
749
+ );
750
+ }
751
+ const webCrypto = globalThis.crypto as Crypto | undefined;
752
+ if (typeof webCrypto?.getRandomValues === 'function') {
753
+ const realGetRandomValues = webCrypto.getRandomValues.bind(webCrypto);
754
+ state.restore.push(
755
+ patchMethod(webCrypto, 'getRandomValues', (array: ArrayBufferView) => {
756
+ abortOnSynchronousPlatformIOAccess();
757
+ return realGetRandomValues(array as never);
758
+ }),
759
+ );
760
+ }
761
+ }
762
+
763
+ function restoreSyncIoShims(): void {
764
+ const state = syncIoShimState();
765
+ if (--state.depth > 0) return;
766
+ state.depth = 0;
767
+ const restore = state.restore;
768
+ state.restore = [];
769
+ for (const undo of restore) undo();
770
+ }
771
+
772
+ /**
773
+ * Run a runtime-prefetch render with the sync-IO shims installed. Sync IO that
774
+ * happens once a runtime API has been awaited (see markRuntimeApiOnAwait)
775
+ * postpones its nearest <Suspense> boundary instead of returning a value.
776
+ */
777
+ export async function runWithRuntimePrefetchSyncIoAbort<T>(fn: () => Promise<T>): Promise<T> {
778
+ installSyncIoShims();
779
+ try {
780
+ return await fn();
781
+ } finally {
782
+ restoreSyncIoShims();
783
+ }
784
+ }