@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,875 @@
1
+ // Action extension registration (COMPAT - may import core freely).
2
+ //
3
+ // Wires next/actions compat behavior into the core extension registries so core never statically imports
4
+ // compat/actions/* or compat/next/rewrites:
5
+ // - buildExtensions.steps <- action discovery/bundling + server-reference manifest
6
+ // - requestExtensions.interceptors <- action dispatch, then next.config rewrites
7
+ // - renderExtensions.serializeServerActionProp <- action id policy
8
+ // - bundlerExtensions.clientEsbuildPlugins <- action client-stub plugin
9
+ //
10
+ // The request interceptors read the live per-server routing state core publishes via
11
+ // routing/request-runtime.ts and call core render/import helpers directly (compat to core is allowed),
12
+ // so the thin `{ config }` interceptor context stays unchanged.
13
+
14
+ import path from 'node:path';
15
+ import { existsSync } from 'node:fs';
16
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
17
+ import { fileURLToPath } from 'node:url';
18
+ import type { ResolvedConfig } from '../../config';
19
+ import { pathToFileHref } from '../../config';
20
+ import { nextCompatEnabled } from '../../compat';
21
+ import {
22
+ registerBuildSteps,
23
+ registerInitHooks,
24
+ registerOutsideBasePathInterceptors,
25
+ registerRequestInterceptors,
26
+ registerServerSourceTransforms,
27
+ setBundlerExtensions,
28
+ setRenderExtensions,
29
+ withSniff,
30
+ type ActionRef,
31
+ type BuildStep,
32
+ type BuildStepContext,
33
+ type RequestInterceptor,
34
+ type ServerActionPropContext,
35
+ } from '../../extensions';
36
+ import { setActionModuleExtensions } from '../../render/hooks';
37
+ import { getRequestRuntime, type RequestRuntime } from '../../routing/request-runtime';
38
+ import { setClientActionBundler } from '../../dev/client-actions';
39
+ import { toPosixPath } from '../../utils/fs';
40
+ import type { ActionManifestEntry, BuildManifest, RouteManifestEntry } from '../../types';
41
+ import { devServerModuleHref } from '../../dev/imports';
42
+ import { serverBundleTargetForRuntime } from '../../runtime/server';
43
+ import {
44
+ renderActionReturnElement,
45
+ renderGlobalNotFoundResponse,
46
+ renderPage,
47
+ renderPageResponse,
48
+ } from '../../render';
49
+ import { routeParamsFromPath } from '../../routing/handler';
50
+ import { selectRouteForRequest } from '../../routing/routes';
51
+ import { discoverActions, registerDiscoveredActions } from '../actions/discovery';
52
+ import { clearActions, lookupActionSource, registerActionModule } from '../actions/registry';
53
+ import {
54
+ actionClientStubPlugin,
55
+ actionStubSourceFor,
56
+ canonical,
57
+ canonicalActionId,
58
+ clearClientActionModules,
59
+ clientActionModuleFiles,
60
+ clientActionModulesRoot,
61
+ isClientActionModule,
62
+ setClientActionModules,
63
+ } from '../actions/client-plugin';
64
+ import {
65
+ isActionRequest,
66
+ isProgressiveActionRequest,
67
+ serveAction,
68
+ type ServeActionOptions,
69
+ } from '../actions/serve';
70
+ import {
71
+ NEXT_ACTION_NOT_FOUND_HEADER,
72
+ PROP_ACTION_MARKER,
73
+ } from '../actions/protocol';
74
+ import { loadServerActionsConfig, validateServerActionsConfigSync } from '../actions/config';
75
+ import { renderWithFormStateOverride } from '../actions/form-state';
76
+ import { loadCompatRewrites, type CompatRewrite, resolveCompatRewrite } from '../next/rewrites';
77
+ import { loadCompatRedirects, type CompatRedirect, resolveCompatRedirect } from '../next/redirects';
78
+ import { recordRewrite } from '../protocol';
79
+ import { activateTestProxy } from '../lifecycle/testmode';
80
+ import { canonicalUrlHref, recordCanonicalUrl } from '../next/canonical-url';
81
+ import {
82
+ assignedInlineActionId,
83
+ createInstanceScope,
84
+ instanceActionRouteId,
85
+ isInstanceActionId,
86
+ isMarkedInlineAction,
87
+ overriddenActionId,
88
+ registerActionInstance,
89
+ type InstanceScope,
90
+ } from '../actions/instances';
91
+ import { serverActionId, tagServerAction } from '../actions/server-tag';
92
+ import { opensWithUseServerDirective } from '../actions/detect';
93
+ import { hoistNonCapturingInlineActions } from '../actions/hoist';
94
+ import {
95
+ inlineActionModuleKey,
96
+ isInlineActionId,
97
+ rewriteAnonymousInlineActionMarks,
98
+ rewriteInlineActionTags,
99
+ } from '../actions/rewrite';
100
+ import { reportActionErrorToUser } from '../lifecycle/error-funnel';
101
+
102
+ export function registerActionExtensions(_config: ResolvedConfig): void {
103
+ // Discovery reads config + source text only, so it runs under the route-facts
104
+ // scan; the client bundle is the one thing that waits on its stub set.
105
+ runActionBuildStep.early = true;
106
+ registerBuildSteps(runActionBuildStep);
107
+ // Validate serverActions.bodySizeLimit at build + start startup (Next fails
108
+ // the CLI with the exact size-limit error before serving), rather than only
109
+ // lazily on the first action request.
110
+ registerInitHooks(config => validateServerActionsConfigSync(config.root));
111
+ // Ordering matches the old servers: action dispatch first (on the requested
112
+ // pathname), then next.config redirects (short-circuit before routing), then
113
+ // rewrites swap the request for downstream matching / static-lookup /
114
+ // staleness.
115
+ registerRequestInterceptors(actionDispatchInterceptor, redirectInterceptor, rewriteInterceptor);
116
+ // The same two rules also serve requests from outside the basePath, where only
117
+ // their `basePath: false` entries apply (both interceptors partition their
118
+ // rules on ctx.outsideBasePath).
119
+ registerOutsideBasePathInterceptors(redirectInterceptor, rewriteInterceptor);
120
+ // Tag MODULE-SCOPE inline 'use server' functions with React-compatible `$$id` metadata at module-eval
121
+ // time. Without this, top-level inline actions that module-scope code introspects - Next's version-skew
122
+ // fixtures read and override `fn.$$id` - throw at import. Runs on the server graph only; module-level
123
+ // 'use server' files are skipped by the transform. Anonymous inline actions get no id, only a mark: the
124
+ // compiled function loses its directive, so this is what tells serialization it is an action and not a
125
+ // client handler. Hoisting runs FIRST: it lifts capture-free anonymous inline actions to module-scope
126
+ // declarations, which the tag pass then gives stable `if:` ids, taking the whole-route recovery render
127
+ // off the action's critical path for pages served from prebuilt HTML.
128
+ const inlineActionSniff = ['use server'];
129
+ registerServerSourceTransforms(
130
+ withSniff(inlineActionSniff, source => hoistNonCapturingInlineActions(source)),
131
+ withSniff(inlineActionSniff, (source, file, root) =>
132
+ rewriteInlineActionTags(source, file, root),
133
+ ),
134
+ withSniff(inlineActionSniff, source => rewriteAnonymousInlineActionMarks(source)),
135
+ );
136
+ setRenderExtensions({ serializeServerActionProp });
137
+ // Tag imported action-module function exports with their stable wire id so a
138
+ // <form action={serverAction}> imported straight into a page (same module
139
+ // singleton) serializes to that module id instead of a per-render inline id.
140
+ setActionModuleExtensions({ tagActionModuleExports, tagActionModulesForRender });
141
+ // Prod/plain client builds swap 'use server' imports for the RPC stub via
142
+ // the standalone esbuild plugin (client/build.ts appends these). No-op for
143
+ // pure-core / non-next apps.
144
+ setBundlerExtensions({ clientEsbuildPlugins: () => [actionClientStubPlugin()] });
145
+ // The dev route-client bundle resolves modules into its own esbuild namespace
146
+ // that a standalone plugin can't intercept, so dev/imports reads this holder
147
+ // instead. ensureArmed runs the discovery dev startup/reload used to run
148
+ // inline, lazily per dev import version.
149
+ setClientActionBundler({
150
+ ensureArmed: async () => {
151
+ const runtime = getRequestRuntime();
152
+ if (runtime?.dev) await ensureActionsRegistered(runtime);
153
+ },
154
+ isClientActionModule,
155
+ stubSource: file => actionStubSourceFor(file, clientActionModulesRoot() ?? undefined),
156
+ });
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Build step: action discovery/bundling + server-reference manifest.
161
+ // ---------------------------------------------------------------------------
162
+
163
+ interface ActionStepState {
164
+ actions: ActionManifestEntry[];
165
+ actionSources?: string[];
166
+ deferred?: Promise<void>;
167
+ }
168
+
169
+ const runActionBuildStep: BuildStep = async (ctx: BuildStepContext): Promise<void> => {
170
+ const config = ctx.config;
171
+ const state = ctx.manifest as ActionStepState;
172
+ // Discovery is what arms the client stub set and names the action source
173
+ // files, and it is the only part of this step the client stage waits on.
174
+ const discovery = await (ctx.log
175
+ ? ctx.log.step('action discovery', () => discoverActions(config))
176
+ : discoverActions(config));
177
+ state.actionSources = actionSourceKeys(config, discovery);
178
+ // Compiling those modules to server bundles produces manifest entries nothing
179
+ // upstream of the build manifest reads, so it is handed back to run under the
180
+ // client stage instead of ahead of it.
181
+ state.deferred = (async () => {
182
+ const actions = await compileActionModules(config, discovery, ctx.log);
183
+ // The step's `manifest` accumulator carries the actions array the core build
184
+ // folds into the build manifest (manifest.actions) verbatim.
185
+ state.actions = actions;
186
+ if (actions.length > 0) await writeServerReferenceManifest(config, actions);
187
+ })();
188
+ };
189
+
190
+ /**
191
+ * Discover 'use server' modules, compile each to a server bundle (compat
192
+ * aliases + directive handling applied, like the proxy module), and produce the
193
+ * action manifest start serves from. Also arms the client-bundle stub set so
194
+ * client imports of these modules become the RPC stub during buildClientEntries.
195
+ *
196
+ * The wire id is computed from the SOURCE file path (config.root-relative), so
197
+ * the client stub and the server registry agree; modulePath is the COMPILED
198
+ * output the endpoint imports at request time.
199
+ */
200
+ function actionSourceKeys(
201
+ config: ResolvedConfig,
202
+ discovery: Awaited<ReturnType<typeof discoverActions>>,
203
+ ): string[] {
204
+ clearActions();
205
+ if (discovery.modules.length === 0) {
206
+ setClientActionModules(config.root, []);
207
+ return [];
208
+ }
209
+ // Client bundling replaces these source imports with the stub.
210
+ setClientActionModules(config.root, discovery.actionFiles);
211
+ // Registered from the discovery facts alone (ids come from source paths), so
212
+ // build-time lookupAction parity holds before the compile below finishes.
213
+ registerDiscoveredActions(config, discovery);
214
+ return discovery.modules.map(module => actionSourceKey(config, module.file));
215
+ }
216
+
217
+ /** The registry id's source key: canonical, root-relative, POSIX. */
218
+ function actionSourceKey(config: ResolvedConfig, file: string) {
219
+ // Must reproduce the registry id at startup: registerManifestActions calls
220
+ // actionId(sourceKey) with no root, so this has to equal
221
+ // workspaceRelative(canonical(file), canonical(root)).
222
+ return toPosixPath(path.relative(canonical(config.root), canonical(file)));
223
+ }
224
+
225
+ async function compileActionModules(
226
+ config: ResolvedConfig,
227
+ discovery: Awaited<ReturnType<typeof discoverActions>>,
228
+ log?: BuildStepContext['log'],
229
+ ): Promise<ActionManifestEntry[]> {
230
+ if (discovery.modules.length === 0) return [];
231
+
232
+ const entries: ActionManifestEntry[] = [];
233
+ const compile = async () => {
234
+ for (const module of discovery.modules) {
235
+ // 'build', matching render/index.tsx's prod moduleHref: the endpoint and the
236
+ // page render must import the SAME compiled file so a module imported in
237
+ // both the render and action layers is one instance (shared module state).
238
+ const href = await devServerModuleHref(config, module.file, 'build');
239
+ const compiled = fileURLToPath(href);
240
+ const modulePath = toPosixPath(path.relative(config.outPath, compiled));
241
+ const sourceKey = actionSourceKey(config, module.file);
242
+ for (const exportName of module.exports) {
243
+ entries.push({ id: module.ids[exportName]!, sourceKey, modulePath, exportName });
244
+ }
245
+ }
246
+ };
247
+ if (log) await log.step(`action modules (${discovery.modules.length})`, compile);
248
+ else await compile();
249
+ return entries;
250
+ }
251
+
252
+ // Next-compatible server-reference manifest (.next/server/): tools and Next's
253
+ // own e2e suite introspect it for the registered action ids and their source
254
+ // modules. pnext's real registry lives in .pnext/manifest.json; this mirror
255
+ // exists purely for ecosystem compatibility.
256
+ async function writeServerReferenceManifest(
257
+ config: ResolvedConfig,
258
+ actions: ActionManifestEntry[],
259
+ ) {
260
+ const node: Record<
261
+ string,
262
+ {
263
+ workers: Record<string, unknown>;
264
+ layer: Record<string, unknown>;
265
+ filename: string;
266
+ exportedName: string;
267
+ }
268
+ > = {};
269
+ for (const action of actions) {
270
+ node[action.id] = {
271
+ workers: {},
272
+ layer: {},
273
+ filename: action.sourceKey,
274
+ exportedName: action.exportName,
275
+ };
276
+ }
277
+ const manifest = { node, edge: {}, encryptionKey: 'pnext' };
278
+ const outDir = path.join(config.root, '.next', 'server');
279
+ await mkdir(outDir, { recursive: true });
280
+ await writeFile(
281
+ path.join(outDir, 'server-reference-manifest.json'),
282
+ JSON.stringify(manifest, null, 2),
283
+ );
284
+ }
285
+
286
+ // Request interceptor: server-action dispatch. Actions POST to the page's own URL (id in the
287
+ // next-action header for JS clients, or a hidden form field for no-JS progressive submissions).
288
+ // Route-handler POSTs pass through even with an id header - actions only dispatch on page URLs, or on
289
+ // unmatched paths, which still get the action error semantics.
290
+
291
+ const actionDispatchInterceptor: RequestInterceptor = async (request, ctx) => {
292
+ const config = ctx.config;
293
+ if (!nextCompatEnabled(config)) return undefined;
294
+ const runtime = getRequestRuntime();
295
+ if (!runtime) return undefined;
296
+ const url = new URL(request.url);
297
+ const actionTarget = selectRouteForRequest(runtime.routes, url.pathname, undefined);
298
+ // Route-handler POSTs pass through even with an id header — actions only
299
+ // dispatch on page URLs (or unmatched paths, which still get the action
300
+ // error semantics the unrecognized-action suite asserts).
301
+ if (actionTarget && actionTarget.route.kind !== 'page') return undefined;
302
+
303
+ // A form-encoded POST to an existing page with no recognizable action id is still an MPA action attempt
304
+ // in Next, which routes every page form POST through the action handler: with no action to run it
305
+ // answers 404 action-not-found rather than rendering the page. Only applies to real page targets - a
306
+ // POST to a nonexistent route stays a clean 404.
307
+ const isFormPost = actionTarget?.route.kind === 'page' && isFormPostRequest(request);
308
+ if (
309
+ !(isActionRequest(request) || (await isProgressiveActionRequest(request)) || isFormPost)
310
+ ) {
311
+ return undefined;
312
+ }
313
+
314
+ await ensureActionsRegistered(runtime);
315
+
316
+ const actionId = request.headers.get('next-action');
317
+ if (
318
+ actionId &&
319
+ canonicalUrlHref() !== undefined &&
320
+ isForwardedActionForRoute(config, actionTarget, actionId)
321
+ ) {
322
+ return new Response(`Server Action "${actionId}" was not found on the server.`, {
323
+ status: 404,
324
+ headers: {
325
+ 'content-type': 'text/plain; charset=utf-8',
326
+ [NEXT_ACTION_NOT_FOUND_HEADER]: '1',
327
+ },
328
+ });
329
+ }
330
+
331
+ const maxBodyBytes = runtime.maxBodyBytes ?? (await resolveMaxBodyBytes(config));
332
+ const options = buildServeActionOptions(runtime, request, actionTarget, maxBodyBytes);
333
+ return serveAction(request, options);
334
+ };
335
+
336
+ // Strip the configured basePath from a redirect-target URL so it lines up with
337
+ // the app-relative route table. Same matching rule as start.ts stripBasePath;
338
+ // a target outside the basePath (a `basePath:false` rewrite destination) is
339
+ // returned unchanged so it simply fails to match a page (external redirect).
340
+ function stripActionBasePath(target: URL, basePath: string): URL {
341
+ if (!basePath) return target;
342
+ const pathname = target.pathname;
343
+ if (pathname !== basePath && !pathname.startsWith(`${basePath}/`)) return target;
344
+ const stripped = new URL(target);
345
+ stripped.pathname = pathname.slice(basePath.length) || '/';
346
+ return stripped;
347
+ }
348
+
349
+ // Rebuild the render request against the basePath-stripped URL, preserving the
350
+ // action's applied cookie/header state. Returns the original request untouched
351
+ // when no stripping happened.
352
+ function strippedRequest(request: Request, target: URL, stripped: URL): Request {
353
+ if (stripped.href === target.href) return request;
354
+ return new Request(stripped.href, { headers: request.headers });
355
+ }
356
+
357
+ function isFormPostRequest(request: Request): boolean {
358
+ if (request.method.toUpperCase() !== 'POST') return false;
359
+ if (request.headers.has('next-action')) return false;
360
+ const contentType = request.headers.get('content-type') ?? '';
361
+ // Only multipart counts as an id-less MPA action attempt: React's progressive action forms always post
362
+ // multipart/form-data, so a urlencoded POST without an action id is a plain form POST to the page - a
363
+ // browser re-POSTing after a route handler's 307/308 redirect, say - and must render the page, not 404.
364
+ // urlencoded POSTs that DO carry the hidden action id field are still dispatched.
365
+ return contentType.includes('multipart/form-data');
366
+ }
367
+
368
+ // serverActions.bodySizeLimit from next.config, resolved once per project.
369
+ // Invalid values throw (Next's message) — surfaced on the first action request
370
+ // as a 500, matching the size-limit-invalid suite's expectation.
371
+ const bodyLimitCache = new Map<string, Promise<number | undefined>>();
372
+
373
+ function resolveMaxBodyBytes(config: ResolvedConfig): Promise<number | undefined> {
374
+ const key = config.root;
375
+ let cached = bodyLimitCache.get(key);
376
+ if (!cached) {
377
+ cached = loadServerActionsConfig(config.root).then(c => c.bodySizeLimitBytes);
378
+ bodyLimitCache.set(key, cached);
379
+ }
380
+ return cached;
381
+ }
382
+
383
+ // Instance-recovery render dedup: the first dispatch against a prebuilt page
384
+ // re-renders the route to re-materialize its `i:` action closures
385
+ // (instances.ts); concurrent dispatches share that in-flight render instead of
386
+ // each starting a full one. Keyed route+pathname (dynamic routes recover per
387
+ // path), cleared on settle so evicted instances recover again.
388
+ const instanceRecoveries = new Map<string, Promise<void>>();
389
+
390
+ function dedupeInstanceRecovery(
391
+ routeId: string,
392
+ pathname: string,
393
+ render: () => Promise<void>,
394
+ ): Promise<void> {
395
+ const key = `${routeId}${pathname}`;
396
+ const inFlight = instanceRecoveries.get(key);
397
+ if (inFlight) return inFlight;
398
+ const started = render().finally(() => instanceRecoveries.delete(key));
399
+ instanceRecoveries.set(key, started);
400
+ return started;
401
+ }
402
+
403
+ /**
404
+ * Reconstruct the ServeActionOptions the old start/dev handlers built inline,
405
+ * from the published request runtime + core render/import helpers. Dev vs prod
406
+ * only changes how modules resolve to hrefs.
407
+ */
408
+ function buildServeActionOptions(
409
+ runtime: RequestRuntime,
410
+ request: Request,
411
+ actionTarget: ReturnType<typeof selectRouteForRequest>,
412
+ maxBodyBytes: number | undefined,
413
+ ): ServeActionOptions {
414
+ const config = runtime.config;
415
+ const dev = runtime.dev;
416
+ const version = runtime.devImportVersion ?? 'build';
417
+ const url = new URL(request.url);
418
+ const actionConditionTarget =
419
+ actionTarget?.route.kind === 'page'
420
+ ? serverBundleTargetForRuntime(actionTarget.route.segmentConfig?.runtime)
421
+ : 'server';
422
+
423
+ const importModule = dev
424
+ ? async (modulePath: string) =>
425
+ (await import(
426
+ await devServerModuleHref(config, modulePath, version, {
427
+ conditionTarget: actionConditionTarget,
428
+ })
429
+ )) as Record<string, unknown>
430
+ : async (modulePath: string) =>
431
+ (await import(pathToFileHref(path.resolve(config.outPath, modulePath)))) as Record<
432
+ string,
433
+ unknown
434
+ >;
435
+
436
+ const options: ServeActionOptions = {
437
+ ...(dev ? { dev: true } : {}),
438
+ importModule,
439
+ renderNotFound: async notFoundRequest =>
440
+ renderGlobalNotFoundResponse({
441
+ config,
442
+ url: new URL(notFoundRequest.url),
443
+ // GET conversion: the page renderer refuses non-GET requests.
444
+ request: new Request(notFoundRequest.url, { headers: notFoundRequest.headers }),
445
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
446
+ }),
447
+ renderPageForFormState: async (formRequest, state, actionId) => {
448
+ if (actionTarget?.route.kind !== 'page') return null;
449
+ const formUrl = new URL(formRequest.url);
450
+ return renderWithFormStateOverride(state, actionId, () =>
451
+ renderPageResponse({
452
+ config,
453
+ route: actionTarget.route,
454
+ params: actionTarget.params,
455
+ url: formUrl,
456
+ request: new Request(formRequest.url, { headers: formRequest.headers }),
457
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
458
+ }),
459
+ );
460
+ },
461
+ renderRedirectTarget: async (redirectRequest, target) => {
462
+ // The redirect target arrives in basePath space (redirect() prefixes it);
463
+ // strip the prefix so it matches the app-relative route table and the
464
+ // rendered page's usePathname excludes the basePath, exactly like a
465
+ // freshly-routed GET (which start.ts strips before matching).
466
+ const stripped = stripActionBasePath(target, config.basePath);
467
+ const targetMatch = selectRouteForRequest(runtime.routes, stripped.pathname, undefined);
468
+ if (targetMatch?.route.kind !== 'page') return null;
469
+ return renderPageResponse({
470
+ config,
471
+ route: targetMatch.route,
472
+ params: targetMatch.params,
473
+ url: stripped,
474
+ request: strippedRequest(redirectRequest, target, stripped),
475
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
476
+ });
477
+ },
478
+ renderActionFlight: async (flightRequest, target) => {
479
+ const stripped = stripActionBasePath(target, config.basePath);
480
+ const targetMatch = selectRouteForRequest(runtime.routes, stripped.pathname, undefined);
481
+ if (targetMatch?.route.kind !== 'page') return null;
482
+ return renderPageResponse({
483
+ config,
484
+ route: targetMatch.route,
485
+ params: targetMatch.params,
486
+ url: stripped,
487
+ request: strippedRequest(flightRequest, target, stripped),
488
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
489
+ });
490
+ },
491
+ importSourceModule: async moduleKey => {
492
+ const absolute = path.resolve(config.root, moduleKey);
493
+ await import(
494
+ await devServerModuleHref(config, absolute, version, {
495
+ conditionTarget: actionConditionTarget,
496
+ }),
497
+ );
498
+ },
499
+ rerenderRoute: async routeId => {
500
+ const route = runtime.routes.find(entry => entry.id === routeId && entry.kind === 'page');
501
+ if (!route) throw new Error(`Unknown route for action re-render: ${routeId}`);
502
+ await dedupeInstanceRecovery(routeId, url.pathname, () =>
503
+ renderPage({
504
+ config,
505
+ route,
506
+ params: routeParamsFromPath(route, url.pathname),
507
+ url,
508
+ request: new Request(request.url, { headers: request.headers }),
509
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
510
+ }).then(() => undefined),
511
+ );
512
+ },
513
+ isForwardedAction: id => isForwardedActionForRoute(config, actionTarget, id),
514
+ onError: reportActionErrorToUser,
515
+ ...(maxBodyBytes !== undefined ? { maxBodyBytes } : {}),
516
+ // Element-returning actions server-render the returned tree through the same
517
+ // machinery a page render uses (async server components, host elements),
518
+ // producing the island wire HTML the action client revives. Scoped to page
519
+ // targets — the only routes that own such actions.
520
+ ...(actionTarget?.route.kind === 'page'
521
+ ? {
522
+ renderActionElement: (element: unknown) =>
523
+ renderActionReturnElement(element, {
524
+ config,
525
+ route: actionTarget.route,
526
+ params: actionTarget.params,
527
+ url,
528
+ request: new Request(request.url, { headers: request.headers }),
529
+ ...(dev ? { dev: true, devImportVersion: version } : {}),
530
+ }),
531
+ }
532
+ : {}),
533
+ };
534
+ return options;
535
+ }
536
+
537
+ /**
538
+ * Next "forwarded action" discrimination: the POSTed route's static source closure does not contain the
539
+ * action's source module, meaning the client dispatched the action after navigating to a different page.
540
+ * For live-instance actions the owning route is encoded in the id; for module and inline actions the
541
+ * registered source file is checked against the route's sourceFiles closure, realpath-canonicalized since
542
+ * the closure may hold /private-var vs /var aliases of the same file on macOS.
543
+ */
544
+ function isForwardedActionForRoute(
545
+ config: ResolvedConfig,
546
+ actionTarget: ReturnType<typeof selectRouteForRequest>,
547
+ id: string,
548
+ ): boolean {
549
+ const route = actionTarget?.route;
550
+ if (route?.kind !== 'page') return false;
551
+ if (isInstanceActionId(id)) {
552
+ const owner = instanceActionRouteId(id);
553
+ return Boolean(owner && owner !== route.id);
554
+ }
555
+ const source = isInlineActionId(id) ? inlineActionModuleKey(id) : lookupActionSource(id);
556
+ if (!source) return false;
557
+ const sources = route.sourceFiles;
558
+ if (!sources?.length) return false;
559
+ const target = canonical(path.resolve(config.root, source));
560
+ return !sources.some(file => canonical(file) === target);
561
+ }
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // Action registry arming.
565
+ //
566
+ // prod: rebuild the registry from the built manifest once (id parity with the
567
+ // client stub via the stored source key).
568
+ // dev: re-run discovery whenever the dev import version changes (reload), so
569
+ // the endpoint + client-stub set stay current without core importing us.
570
+ // ---------------------------------------------------------------------------
571
+
572
+ // Memoized on the in-flight PROMISE, not on a "started" flag: a page can fire several actions at once,
573
+ // and a flag set before the await lets the second POST through against a registry that is still empty, so
574
+ // it 404s as an unknown action. Every caller awaits the same registration. A failed one is dropped so the
575
+ // next request retries instead of inheriting the rejection forever. `UNREGISTERED` keys the initial (and
576
+ // post-failure) state so the memo itself is never optional.
577
+ const UNREGISTERED = Symbol('pnext.actions.unregistered');
578
+ const noRegistration = { key: UNREGISTERED, version: UNREGISTERED, done: Promise.resolve() };
579
+ let manifestRegistration: { key: string | symbol; done: Promise<void> } = noRegistration;
580
+ let devRegistration: { version: string | symbol | undefined; done: Promise<void> } =
581
+ noRegistration;
582
+
583
+ async function ensureActionsRegistered(runtime: RequestRuntime): Promise<void> {
584
+ if (runtime.dev) {
585
+ if (devRegistration.version !== runtime.devImportVersion) {
586
+ const pending = { version: runtime.devImportVersion, done: Promise.resolve() };
587
+ pending.done = refreshDevActions(runtime.config).catch((error: unknown) => {
588
+ if (devRegistration === pending) devRegistration = noRegistration;
589
+ throw error;
590
+ });
591
+ devRegistration = pending;
592
+ }
593
+ await devRegistration.done;
594
+ return;
595
+ }
596
+ const key = runtime.config.outPath;
597
+ if (manifestRegistration.key !== key) {
598
+ const pending = { key, done: Promise.resolve() };
599
+ pending.done = registerManifestActions(runtime.config).catch((error: unknown) => {
600
+ if (manifestRegistration === pending) manifestRegistration = noRegistration;
601
+ throw error;
602
+ });
603
+ manifestRegistration = pending;
604
+ }
605
+ await manifestRegistration.done;
606
+ }
607
+
608
+ // Rebuild the action registry from the built manifest so the endpoint can
609
+ // resolve an incoming id to its compiled module + export. Also restore the
610
+ // client-action module set (normally armed by the build): render-time form/prop
611
+ // tagging reads it to give module-level actions their stable wire ids.
612
+ async function registerManifestActions(config: ResolvedConfig): Promise<void> {
613
+ const manifestPath = path.join(config.outPath, 'manifest.json');
614
+ let manifest: BuildManifest;
615
+ try {
616
+ manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as BuildManifest;
617
+ } catch {
618
+ return;
619
+ }
620
+ const files = new Set<string>();
621
+ for (const action of manifest.actions ?? []) {
622
+ registerActionModule(action.sourceKey, [action.exportName], {
623
+ modulePath: action.modulePath,
624
+ });
625
+ files.add(path.resolve(config.root, action.sourceKey));
626
+ }
627
+ if (files.size > 0) setClientActionModules(config.root, files);
628
+ }
629
+
630
+ // Rescan the app for 'use server' modules and refresh both the endpoint
631
+ // registry (id -> module) and the client-bundle stub set. In dev the action's
632
+ // modulePath stays the source file: devServerModuleHref compiles it on demand
633
+ // when the endpoint imports it. Gated on compat.next inside discoverActions.
634
+ async function refreshDevActions(config: ResolvedConfig): Promise<void> {
635
+ clearActions();
636
+ clearClientActionModules();
637
+ const discovery = await discoverActions(config);
638
+ if (discovery.modules.length === 0) return;
639
+ registerDiscoveredActions(config, discovery);
640
+ setClientActionModules(config.root, discovery.actionFiles);
641
+ }
642
+
643
+ // ---------------------------------------------------------------------------
644
+ // Request interceptor: next.config rewrites.
645
+ //
646
+ // Rewrites map the request pathname to its destination before static lookup and
647
+ // routing, so prebuilt output + revalidatePath marks work against the
648
+ // destination. The render keeps the requested URL canonical (usePathname); core
649
+ // preserves the original URL for rendering (see start/dev canonicalUrl).
650
+ // ---------------------------------------------------------------------------
651
+
652
+ const rewriteCache = new Map<string, Promise<CompatRewrite[]>>();
653
+
654
+ function loadRewritesFor(config: ResolvedConfig): Promise<CompatRewrite[]> {
655
+ const key = config.root;
656
+ let cached = rewriteCache.get(key);
657
+ if (!cached) {
658
+ cached = loadCompatRewrites(config.root);
659
+ rewriteCache.set(key, cached);
660
+ }
661
+ return cached;
662
+ }
663
+
664
+ const redirectCache = new Map<string, Promise<CompatRedirect[]>>();
665
+
666
+ function loadRedirectsFor(config: ResolvedConfig): Promise<CompatRedirect[]> {
667
+ const key = config.root;
668
+ let cached = redirectCache.get(key);
669
+ if (!cached) {
670
+ cached = loadCompatRedirects(config.root);
671
+ redirectCache.set(key, cached);
672
+ }
673
+ return cached;
674
+ }
675
+
676
+ const redirectInterceptor: RequestInterceptor = async (request, ctx) => {
677
+ const config = ctx.config;
678
+ if (!nextCompatEnabled(config)) return undefined;
679
+ // `basePath: false` entries match the raw path core would otherwise 404;
680
+ // every other entry matches the in-app (basePath-stripped) path. The two
681
+ // passes are disjoint.
682
+ const redirects = (await loadRedirectsFor(config)).filter(
683
+ redirect => redirect.outsideBasePath === Boolean(ctx.outsideBasePath),
684
+ );
685
+ if (redirects.length === 0) return undefined;
686
+ const url = new URL(request.url);
687
+ const result = resolveCompatRedirect(redirects, url.pathname, {
688
+ host: request.headers.get('host') ?? '',
689
+ headers: request.headers,
690
+ cookies: parseCookieHeader(request.headers.get('cookie')),
691
+ query: url.searchParams,
692
+ });
693
+ if (!result) return undefined;
694
+ const location = new URL(result.location, url.href);
695
+ // Next passes the incoming query through to the redirect destination (the
696
+ // `_rsc` union query among it — rsc-query-routing relies on the followed
697
+ // request keeping it). Destination-specified params win.
698
+ for (const [key, value] of url.searchParams) {
699
+ if (!location.searchParams.has(key)) location.searchParams.append(key, value);
700
+ }
701
+ return new Response(null, { status: result.status, headers: { location: location.href } });
702
+ };
703
+
704
+ const rewriteInterceptor: RequestInterceptor = async (request, ctx) => {
705
+ const config = ctx.config;
706
+ if (!nextCompatEnabled(config)) return undefined;
707
+ // See redirectInterceptor: `basePath: false` rules own the outside-basePath
708
+ // pass, the rest own the in-app one.
709
+ const rewrites = (await loadRewritesFor(config)).filter(
710
+ rewrite => rewrite.outsideBasePath === Boolean(ctx.outsideBasePath),
711
+ );
712
+ if (rewrites.length === 0) return undefined;
713
+ const url = new URL(request.url);
714
+ const context = {
715
+ host: request.headers.get('host') ?? '',
716
+ headers: request.headers,
717
+ cookies: parseCookieHeader(request.headers.get('cookie')),
718
+ query: url.searchParams,
719
+ };
720
+ // beforeFiles/afterFiles (and the array form) apply ahead of routing; a
721
+ // `fallback` rewrite only gets a turn once nothing else can serve the path.
722
+ let result = resolveCompatRewrite(
723
+ rewrites.filter(rewrite => !rewrite.fallback),
724
+ url.pathname,
725
+ context,
726
+ );
727
+ if (!result && requestWouldMiss(config, url.pathname)) {
728
+ result = resolveCompatRewrite(
729
+ rewrites.filter(rewrite => rewrite.fallback),
730
+ url.pathname,
731
+ context,
732
+ );
733
+ }
734
+ if (!result) return undefined;
735
+ if (result.destination) {
736
+ activateTestProxy(request.headers);
737
+ // The incoming `host` (and its forwarded twins) name THIS server; carrying
738
+ // them onto an outbound request makes the TLS handshake disagree with the
739
+ // destination's certificate. Let fetch derive them from the destination URL.
740
+ const headers = new Headers(request.headers);
741
+ for (const header of ['host', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto']) {
742
+ headers.delete(header);
743
+ }
744
+ const rewrittenRequest = new Request(result.destination, {
745
+ method: request.method,
746
+ headers,
747
+ ...(request.body ? { body: request.body, duplex: 'half' } : {}),
748
+ });
749
+ return fetch(rewrittenRequest);
750
+ }
751
+ const rewritten = new URL(url.href);
752
+ rewritten.pathname = result.pathname;
753
+ rewritten.search = result.search.toString();
754
+ recordCanonicalUrl(url);
755
+ recordRewrite(url, rewritten);
756
+ return { request: new Request(rewritten, request) };
757
+ };
758
+
759
+ /**
760
+ * Whether a request for `pathname` would 404 - the gate Next's `fallback` rewrites sit behind. A path is
761
+ * servable when the route table matches it or a built/public file backs it; `/_next/*` assets are always
762
+ * served by core. Divergence: dev has no built output, so only the route table is consulted.
763
+ */
764
+ function requestWouldMiss(config: ResolvedConfig, pathname: string): boolean {
765
+ if (pathname.startsWith('/_next/')) return false;
766
+ const runtime = getRequestRuntime();
767
+ if (runtime && selectRouteForRequest(runtime.routes, pathname)) return false;
768
+ return !servableFile(config, pathname);
769
+ }
770
+
771
+ function servableFile(config: ResolvedConfig, pathname: string): boolean {
772
+ const publicRoot = path.join(config.outPath, 'public');
773
+ const trimmed = pathname.replace(/^\/+|\/+$/g, '');
774
+ const file = trimmed ? path.join(publicRoot, trimmed) : publicRoot;
775
+ // Never let a traversal-shaped path claim to be servable (it is not).
776
+ const relative = path.relative(publicRoot, file);
777
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return false;
778
+ return (
779
+ existsSync(path.join(file, 'index.html')) ||
780
+ (trimmed !== '' && (existsSync(file) || existsSync(`${file}.html`)))
781
+ );
782
+ }
783
+
784
+ function parseCookieHeader(header: string | null): Record<string, string> {
785
+ if (!header) return {};
786
+ const cookies: Record<string, string> = {};
787
+ for (const part of header.split(';')) {
788
+ const eq = part.indexOf('=');
789
+ if (eq === -1) continue;
790
+ cookies[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
791
+ }
792
+ return cookies;
793
+ }
794
+
795
+ // ---------------------------------------------------------------------------
796
+ // Render extension: serialize a server-action function crossing the
797
+ // server->client boundary into the { [PROP_ACTION_MARKER]: id } marker.
798
+ //
799
+ // Id policy (Next-compatible), highest priority first:
800
+ // 1. caller-overridden $$id (version-skew simulation)
801
+ // 2. compile-tagged module-scope inline action ($$pnextAssignedId, 'if:')
802
+ // 3. tagged module-level 'use server' export (stable module id)
803
+ // 4. inline / .bind() / HOC closures -> a live per-render instance ('i:')
804
+ //
805
+ // Per-render instance scopes are keyed on the opaque renderKey (core's
806
+ // RenderOptions) so occurrence indices stay consistent within one render.
807
+ // ---------------------------------------------------------------------------
808
+
809
+ const instanceScopes = new WeakMap<object, InstanceScope>();
810
+
811
+ function serializeServerActionProp(
812
+ value: unknown,
813
+ context: ServerActionPropContext,
814
+ ): ActionRef | undefined {
815
+ if (typeof value !== 'function') return undefined;
816
+ if (!nextCompatEnabled(context.config)) return undefined;
817
+
818
+ const overridden = overriddenActionId(value);
819
+ if (overridden) return { [PROP_ACTION_MARKER]: overridden };
820
+
821
+ const assigned = assignedInlineActionId(value);
822
+ if (assigned) return { [PROP_ACTION_MARKER]: assigned };
823
+
824
+ const moduleId = serverActionId(value);
825
+ if (moduleId) return { [PROP_ACTION_MARKER]: moduleId };
826
+
827
+ if (context.identifiedOnly && !isMarkedInlineAction(value) && !opensWithUseServerDirective(value)) {
828
+ return undefined;
829
+ }
830
+
831
+ const scope = instanceScopeFor(context.renderKey);
832
+ const id = registerActionInstance(scope, value);
833
+ return { [PROP_ACTION_MARKER]: id };
834
+ }
835
+
836
+ /** Tag each function export of a discovered action module with its wire id. */
837
+ function tagActionModuleExports(
838
+ config: ResolvedConfig,
839
+ file: string,
840
+ module: Record<string, unknown>,
841
+ ): void {
842
+ if (!nextCompatEnabled(config) || !isClientActionModule(file)) return;
843
+ const root = clientActionModulesRoot() ?? config.root;
844
+ for (const [exportName, value] of Object.entries(module)) {
845
+ if (typeof value === 'function') {
846
+ tagServerAction(value, canonicalActionId(file, exportName, root));
847
+ }
848
+ }
849
+ }
850
+
851
+ /** Import + tag every discovered action module ahead of a page render. */
852
+ async function tagActionModulesForRender(
853
+ config: ResolvedConfig,
854
+ importModule: (file: string) => Promise<Record<string, unknown>>,
855
+ ): Promise<void> {
856
+ if (!nextCompatEnabled(config)) return;
857
+ for (const file of clientActionModuleFiles()) {
858
+ try {
859
+ tagActionModuleExports(config, file, await importModule(file));
860
+ } catch {
861
+ // Best-effort: an action module that fails to import here still POSTs via
862
+ // the client stub when JS runs; the form just renders without the hidden id.
863
+ }
864
+ }
865
+ }
866
+
867
+ function instanceScopeFor(renderKey: object): InstanceScope {
868
+ let scope = instanceScopes.get(renderKey);
869
+ if (!scope) {
870
+ const routeId = (renderKey as { route?: RouteManifestEntry }).route?.id ?? 'unknown';
871
+ scope = createInstanceScope(routeId);
872
+ instanceScopes.set(renderKey, scope);
873
+ }
874
+ return scope;
875
+ }