@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
@@ -0,0 +1,602 @@
1
+ import { isNotFoundError, isRedirectError, type PNextRedirectError } from '../../api/navigation';
2
+ import { runWithRequest, setPhase } from '../../request/context';
3
+ import { toNextRequest } from '../../api/server';
4
+ import { revalidationEventCount } from '../cache/revalidate';
5
+ import { serializeBoundaryError } from '../lifecycle/error-serialize';
6
+ import { lookupAction } from './registry';
7
+ import { instanceActionRouteId, isInstanceActionId, lookupActionInstance } from './instances';
8
+ import { inlineActionModuleKey, isInlineActionId } from './rewrite';
9
+ import {
10
+ ACTION_STREAM_CONTENT_TYPE,
11
+ createTempRefScope,
12
+ extractTempRefs,
13
+ isReadableStreamReturn,
14
+ serializeReturnWithTempRefs,
15
+ } from '../misc/action-return';
16
+ import {
17
+ ACTION_ARGS_FIELD,
18
+ ACTION_ID_FIELD,
19
+ FORM_STATE_FIELD,
20
+ ACTION_ID_HEADER,
21
+ ACTION_NOT_FOUND_HEADER,
22
+ NEXT_ACTION_NOT_FOUND_HEADER,
23
+ ACTION_REDIRECT_HEADER,
24
+ ACTION_REFRESH_HEADER,
25
+ NEXT_ACTION_REVALIDATED_HEADER,
26
+ ARG_ACTION_MARKER,
27
+ ARG_FILE_MARKER,
28
+ ARG_FILE_PREFIX,
29
+ ARG_FORMDATA_MARKER,
30
+ ARG_FORMDATA_PREFIX,
31
+ REDIRECT_TYPE_HEADER,
32
+ ACTION_ERROR_DIGEST_HEADER,
33
+ MASKED_ACTION_ERROR,
34
+ MAX_ACTION_ARGS,
35
+ tooManyArgsMessage,
36
+ type ActionSuccessBody,
37
+ } from './protocol';
38
+
39
+ /**
40
+ * Framework-agnostic runtime handler for a server-action RPC. It reads the action id header, decodes
41
+ * the arguments (JSON args as text/plain, or multipart/urlencoded FormData for form submissions),
42
+ * resolves the action via the module registry or the live-instance registry, and invokes it inside a
43
+ * request scope so headers()/cookies() work and cookie mutations propagate to the response.
44
+ * redirect()/notFound() thrown by the action are translated to the right HTTP responses; other throws
45
+ * become masked (prod) or detailed (dev) text/plain errors; successes become { data } JSON.
46
+ *
47
+ * This module never imports build/dev machinery - `importModule` and `rerenderRoute` are the only
48
+ * bridges to real module loading/rendering, which keeps the endpoint portable across runtimes.
49
+ */
50
+ export interface RunActionOptions {
51
+ /** Load an action module by its registered modulePath. Injected by the caller. */
52
+ importModule: (modulePath: string) => Promise<Record<string, unknown>>;
53
+ /**
54
+ * Re-render a route server-side to re-register live inline-action instances
55
+ * (see instances.ts) after a process restart. Optional; without it a cold
56
+ * instance id fails with unknown-action.
57
+ */
58
+ rerenderRoute?: (routeId: string, request: Request) => Promise<void>;
59
+ /**
60
+ * Import an app module by its workspace-relative source key so its
61
+ * compile-injected inline-action registrations run (cold 'if:' id lookups).
62
+ */
63
+ importSourceModule?: (moduleKey: string) => Promise<void>;
64
+ /** Dev mode: error bodies carry real messages instead of the masked text. */
65
+ dev?: boolean;
66
+ /** Body size limit in bytes (Next default 1mb). */
67
+ maxBodyBytes?: number;
68
+ /**
69
+ * Report a thrown action error to the app's onRequestError instrumentation.
70
+ * Injected by the caller (compat glue) so this module stays portable; called
71
+ * once for a real (non control-flow) throw before the masked 500 is returned.
72
+ */
73
+ onError?: (error: unknown, request: Request) => void | Promise<void>;
74
+ /**
75
+ * Server-render a React element tree an action RETURNED to island wire HTML
76
+ * (the JSON return serializer cannot carry vnode Symbols). Injected by the
77
+ * caller (register-actions) so this module never imports the renderer; when
78
+ * absent, element returns fall through the JSON serializer unchanged.
79
+ */
80
+ renderActionElement?: (element: unknown) => Promise<string>;
81
+ }
82
+
83
+ export const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
84
+
85
+ /**
86
+ * Server-side fallback for client-action stubs. A `'use server'` function imported into a client
87
+ * component compiles, in the CLIENT bundle, to a stub that dispatches over `window.__PNEXT_ACTIONS__`.
88
+ * That same bundle server-renders the client component, so when such an action is *called* on the
89
+ * server the stub has no window and would throw. Expose the endpoint's own id-to-module resolution on
90
+ * a global the stub can reach, so it instead runs the real compiled action, inheriting the request
91
+ * scope already active around it.
92
+ */
93
+ function installServerActionDispatch(
94
+ importModule: (modulePath: string) => Promise<Record<string, unknown>>,
95
+ ): void {
96
+ (
97
+ globalThis as {
98
+ __PNEXT_SERVER_ACTIONS__?: { call(id: string, args: unknown[]): Promise<unknown> };
99
+ }
100
+ ).__PNEXT_SERVER_ACTIONS__ = {
101
+ async call(dispatchId: string, dispatchArgs: unknown[]): Promise<unknown> {
102
+ const target = lookupAction(dispatchId);
103
+ if (!target) {
104
+ throw new Error(
105
+ `Failed to find Server Action "${dispatchId}". This request might be from an older or newer deployment.`,
106
+ );
107
+ }
108
+ const module = await importModule(target.modulePath);
109
+ const fn = module[target.exportName];
110
+ if (typeof fn !== 'function') {
111
+ throw new Error(`Failed to find Server Action "${dispatchId}".`);
112
+ }
113
+ return (fn as (...a: unknown[]) => unknown)(...dispatchArgs);
114
+ },
115
+ };
116
+ }
117
+
118
+ export async function runAction(request: Request, options: RunActionOptions): Promise<Response> {
119
+ const id = request.headers.get(ACTION_ID_HEADER);
120
+ if (!id) {
121
+ return errorResponse(`Missing ${ACTION_ID_HEADER} header`, 400);
122
+ }
123
+ // Arm the server-side stub dispatcher for this request's module loader before
124
+ // resolving/running the action (a client-imported action may re-render the
125
+ // route, whose SSR captures client stubs that dispatch back through here).
126
+ installServerActionDispatch(options.importModule);
127
+
128
+ // A resolution failure (a compiled action module that fails to import, a route re-render that
129
+ // throws) must surface as the recognizable 404 not-found wire - never an unhandled 500 the client
130
+ // reads as "unexpected response". A forwarded action dispatched from another page hits the same
131
+ // global resolution and needs the same graceful degradation.
132
+ let action: ((...args: unknown[]) => unknown) | undefined;
133
+ try {
134
+ action = await resolveAction(id, request, options);
135
+ } catch (error) {
136
+ console.error(error);
137
+ action = undefined;
138
+ }
139
+ if (!action) {
140
+ // Wire + log shape asserted by the actions-unrecognized suite: the log is
141
+ // the two-line "Failed to find" text; the RPC body is the exact message
142
+ // React's flight client throws for a missing action (unmasked even in
143
+ // prod — version skew is actionable for users).
144
+ console.error(
145
+ `Failed to find Server Action "${id}". This request might be from an older or newer deployment.\n` +
146
+ 'Read more: https://nextjs.org/docs/messages/failed-to-find-server-action',
147
+ );
148
+ return new Response(`Server Action "${id}" was not found on the server.`, {
149
+ status: 404,
150
+ headers: {
151
+ 'content-type': 'text/plain; charset=utf-8',
152
+ [NEXT_ACTION_NOT_FOUND_HEADER]: '1',
153
+ },
154
+ });
155
+ }
156
+
157
+ const limit = options.maxBodyBytes ?? DEFAULT_BODY_LIMIT_BYTES;
158
+ let args: unknown[];
159
+ try {
160
+ args = await decodeArgs(request, limit);
161
+ } catch (error) {
162
+ if (error instanceof BodyLimitError) {
163
+ console.error(`Error: ${error.message}`);
164
+ console.error(
165
+ 'To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit',
166
+ );
167
+ return errorResponse(options.dev ? error.message : MASKED_ACTION_ERROR, 500);
168
+ }
169
+ return errorResponse(`Invalid action payload: ${messageOf(error)}`, 400);
170
+ }
171
+
172
+ try {
173
+ args = await reviveActionRefs(args, id => resolveAction(id, request, options));
174
+ } catch (error) {
175
+ if (error instanceof UnknownActionReferenceError) {
176
+ return errorResponse(error.message, 404);
177
+ }
178
+ return errorResponse(`Invalid action payload: ${messageOf(error)}`, 400);
179
+ }
180
+
181
+ if (args.length > MAX_ACTION_ARGS) {
182
+ const message = tooManyArgsMessage(args.length);
183
+ console.error(`Error: ${message}`);
184
+ return errorResponse(options.dev ? message : MASKED_ACTION_ERROR, 500);
185
+ }
186
+
187
+ // Temporary-reference scope: strip the client's temp-ref tags off the decoded
188
+ // args (so the action sees clean objects) and remember object -> id, so a
189
+ // returned arg object serializes back to the identical client reference.
190
+ const tempRefs = createTempRefScope();
191
+ args = args.map(arg => extractTempRefs(arg, tempRefs));
192
+
193
+ // Run inside a request scope with a response-headers channel: headers() and
194
+ // cookies() read the RPC request (which carries the browser's live cookies),
195
+ // and cookies().set()/delete() append set-cookie onto the channel.
196
+ const responseHeaders = new Headers();
197
+ const revalidationsBefore = revalidationEventCount();
198
+ const nextRequest = toNextRequest(request);
199
+ try {
200
+ // The action body runs under the 'action' phase so cookies().set() is legal
201
+ // here (and illegal once the phase flips to 'render'/'after' — phase-changes
202
+ // suite). Restored to 'render' once the action settles so a follow-up
203
+ // redirect-target render is treated as a render.
204
+ setPhase('action');
205
+ const data = await runWithRequest(
206
+ nextRequest,
207
+ () => Promise.resolve(action(...args)),
208
+ {},
209
+ { responseHeaders },
210
+ ).finally(() => setPhase('render'));
211
+ const refresh =
212
+ revalidationEventCount() !== revalidationsBefore || hasSetCookie(responseHeaders);
213
+ // A ReadableStream return streams the raw bytes to the client unbuffered
214
+ // (actions-streaming): JSON can't carry a stream, so the body IS the stream
215
+ // under a marker content-type the client detects.
216
+ if (isReadableStreamReturn(data)) {
217
+ const headers = new Headers({ 'content-type': ACTION_STREAM_CONTENT_TYPE });
218
+ setRefreshHeaders(headers, refresh);
219
+ return withResponseHeaders(new Response(data, { status: 200, headers }), responseHeaders);
220
+ }
221
+ const serialized = await serializeReturnWithTempRefs(
222
+ data,
223
+ tempRefs,
224
+ options.renderActionElement,
225
+ );
226
+ const body: ActionSuccessBody = {
227
+ data: encodeBinaryReturn(serialized),
228
+ };
229
+ const headers = new Headers({ 'content-type': 'application/json; charset=utf-8' });
230
+ setRefreshHeaders(headers, refresh);
231
+ return withResponseHeaders(
232
+ new Response(JSON.stringify(body), { status: 200, headers }),
233
+ responseHeaders,
234
+ );
235
+ } catch (error) {
236
+ if (isRedirectError(error)) {
237
+ return withResponseHeaders(
238
+ redirectResponse(
239
+ error,
240
+ revalidationEventCount() !== revalidationsBefore || hasSetCookie(responseHeaders),
241
+ ),
242
+ responseHeaders,
243
+ );
244
+ }
245
+ if (isNotFoundError(error)) {
246
+ return withResponseHeaders(
247
+ new Response('Not found', {
248
+ status: 404,
249
+ headers: {
250
+ 'content-type': 'text/plain; charset=utf-8',
251
+ [ACTION_NOT_FOUND_HEADER]: '1',
252
+ },
253
+ }),
254
+ responseHeaders,
255
+ );
256
+ }
257
+ const message = messageOf(error);
258
+ // Compute the boundary digest with the same deterministic scheme the render
259
+ // path uses (error-serialize.ts) so the client-visible digest and the server
260
+ // log agree. Next's prod server log prints the ORIGINAL error trace followed
261
+ // by `digest: '<digest>'` so operators can correlate the redacted client
262
+ // digest with the full server-side stack.
263
+ let digest: string | undefined;
264
+ if (error instanceof Error) {
265
+ digest = serializeBoundaryError({ error, dev: Boolean(options.dev) }).digest;
266
+ const trace = error.stack ?? error.message;
267
+ console.error(typeof digest === 'string' ? `${trace}\n digest: '${digest}'` : trace);
268
+ } else {
269
+ console.error(error);
270
+ }
271
+ await options.onError?.(error, request);
272
+ const masked = errorResponse(options.dev ? message : MASKED_ACTION_ERROR, 500);
273
+ if (typeof digest === 'string') masked.headers.set(ACTION_ERROR_DIGEST_HEADER, digest);
274
+ return withResponseHeaders(masked, responseHeaders);
275
+ }
276
+ }
277
+
278
+ async function resolveAction(
279
+ id: string,
280
+ request: Request,
281
+ options: RunActionOptions,
282
+ ): Promise<((...args: unknown[]) => unknown) | undefined> {
283
+ if (isInlineActionId(id)) {
284
+ let instance = lookupActionInstance(id);
285
+ if (!instance && options.importSourceModule) {
286
+ const moduleKey = inlineActionModuleKey(id);
287
+ if (moduleKey) {
288
+ try {
289
+ await options.importSourceModule(moduleKey);
290
+ } catch {
291
+ // fall through to the unknown-action error
292
+ }
293
+ instance = lookupActionInstance(id);
294
+ }
295
+ }
296
+ return instance;
297
+ }
298
+ if (isInstanceActionId(id)) {
299
+ let instance = lookupActionInstance(id);
300
+ if (!instance && options.rerenderRoute) {
301
+ const routeId = instanceActionRouteId(id);
302
+ if (routeId) {
303
+ try {
304
+ await options.rerenderRoute(routeId, request);
305
+ } catch {
306
+ // fall through to the unknown-action error below
307
+ }
308
+ instance = lookupActionInstance(id);
309
+ }
310
+ }
311
+ return instance;
312
+ }
313
+
314
+ const target = lookupAction(id);
315
+ if (!target) return undefined;
316
+ const module = await options.importModule(target.modulePath);
317
+ const action = module[target.exportName];
318
+ return typeof action === 'function' ? (action as (...args: unknown[]) => unknown) : undefined;
319
+ }
320
+
321
+ class BodyLimitError extends Error {}
322
+
323
+ /**
324
+ * Drain the request body ONCE into bytes and enforce the size limit. All decoding then happens off
325
+ * this buffer: the text path decodes the bytes directly (a single whole-buffer UTF-8 decode, so
326
+ * multi-byte characters can never split across chunk boundaries), and the multipart path parses a
327
+ * reconstructed Request whose stale content-length header is dropped - the upstream request may have
328
+ * been rewrapped by forwarding/middleware bridges, and a mismatched declared length must not truncate
329
+ * the buffered body.
330
+ */
331
+ async function readBodyBytesWithLimit(request: Request, limit: number): Promise<ArrayBuffer> {
332
+ const bytes = await request.arrayBuffer();
333
+ if (bytes.byteLength > limit) {
334
+ throw new BodyLimitError(`Body exceeded ${formatLimit(limit)} limit`);
335
+ }
336
+ return bytes;
337
+ }
338
+
339
+ function bufferedFormRequest(request: Request, bytes: ArrayBuffer): Request {
340
+ const headers = new Headers(request.headers);
341
+ headers.delete('content-length');
342
+ return new Request(request.url, {
343
+ method: request.method,
344
+ headers,
345
+ body: bytes.byteLength === 0 ? undefined : bytes,
346
+ });
347
+ }
348
+
349
+ function formatLimit(limit: number) {
350
+ if (limit % (1024 * 1024) === 0) return `${limit / (1024 * 1024)}mb`;
351
+ if (limit % 1024 === 0) return `${limit / 1024}kb`;
352
+ return `${limit} bytes`;
353
+ }
354
+
355
+ /**
356
+ * Decode the request body into an argument array.
357
+ * - multipart/form-data or urlencoded without ACTION_ARGS_FIELD -> a single
358
+ * FormData argument (form submissions; the action id field is stripped).
359
+ * - multipart with ACTION_ARGS_FIELD -> the JSON arg list with FormData/File
360
+ * slots re-materialized from the other parts (bound args ahead of a form).
361
+ * - anything else (the client runtime sends text/plain) -> JSON args array.
362
+ * - empty body -> no args.
363
+ */
364
+ async function decodeArgs(request: Request, limit: number): Promise<unknown[]> {
365
+ const bytes = await readBodyBytesWithLimit(request, limit);
366
+ const contentType = request.headers.get('content-type') ?? '';
367
+
368
+ if (contentType.includes('multipart/form-data') || contentType.includes('form-urlencoded')) {
369
+ const form = await bufferedFormRequest(request, bytes).formData();
370
+ const argsField = form.get(ACTION_ARGS_FIELD);
371
+ form.delete(ACTION_ID_FIELD);
372
+ if (typeof argsField === 'string') {
373
+ form.delete(ACTION_ARGS_FIELD);
374
+ return decodeStructuredArgs(argsField, form);
375
+ }
376
+ // Progressive useActionState submission: the form carries its current
377
+ // state; the action signature is (state, formData).
378
+ const stateField = form.get(FORM_STATE_FIELD);
379
+ if (typeof stateField === 'string') {
380
+ form.delete(FORM_STATE_FIELD);
381
+ let state: unknown = null;
382
+ try {
383
+ state = JSON.parse(stateField);
384
+ } catch {
385
+ state = stateField;
386
+ }
387
+ return [state, form];
388
+ }
389
+ return [form];
390
+ }
391
+
392
+ // Decode the buffered bytes in one shot (never per-chunk): a streaming or
393
+ // rewrapped body can otherwise split a multi-byte UTF-8 character across
394
+ // chunk boundaries (decode-req-body suite, 100k あ ≈ 300KB).
395
+ const text = new TextDecoder('utf-8').decode(bytes);
396
+ if (text.trim() === '') return [];
397
+
398
+ const parsed = JSON.parse(text) as unknown;
399
+ return Array.isArray(parsed) ? (parsed as unknown[]) : [parsed];
400
+ }
401
+
402
+ /**
403
+ * Revive server-action references passed as arguments: the client encodes a
404
+ * function arg that is itself an action as its wire id; the server swaps the
405
+ * marker back for the live server-side function, preserving identity (an
406
+ * action passed through the wire compares === to the module/instance action).
407
+ */
408
+ async function reviveActionRefs(
409
+ args: unknown[],
410
+ resolve: (id: string) => Promise<((...a: unknown[]) => unknown) | undefined>,
411
+ ): Promise<unknown[]> {
412
+ const reviveValue = async (value: unknown): Promise<unknown> => {
413
+ if (value === null || typeof value !== 'object') return value;
414
+ if (Array.isArray(value)) return Promise.all(value.map(reviveValue));
415
+ const record = value as Record<string, unknown>;
416
+ if (typeof record[ARG_ACTION_MARKER] === 'string') {
417
+ const fn = await resolve(record[ARG_ACTION_MARKER]);
418
+ if (!fn) {
419
+ const id = String(record[ARG_ACTION_MARKER]);
420
+ console.error(
421
+ `Failed to find Server Action "${id}". This request might be from an older or newer deployment.\n` +
422
+ 'Read more: https://nextjs.org/docs/messages/failed-to-find-server-action',
423
+ );
424
+ throw new UnknownActionReferenceError(id);
425
+ }
426
+ return fn;
427
+ }
428
+ if (record.$$pnext_undefined === true) return undefined;
429
+ const binary = reviveBinaryMarker(record);
430
+ if (binary !== undefined) return binary;
431
+ if (typeof record.$$pnext_date === 'string') return new Date(record.$$pnext_date);
432
+ if (Array.isArray(record.$$pnext_map)) {
433
+ const entries = await Promise.all(
434
+ (record.$$pnext_map as [unknown, unknown][]).map(
435
+ async ([k, v]) => [await reviveValue(k), await reviveValue(v)] as [unknown, unknown],
436
+ ),
437
+ );
438
+ return new Map(entries);
439
+ }
440
+ if (Array.isArray(record.$$pnext_set)) {
441
+ return new Set(await Promise.all((record.$$pnext_set as unknown[]).map(reviveValue)));
442
+ }
443
+ if (value instanceof FormData || value instanceof Blob) return value;
444
+ if (
445
+ Object.getPrototypeOf(value) !== Object.prototype &&
446
+ Object.getPrototypeOf(value) !== null
447
+ ) {
448
+ return value;
449
+ }
450
+ const out: Record<string, unknown> = {};
451
+ for (const [key, item] of Object.entries(record)) out[key] = await reviveValue(item);
452
+ return out;
453
+ };
454
+ return Promise.all(args.map(reviveValue));
455
+ }
456
+
457
+ const TYPED_ARRAY_MARKER = '$$pnext_typed_array';
458
+ const ARRAY_BUFFER_MARKER = '$$pnext_array_buffer';
459
+
460
+ const TYPED_ARRAY_CTORS: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView> = {
461
+ Int8Array,
462
+ Uint8Array,
463
+ Uint8ClampedArray,
464
+ Int16Array,
465
+ Uint16Array,
466
+ Int32Array,
467
+ Uint32Array,
468
+ Float32Array,
469
+ Float64Array,
470
+ BigInt64Array,
471
+ BigUint64Array,
472
+ };
473
+
474
+ function isTypedArrayView(value: unknown): value is ArrayBufferView {
475
+ return ArrayBuffer.isView(value) && !(value instanceof DataView);
476
+ }
477
+
478
+ /** Deep-copy a return value, marker-encoding typed arrays / ArrayBuffers. */
479
+ function encodeBinaryReturn(value: unknown, seen = new Set<unknown>()): unknown {
480
+ if (value === null || typeof value !== 'object') return value;
481
+ if (value instanceof ArrayBuffer) {
482
+ return { [ARRAY_BUFFER_MARKER]: Buffer.from(new Uint8Array(value)).toString('base64') };
483
+ }
484
+ if (isTypedArrayView(value)) {
485
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
486
+ return {
487
+ [TYPED_ARRAY_MARKER]: {
488
+ type: value.constructor.name,
489
+ data: Buffer.from(bytes).toString('base64'),
490
+ },
491
+ };
492
+ }
493
+ if (seen.has(value)) return value;
494
+ seen.add(value);
495
+ if (Array.isArray(value)) return value.map(item => encodeBinaryReturn(item, seen));
496
+ const proto = Object.getPrototypeOf(value) as object | null;
497
+ if (proto !== Object.prototype && proto !== null) return value;
498
+ const out: Record<string, unknown> = {};
499
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
500
+ out[key] = encodeBinaryReturn(item, seen);
501
+ }
502
+ return out;
503
+ }
504
+
505
+ /** Revive a typed-array / ArrayBuffer wire marker back into a live view, else undefined. */
506
+ function reviveBinaryMarker(record: Record<string, unknown>): unknown {
507
+ const typed = record[TYPED_ARRAY_MARKER];
508
+ if (typed && typeof typed === 'object') {
509
+ const { type, data } = typed as { type?: string; data?: string };
510
+ const Ctor = type ? TYPED_ARRAY_CTORS[type] : undefined;
511
+ if (Ctor && typeof data === 'string') {
512
+ const bytes = Buffer.from(data, 'base64');
513
+ return new Ctor(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
514
+ }
515
+ }
516
+ const buffer = record[ARRAY_BUFFER_MARKER];
517
+ if (typeof buffer === 'string') {
518
+ const bytes = Buffer.from(buffer, 'base64');
519
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
520
+ }
521
+ return undefined;
522
+ }
523
+
524
+ class UnknownActionReferenceError extends Error {
525
+ constructor(public actionId: string) {
526
+ super(`Server Action "${actionId}" was not found on the server.`);
527
+ }
528
+ }
529
+
530
+ function decodeStructuredArgs(argsJson: string, form: FormData): unknown[] {
531
+ const parsed = JSON.parse(argsJson) as unknown;
532
+ const list = Array.isArray(parsed) ? (parsed as unknown[]) : [parsed];
533
+ return list.map(item => reviveArg(item, form));
534
+ }
535
+
536
+ function reviveArg(value: unknown, form: FormData): unknown {
537
+ if (value === null || typeof value !== 'object') return value;
538
+ if (Array.isArray(value)) return value.map(item => reviveArg(item, form));
539
+
540
+ const record = value as Record<string, unknown>;
541
+ if (typeof record[ARG_FORMDATA_MARKER] === 'number') {
542
+ const slot = record[ARG_FORMDATA_MARKER];
543
+ const prefix = `${ARG_FORMDATA_PREFIX}${slot}:`;
544
+ const data = new FormData();
545
+ for (const [key, item] of form.entries()) {
546
+ if (key.startsWith(prefix)) data.append(key.slice(prefix.length), item as string | Blob);
547
+ }
548
+ return data;
549
+ }
550
+ if (typeof record[ARG_FILE_MARKER] === 'number') {
551
+ const slot = record[ARG_FILE_MARKER];
552
+ return form.get(`${ARG_FILE_PREFIX}${slot}`);
553
+ }
554
+
555
+ const revived: Record<string, unknown> = {};
556
+ for (const [key, item] of Object.entries(record)) revived[key] = reviveArg(item, form);
557
+ return revived;
558
+ }
559
+
560
+ function redirectResponse(error: PNextRedirectError, refresh: boolean): Response {
561
+ const redirectType = (error as PNextRedirectError & { redirectType?: string }).redirectType;
562
+ // 303: fetch() does not follow 307/308 the way a browser follows a nav, and
563
+ // a no-JS form submit needs the browser to follow with a GET. The redirect
564
+ // marker header lets the client runtime treat this as a framework redirect
565
+ // (soft-navigable) rather than an opaque HTTP redirect.
566
+ const headers = new Headers({
567
+ location: error.location,
568
+ [ACTION_REDIRECT_HEADER]: error.location,
569
+ });
570
+ if (redirectType === 'replace') headers.set(REDIRECT_TYPE_HEADER, 'replace');
571
+ setRefreshHeaders(headers, refresh);
572
+ return new Response(null, { status: 303, headers });
573
+ }
574
+
575
+ function setRefreshHeaders(headers: Headers, refresh: boolean): void {
576
+ if (!refresh) return;
577
+ headers.set(ACTION_REFRESH_HEADER, '1');
578
+ headers.set(NEXT_ACTION_REVALIDATED_HEADER, '1');
579
+ }
580
+
581
+ function errorResponse(message: string, status: number): Response {
582
+ return new Response(message, {
583
+ status,
584
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
585
+ });
586
+ }
587
+
588
+ /** Merge scope-collected headers (set-cookie appends) onto the response. */
589
+ function withResponseHeaders(response: Response, extra: Headers): Response {
590
+ const setCookies = (extra as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
591
+ if (setCookies.length === 0) return response;
592
+ for (const cookie of setCookies) response.headers.append('set-cookie', cookie);
593
+ return response;
594
+ }
595
+
596
+ function hasSetCookie(headers: Headers): boolean {
597
+ return headers.has('set-cookie');
598
+ }
599
+
600
+ function messageOf(error: unknown): string {
601
+ return error instanceof Error ? error.message : String(error);
602
+ }
@@ -0,0 +1,52 @@
1
+ import { ACTION_REDIRECT_HEADER, REDIRECT_TYPE_HEADER } from './protocol';
2
+
3
+ export const ACTION_FLIGHT_CONTENT_TYPE = 'text/x-component';
4
+
5
+ export interface ActionFlightPayload {
6
+ a?: unknown;
7
+ f: { kind: 'pnext-html'; html: string; url: string } | '';
8
+ q: string;
9
+ i: boolean;
10
+ }
11
+
12
+ export interface CreateActionFlightResponseOptions {
13
+ pageResponse: Response;
14
+ target: URL;
15
+ actionHeaders: Headers;
16
+ redirect?: string;
17
+ redirectType?: string;
18
+ }
19
+
20
+ export async function createActionFlightResponse({
21
+ pageResponse,
22
+ target,
23
+ actionHeaders,
24
+ redirect,
25
+ redirectType,
26
+ }: CreateActionFlightResponseOptions): Promise<Response> {
27
+ const html = await pageResponse.text();
28
+ const payload: ActionFlightPayload = {
29
+ f: { kind: 'pnext-html', html, url: target.pathname + target.search },
30
+ q: target.search,
31
+ i: false,
32
+ };
33
+ const headers = new Headers(pageResponse.headers);
34
+ headers.set('content-type', ACTION_FLIGHT_CONTENT_TYPE);
35
+ copyActionHeaders(actionHeaders, headers);
36
+ if (redirect) headers.set(ACTION_REDIRECT_HEADER, redirect);
37
+ if (redirectType) headers.set(REDIRECT_TYPE_HEADER, redirectType);
38
+ // A framework redirect returns HTTP 303 (matching Next's action-redirect wire
39
+ // the e2e suite asserts) with the destination page HTML inline in the flight
40
+ // payload; the client swaps the document without a follow-up GET. A plain
41
+ // flight envelope (revalidation, no redirect) stays 200.
42
+ return new Response(JSON.stringify(payload), { status: redirect ? 303 : 200, headers });
43
+ }
44
+
45
+ function copyActionHeaders(from: Headers, to: Headers): void {
46
+ for (const name of ['x-pnext-action-refresh', 'x-action-revalidated']) {
47
+ const value = from.get(name);
48
+ if (value) to.set(name, value);
49
+ }
50
+ const setCookies = (from as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
51
+ for (const cookie of setCookies) to.append('set-cookie', cookie);
52
+ }