@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,414 @@
1
+ import { runAction, type RunActionOptions } from './endpoint';
2
+ import { ACTION_FLIGHT_CONTENT_TYPE, createActionFlightResponse } from './flight';
3
+ import { INVALID_ORIGIN_MESSAGE, isActionOriginAllowed } from './origin';
4
+ import {
5
+ ACTION_ID_FIELD,
6
+ SUBMIT_ACTION_ID_FIELD,
7
+ ACTION_ID_HEADER,
8
+ ACTION_NOT_FOUND_HEADER,
9
+ NEXT_ACTION_NOT_FOUND_HEADER,
10
+ ACTION_REDIRECT_HEADER,
11
+ ACTION_REFRESH_HEADER,
12
+ NEXT_ACTION_REVALIDATED_HEADER,
13
+ FORM_STATE_FIELD,
14
+ MASKED_ACTION_ERROR,
15
+ REDIRECT_TYPE_HEADER,
16
+ type ActionSuccessBody,
17
+ } from './protocol';
18
+
19
+ export { ACTION_REFRESH_HEADER, ACTION_ID_FIELD };
20
+
21
+ /**
22
+ * Server-action dispatch for the pnext servers. Actions POST to the page's own
23
+ * URL (mirroring Next's wire shape, which the e2e suites observe):
24
+ *
25
+ * - JS clients (the pnext action client runtime) send the id in the
26
+ * `next-action` header. Redirects come back as 200 JSON `{ redirect }` so the
27
+ * client can soft-navigate (an HTTP 303 would be transparently followed by
28
+ * fetch, breaking external URLs and history control).
29
+ * - No-JS progressive submissions carry the id in a hidden form field; the
30
+ * server promotes it to the header and shapes browser-navigation responses:
31
+ * 303 back to the page on success, 303 to the target on redirect(), and the
32
+ * 404 page for notFound().
33
+ */
34
+
35
+ /** True when a request is a JS-client action RPC (id in header). */
36
+ export function isActionRequest(request: Request): boolean {
37
+ return request.method.toUpperCase() === 'POST' && request.headers.has(ACTION_ID_HEADER);
38
+ }
39
+
40
+ /**
41
+ * True when a POST looks like a no-JS progressive action submission: a form
42
+ * body carrying the hidden action id field. Reads a clone, so the request
43
+ * stays consumable. Non-form POSTs and forms without the field return false.
44
+ */
45
+ export async function isProgressiveActionRequest(request: Request): Promise<boolean> {
46
+ if (request.method.toUpperCase() !== 'POST') return false;
47
+ if (request.headers.has(ACTION_ID_HEADER)) return false;
48
+ const contentType = request.headers.get('content-type') ?? '';
49
+ const isForm =
50
+ contentType.includes('multipart/form-data') || contentType.includes('form-urlencoded');
51
+ if (!isForm) return false;
52
+ try {
53
+ const form = await request.clone().formData();
54
+ // A form-level action (ACTION_ID_FIELD) OR a bare submitter override
55
+ // (SUBMIT_ACTION_ID_FIELD, e.g. <form><button formAction={fn}> with no form
56
+ // action) both count as a progressive action submission.
57
+ const id =
58
+ lastFieldValue(form, SUBMIT_ACTION_ID_FIELD) ?? lastFieldValue(form, ACTION_ID_FIELD);
59
+ return typeof id === 'string' && id !== '';
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export interface ServeActionOptions extends RunActionOptions {
66
+ /** Render the app's 404 page for a notFound() result (progressive + RPC). */
67
+ renderNotFound?: (request: Request) => Promise<Response>;
68
+ /**
69
+ * Progressive useActionState submissions: re-render the posted page with the
70
+ * action's result as the form's state (published via the consumed-once
71
+ * global override) and return the HTML. Null falls back to the 303.
72
+ */
73
+ renderPageForFormState?: (
74
+ request: Request,
75
+ state: unknown,
76
+ actionId: string,
77
+ ) => Promise<Response | null>;
78
+ /**
79
+ * Single-pass redirects: render the redirect target's page for an RPC redirect so the 303 response
80
+ * carries the destination HTML and the client swaps it in without a follow-up GET. The request passed
81
+ * already carries the action's cookie mutations. Null falls back to the JSON redirect envelope.
82
+ */
83
+ renderRedirectTarget?: (request: Request, target: URL) => Promise<Response | null>;
84
+ /** Render a page payload for a JS action Flight response. */
85
+ renderActionFlight?: (request: Request, target: URL) => Promise<Response | null>;
86
+ /**
87
+ * Next "forwarded action" discrimination: true when the POSTed route's module graph does NOT contain
88
+ * the action (the client dispatched it after navigating away from the page that owns it). Next
89
+ * forwards such requests to the owning worker and relays the inner response's HEADERS but not its
90
+ * status, so the browser observes 200 + `x-action-redirect` instead of the direct-dispatch 303.
91
+ */
92
+ isForwardedAction?: (id: string) => boolean;
93
+ }
94
+
95
+ /** Serve a server-action request (either kind). */
96
+ export async function serveAction(
97
+ request: Request,
98
+ options: ServeActionOptions,
99
+ ): Promise<Response> {
100
+ const progressive = !request.headers.has(ACTION_ID_HEADER);
101
+
102
+ // CSRF origin check runs before the action executes: a mismatched origin must
103
+ // never run the action. JS clients get the error as a text/plain body (thrown
104
+ // into the error boundary); no-JS MPA submits get a plain 500.
105
+ if (!isActionOriginAllowed(request)) {
106
+ if (progressive) {
107
+ return new Response('Internal Server Error', {
108
+ status: 500,
109
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
110
+ });
111
+ }
112
+ return new Response(options.dev ? INVALID_ORIGIN_MESSAGE : MASKED_ACTION_ERROR, {
113
+ status: 500,
114
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
115
+ });
116
+ }
117
+
118
+ const promoted = progressive ? await promoteFormActionId(request) : undefined;
119
+ const prepared = promoted?.request ?? request;
120
+
121
+ // A form POST to a page carrying no recognizable action id is an
122
+ // unrecognized MPA action: 404 + Next's marker header and log, rather than a
123
+ // "Missing header" 400. (Non-progressive requests always carry the header.)
124
+ if (progressive && !prepared.headers.has(ACTION_ID_HEADER)) {
125
+ console.error(
126
+ 'Failed to find Server Action. This request might be from an older or newer deployment.\n' +
127
+ 'Read more: https://nextjs.org/docs/messages/failed-to-find-server-action',
128
+ );
129
+ return new Response('Server action not found.', {
130
+ status: 404,
131
+ headers: {
132
+ 'content-type': 'text/plain; charset=utf-8',
133
+ [NEXT_ACTION_NOT_FOUND_HEADER]: '1',
134
+ },
135
+ });
136
+ }
137
+
138
+ const response = await runAction(prepared, options);
139
+
140
+ if (response.headers.has(ACTION_NOT_FOUND_HEADER) && options.renderNotFound) {
141
+ // Serve the real 404 page HTML with a 404 status and the marker header. A
142
+ // progressive submit shows it directly; the JS client runtime detects the
143
+ // 404 + marker and swaps the document in place (action-client.ts). Returning
144
+ // an empty 200 JSON here would leave the JS client with nothing to render.
145
+ const notFound = await options.renderNotFound(request);
146
+ const headers = new Headers(notFound.headers);
147
+ headers.set(ACTION_NOT_FOUND_HEADER, '1');
148
+ copySetCookies(response.headers, headers);
149
+ return new Response(notFound.body, { status: 404, headers });
150
+ }
151
+
152
+ if (!progressive) {
153
+ const wantsFlight = acceptsActionFlight(request);
154
+ if (response.status === 303 && response.headers.has(ACTION_REDIRECT_HEADER)) {
155
+ const target = response.headers.get(ACTION_REDIRECT_HEADER)!;
156
+ const headers = new Headers();
157
+ if (response.headers.has(ACTION_REFRESH_HEADER)) headers.set(ACTION_REFRESH_HEADER, '1');
158
+ const revalidated = response.headers.get(NEXT_ACTION_REVALIDATED_HEADER);
159
+ if (revalidated) headers.set(NEXT_ACTION_REVALIDATED_HEADER, revalidated);
160
+ const redirectType = response.headers.get(REDIRECT_TYPE_HEADER);
161
+ if (redirectType) headers.set(REDIRECT_TYPE_HEADER, redirectType);
162
+ copySetCookies(response.headers, headers);
163
+
164
+ // Forwarded action (dispatched from a page that does not own it): Next
165
+ // relays the owning worker's headers on a 200 — no single-pass 303.
166
+ // The JSON envelope makes the client soft-navigate to the target.
167
+ const actionId = prepared.headers.get(ACTION_ID_HEADER);
168
+ if (actionId && options.isForwardedAction?.(actionId)) {
169
+ headers.set(ACTION_REDIRECT_HEADER, target);
170
+ headers.set('content-type', 'application/json; charset=utf-8');
171
+ return new Response(JSON.stringify({ redirect: target }), { status: 200, headers });
172
+ }
173
+
174
+ // Same-origin targets: single-pass redirect. The 303 has NO location
175
+ // header (fetch would transparently follow one) and carries the
176
+ // destination page HTML; the client pushes history and swaps the
177
+ // document in one round trip, like Next's flight redirects.
178
+ const targetUrl = resolveRedirectTarget(request, target);
179
+ if (targetUrl && (options.renderActionFlight || options.renderRedirectTarget)) {
180
+ const rendered = await options
181
+ .renderActionFlight?.(
182
+ requestWithAppliedCookies(request, targetUrl, response.headers),
183
+ targetUrl,
184
+ )
185
+ .catch(() => null);
186
+ if (wantsFlight && rendered?.status === 200) {
187
+ return createActionFlightResponse({
188
+ pageResponse: rendered,
189
+ target: targetUrl,
190
+ actionHeaders: response.headers,
191
+ redirect: targetUrl.pathname + targetUrl.search,
192
+ ...(redirectType ? { redirectType } : {}),
193
+ });
194
+ }
195
+ const htmlRendered = rendered
196
+ ? rendered
197
+ : await options
198
+ .renderRedirectTarget?.(
199
+ requestWithAppliedCookies(request, targetUrl, response.headers),
200
+ targetUrl,
201
+ )
202
+ .catch(() => null);
203
+ if (htmlRendered?.status === 200) {
204
+ headers.set(ACTION_REDIRECT_HEADER, targetUrl.pathname + targetUrl.search);
205
+ headers.set(
206
+ 'content-type',
207
+ htmlRendered.headers.get('content-type') ?? 'text/html; charset=utf-8',
208
+ );
209
+ return new Response(htmlRendered.body, { status: 303, headers });
210
+ }
211
+ // Same host, but the app cannot render it in-process (a path outside basePath, a rewrite to
212
+ // another origin). Next still answers the action with a redirect status, so send the bare 303
213
+ // plus marker and let the client make the follow-up request.
214
+ headers.set(ACTION_REDIRECT_HEADER, targetUrl.pathname + targetUrl.search);
215
+ return new Response(null, { status: 303, headers });
216
+ }
217
+
218
+ // Fallback: JSON envelope; the client soft-navigates (or hard-navigates
219
+ // for external origins).
220
+ headers.set('content-type', 'application/json; charset=utf-8');
221
+ return new Response(JSON.stringify({ redirect: target }), { status: 200, headers });
222
+ }
223
+ // A non-redirect action returns its plain JSON envelope. When it also revalidated (or mutated
224
+ // cookies), the refresh header rides along and the JS client soft-refreshes the current route in
225
+ // place, preserving hydrated island state. Wrapping the return in a full-page flight envelope here
226
+ // would force the client to rewrite the document, discarding that live state.
227
+ return response;
228
+ }
229
+
230
+ // Progressive success: a useActionState form re-renders the page with the
231
+ // action's result as the new form state; anything else 303s back to the
232
+ // page it posted from so the browser renders the (possibly revalidated)
233
+ // page rather than a JSON body.
234
+ if (response.status === 200) {
235
+ if (promoted?.hasFormState && options.renderPageForFormState) {
236
+ try {
237
+ const body = (await response.clone().json()) as ActionSuccessBody;
238
+ const page = await options.renderPageForFormState(
239
+ request,
240
+ body.data,
241
+ prepared.headers.get(ACTION_ID_HEADER)!,
242
+ );
243
+ if (page) {
244
+ copySetCookies(response.headers, page.headers);
245
+ return page;
246
+ }
247
+ } catch {
248
+ // fall through to the 303
249
+ }
250
+ }
251
+ // A revalidating (or cookie-mutating) progressive action must show its effects immediately. A 303
252
+ // back to the page would have the browser GET it fresh - but that GET can serve prebuilt static
253
+ // output, which predates the action's revalidation and would leave the user looking at stale data.
254
+ // So when the action signalled a refresh, render the posted page inline (a fresh dynamic render
255
+ // that observes the just-applied invalidations and cookie mutations) and return it as the POST
256
+ // response, matching the single-pass render the flight client path already performs.
257
+ if (response.headers.has(ACTION_REFRESH_HEADER) && options.renderRedirectTarget) {
258
+ const pageUrl = new URL(request.url);
259
+ const refreshRequest = requestWithAppliedCookies(request, pageUrl, response.headers);
260
+ refreshRequest.headers.set('x-pnext-soft-nav', '1');
261
+ const rendered = await options
262
+ .renderRedirectTarget(refreshRequest, pageUrl)
263
+ .catch(() => null);
264
+ if (rendered?.status === 200) {
265
+ const headers = new Headers(rendered.headers);
266
+ copySetCookies(response.headers, headers);
267
+ return new Response(rendered.body, { status: 200, headers });
268
+ }
269
+ }
270
+ const headers = new Headers({ location: progressiveReturnTarget(request) });
271
+ copySetCookies(response.headers, headers);
272
+ return new Response(null, { status: 303, headers });
273
+ }
274
+ // Progressive unknown-id: mirror Next's MPA behavior (a plain 500).
275
+ if (response.status === 404 && !response.headers.has(ACTION_NOT_FOUND_HEADER)) {
276
+ console.error(
277
+ 'Error: Failed to find Server Action. This request might be from an older or newer deployment',
278
+ );
279
+ return new Response('Internal Server Error', {
280
+ status: 500,
281
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
282
+ });
283
+ }
284
+ return response;
285
+ }
286
+
287
+ function progressiveReturnTarget(request: Request): string {
288
+ const referer = request.headers.get('referer');
289
+ if (referer) return referer;
290
+ return new URL(request.url).pathname || '/';
291
+ }
292
+
293
+ function acceptsActionFlight(request: Request): boolean {
294
+ return request.headers.get('accept')?.includes(ACTION_FLIGHT_CONTENT_TYPE) ?? false;
295
+ }
296
+
297
+ /**
298
+ * No-JS progressive enhancement: a plain HTML form submit cannot set the next-action header, so the id
299
+ * goes in a hidden field (and on the submit button's name for per-button formAction). Promote the LAST
300
+ * occurrence to the header - the submitter's entry serializes after the form-level hidden field, so it
301
+ * wins.
302
+ */
303
+ async function promoteFormActionId(
304
+ request: Request,
305
+ ): Promise<{ request: Request; hasFormState: boolean }> {
306
+ const contentType = request.headers.get('content-type') ?? '';
307
+ const isForm =
308
+ contentType.includes('multipart/form-data') || contentType.includes('form-urlencoded');
309
+ if (!isForm) return { request, hasFormState: false };
310
+
311
+ let form: FormData;
312
+ try {
313
+ form = await request.clone().formData();
314
+ } catch {
315
+ // Unparseable body (e.g. a mismatched multipart boundary): no id to promote;
316
+ // the caller treats it as an unrecognized MPA action.
317
+ return { request, hasFormState: false };
318
+ }
319
+ // A per-button formAction override (SUBMIT_ACTION_ID_FIELD) wins over the
320
+ // form's default action (ACTION_ID_FIELD), unambiguously and independent of
321
+ // field order — a no-JS submit has no event.submitter to tell them apart, and
322
+ // the form-level hidden field can render either before or after the button.
323
+ const submitId = firstFieldValue(form, SUBMIT_ACTION_ID_FIELD);
324
+ const formActionId = firstFieldValue(form, ACTION_ID_FIELD);
325
+ const id = submitId ?? formActionId;
326
+ if (typeof id !== 'string' || id === '') return { request, hasFormState: false };
327
+ form.delete(SUBMIT_ACTION_ID_FIELD);
328
+ // Form state only applies to the form's own useActionState action, never a
329
+ // plain submitter override (which runs as action(formData), not (state, fd)).
330
+ const hasFormState =
331
+ submitId === undefined && typeof form.get(FORM_STATE_FIELD) === 'string';
332
+ if (!hasFormState) form.delete(FORM_STATE_FIELD);
333
+
334
+ const headers = new Headers(request.headers);
335
+ headers.set(ACTION_ID_HEADER, id);
336
+ // Drop the stale content-type/length; the runtime derives a fresh multipart
337
+ // content-type (with a new boundary) from the FormData body below.
338
+ headers.delete('content-type');
339
+ headers.delete('content-length');
340
+ return {
341
+ request: new Request(request.url, { method: 'POST', headers, body: form }),
342
+ hasFormState,
343
+ };
344
+ }
345
+
346
+ function lastFieldValue(form: FormData, name: string) {
347
+ const values = form.getAll(name);
348
+ return values.length > 0 ? values[values.length - 1] : undefined;
349
+ }
350
+
351
+ function firstFieldValue(form: FormData, name: string) {
352
+ const values = form.getAll(name);
353
+ return values.length > 0 ? values[0] : undefined;
354
+ }
355
+
356
+ function copySetCookies(from: Headers, to: Headers) {
357
+ const setCookies = (from as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
358
+ for (const cookie of setCookies) to.append('set-cookie', cookie);
359
+ }
360
+
361
+ /** Same-origin redirect target URL, or undefined for external redirects. */
362
+ function resolveRedirectTarget(request: Request, target: string): URL | undefined {
363
+ try {
364
+ const requestUrl = new URL(request.url);
365
+ const relativeBase = new URL(requestUrl);
366
+ if (!target.startsWith('/') && !/^[a-z][a-z0-9+.-]*:/i.test(target)) {
367
+ relativeBase.pathname = `${relativeBase.pathname.replace(/\/+$/, '')}/`;
368
+ }
369
+ const url = new URL(target, relativeBase);
370
+ return url.origin === requestUrl.origin ? url : undefined;
371
+ } catch {
372
+ return undefined;
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Build the GET request for the redirect-target render: the action's cookie
378
+ * mutations (set-cookie on the action response) are applied onto the request's
379
+ * cookie header so the target page reads the post-action jar, exactly like a
380
+ * browser following the redirect would send.
381
+ */
382
+ function requestWithAppliedCookies(request: Request, target: URL, actionHeaders: Headers): Request {
383
+ const headers = new Headers(request.headers);
384
+ headers.delete('content-type');
385
+ headers.delete('content-length');
386
+ headers.delete(ACTION_ID_HEADER);
387
+
388
+ const setCookies =
389
+ (actionHeaders as Headers & { getSetCookie?: () => string[] }).getSetCookie?.() ?? [];
390
+ if (setCookies.length > 0) {
391
+ const jar = new Map<string, string>();
392
+ const existing = headers.get('cookie') ?? '';
393
+ for (const part of existing.split(';')) {
394
+ const eq = part.indexOf('=');
395
+ if (eq === -1) continue;
396
+ jar.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
397
+ }
398
+ for (const setCookie of setCookies) {
399
+ const [pair, ...attributes] = setCookie.split(';');
400
+ const eq = (pair ?? '').indexOf('=');
401
+ if (eq === -1) continue;
402
+ const name = pair!.slice(0, eq).trim();
403
+ const value = pair!.slice(eq + 1).trim();
404
+ const expired = attributes.some(attr => /^\s*max-age\s*=\s*0*\s*$/i.test(attr));
405
+ if (expired) jar.delete(name);
406
+ else jar.set(name, value);
407
+ }
408
+ const cookieHeader = [...jar.entries()].map(([name, value]) => `${name}=${value}`).join('; ');
409
+ if (cookieHeader) headers.set('cookie', cookieHeader);
410
+ else headers.delete('cookie');
411
+ }
412
+
413
+ return new Request(target.href, { headers });
414
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Server-side association from an action function instance to its wire id, so
3
+ * render/index.tsx can turn `<form action={serverAction}>` into a progressive-
4
+ * enhancement POST to the action endpoint (hidden id field + method/action
5
+ * attributes) even without JS.
6
+ *
7
+ * A function is tagged when a server render imports an action module and the
8
+ * render layer registers each exported action here. Kept in a WeakMap so tags
9
+ * don't retain modules. Best-effort: an untagged action function renders as a
10
+ * normal form (the client stub still POSTs it when JS runs).
11
+ */
12
+ const actionIds = new WeakMap<object, string>();
13
+
14
+ export function tagServerAction(fn: unknown, id: string): void {
15
+ if (typeof fn === 'function') actionIds.set(fn, id);
16
+ }
17
+
18
+ export function serverActionId(fn: unknown): string | undefined {
19
+ if (typeof fn !== 'function') return undefined;
20
+ return actionIds.get(fn);
21
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The error a server-action dispatch throws when the server no longer
3
+ * recognizes the action id (client/server version skew). Mirrors Next's
4
+ * `UnrecognizedActionError`: a named Error subclass so
5
+ * `unstable_isUnrecognizedActionError` can identify it via `instanceof`
6
+ * (message text is not part of the contract). The client runtime throws it when
7
+ * the action RPC comes back 404 with the `x-nextjs-action-not-found` marker so a
8
+ * user error boundary can catch it and recover (e.g. prompt a page reload).
9
+ */
10
+ export class UnrecognizedActionError extends Error {
11
+ constructor(...args: ConstructorParameters<typeof Error>) {
12
+ super(...args);
13
+ this.name = 'UnrecognizedActionError';
14
+ }
15
+ }
16
+
17
+ /**
18
+ * True for the error a server-action dispatch throws when the server did not
19
+ * recognize the action id. Recognizes the typed error by `instanceof`, and (as
20
+ * a fallback for errors that crossed a serialization boundary and lost their
21
+ * prototype) by the name/message the endpoint sends.
22
+ */
23
+ export function isUnrecognizedActionError(error: unknown): boolean {
24
+ if (error instanceof UnrecognizedActionError) return true;
25
+ return (
26
+ error instanceof Error &&
27
+ (error.name === 'UnrecognizedActionError' ||
28
+ / was not found on the server\.?$/.test(error.message))
29
+ );
30
+ }