@wular/pnext 0.0.4 → 0.0.6

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