@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,526 @@
1
+ /**
2
+ * Chunk-count fold for the initial tier: merges chunks that are provably
3
+ * co-loaded (see `coLoadGroups`), so a merge can never over-fetch. Mechanically
4
+ * an esbuild re-bundle of a renaming barrel plus an import rewrite in every
5
+ * chunk that referenced a member.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { existsSync } from 'node:fs';
9
+ import { unlink } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { build, type BuildOptions, type Metafile } from 'esbuild';
12
+ import { clientProfile } from './profile';
13
+ import { readText, writeText } from '../utils/fs';
14
+ import { escapeRegex } from '../utils/source';
15
+
16
+ /** Unlinking an artifact a concurrent build already removed is not news. */
17
+ function ignoreMissing() {
18
+ return undefined;
19
+ }
20
+
21
+ /** Import/export forms the rewrite understands; anything else vetoes the fold. */
22
+ const namedClause = String.raw`\{([^}]*)\}`;
23
+
24
+ interface FoldOptions {
25
+ outDir: string;
26
+ metafile: Metafile;
27
+ /** esbuild flags of the build being folded, so the merge is emitted the same way. */
28
+ buildOptions: Pick<BuildOptions, 'minify' | 'sourcemap' | 'target' | 'format'>;
29
+ }
30
+
31
+ /**
32
+ * Merge the always-initial chunks of a finished client build. Returns a metafile
33
+ * with the merge applied, or the original untouched when nothing folds.
34
+ */
35
+ export async function foldInitialChunks(options: FoldOptions): Promise<Metafile> {
36
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
37
+ if (process.env.PNEXT_CHUNK_FOLD === '0') return options.metafile;
38
+ // The cost this removes is compressed-transfer only (a gzip header and
39
+ // dictionary reset per chunk), so the pass rides the minify flag.
40
+ if (!options.buildOptions.minify) return options.metafile;
41
+ return foldChunks(options);
42
+ }
43
+
44
+ /**
45
+ * Failure policy: a group that has not touched disk is skipped and reported (the
46
+ * build is correct without the fold); once it has, the metafile and the files on
47
+ * disk have diverged, so it rethrows. `PNEXT_CHUNK_FOLD=strict` throws for both.
48
+ */
49
+ async function foldChunks({ outDir, metafile, buildOptions }: FoldOptions): Promise<Metafile> {
50
+ let folded = metafile;
51
+ const groups = clientProfile.time('foldGroups', () => coLoadGroups(metafile));
52
+ clientProfile.count('foldGroup#', groups.length);
53
+ clientProfile.count('foldOutput#', Object.keys(metafile.outputs).length);
54
+ if (groups.length === 0) return folded;
55
+ // Output text is read once and kept in step with what each group writes, and
56
+ // the same state indexes specifiers — so a group visits only the files naming
57
+ // one of its members instead of re-reading and re-filtering every output.
58
+ const state = await clientProfile.timeAsync('foldRead', () => createFoldState(outDir, metafile));
59
+ for (const group of groups) {
60
+ const mutation = { started: false };
61
+ try {
62
+ folded = await foldGroup({ outDir, metafile: folded, buildOptions }, group, mutation, state);
63
+ } catch (error) {
64
+ if (mutation.started) throw error;
65
+ reportFoldFailure(group, error);
66
+ break;
67
+ }
68
+ }
69
+ return folded;
70
+ }
71
+
72
+ function reportFoldFailure(group: string[], error: unknown) {
73
+ const names = group.map(member => path.basename(member)).join(', ');
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
76
+ if (process.env.PNEXT_CHUNK_FOLD === 'strict') {
77
+ throw new Error(`client chunk fold failed for [${names}]: ${message}`);
78
+ }
79
+ console.warn(`pnext: client chunk fold skipped [${names}] — ${message}`);
80
+ }
81
+
82
+ async function foldGroup(
83
+ { outDir, metafile, buildOptions }: FoldOptions,
84
+ members: string[],
85
+ mutation: { started: boolean },
86
+ state: FoldState,
87
+ ): Promise<Metafile> {
88
+ if (members.length < 2) return metafile;
89
+
90
+ const chunksDir = path.join(outDir, 'chunks');
91
+ const memberNames = members.map(output => path.basename(output));
92
+ if (memberNames.some(name => !existsSync(path.join(chunksDir, name)))) return metafile;
93
+
94
+ const sources = new Map<string, string>();
95
+ for (const name of memberNames) sources.set(name, await readText(path.join(chunksDir, name)));
96
+
97
+ const aliases = memberAliases(memberNames, sources);
98
+ if (!aliases) return metafile;
99
+
100
+ // Every file that referenced a member has to be rewritten; if any of them
101
+ // uses a form the rewriter can't express (`import * as`, `export *`, a bare
102
+ // dynamic `import()` of a member), the fold is abandoned whole.
103
+ const rewrites = new Map<string, string>();
104
+ const rewrite = memberRewriter(memberNames, aliases, chunksDir);
105
+ const rewritable = clientProfile.time('foldRewrite', () => {
106
+ for (const file of state.referrersOf(memberNames)) {
107
+ if (memberNames.includes(path.basename(file))) continue;
108
+ const output = state.outputs.get(file);
109
+ if (!output) continue;
110
+ const next = rewrite(output.text, file);
111
+ if (next === null) return false;
112
+ if (next !== output.text) rewrites.set(file, next);
113
+ }
114
+ return true;
115
+ });
116
+ if (!rewritable) return metafile;
117
+
118
+ const merged = await clientProfile.timeAsync('foldBuild', () =>
119
+ buildMergedChunk(chunksDir, memberNames, aliases, buildOptions),
120
+ );
121
+ if (!merged) return metafile;
122
+
123
+ // Read-only up to here, so failing is skippable. From the first rewrite on the
124
+ // metafile and the files on disk must be kept in step, so failures are fatal.
125
+ mutation.started = true;
126
+
127
+ // The rewrites were produced against a placeholder href — the merged chunk's
128
+ // name is its own content hash and so isn't known until it is built.
129
+ await Promise.all(
130
+ [...rewrites].map(([file, source]) => {
131
+ const next = source.replaceAll(MERGED_PLACEHOLDER, merged.name);
132
+ state.set(file, foldOutput(next));
133
+ return writeText(file, next);
134
+ }),
135
+ );
136
+ await Promise.all(
137
+ memberNames.flatMap(name => [
138
+ unlink(path.join(chunksDir, name)).catch(ignoreMissing),
139
+ unlink(path.join(chunksDir, `${name}.map`)).catch(ignoreMissing),
140
+ ]),
141
+ );
142
+ for (const name of memberNames) state.remove(path.join(chunksDir, name));
143
+ // A later group can reference this merged chunk, and it is what the previous
144
+ // per-group re-walk picked up off disk; index it the same way.
145
+ state.set(path.join(chunksDir, merged.name), foldOutput(merged.text));
146
+ return mergeMetafile(metafile, members, path.join(chunksDir, merged.name), merged.bytes);
147
+ }
148
+
149
+ /**
150
+ * Chunks that are always downloaded together, grouped. Two chunks reachable from
151
+ * exactly the same load roots (route entries and `import()` targets) can merge
152
+ * without adding a byte to any load. Entries never fold - the shared chunk is
153
+ * what a lazily loaded island's CSS/runtime seam resolves against.
154
+ */
155
+ function coLoadGroups(metafile: Metafile) {
156
+ const dynamicTargets = new Set<string>();
157
+ for (const output of Object.values(metafile.outputs)) {
158
+ for (const imported of output.imports ?? []) {
159
+ if (imported.kind === 'dynamic-import') dynamicTargets.add(imported.path);
160
+ }
161
+ }
162
+ // esbuild records an `entryPoint` for the chunk behind every `import()` too,
163
+ // so dynamic targets have to be subtracted before what is left counts as a
164
+ // route entry.
165
+ const entries = Object.keys(metafile.outputs).filter(
166
+ file => metafile.outputs[file]?.entryPoint && !dynamicTargets.has(file),
167
+ );
168
+ if (entries.length === 0) return [];
169
+
170
+ const available = availability(metafile, entries, dynamicTargets);
171
+ const roots = [...available.keys()];
172
+ const groups = new Map<string, string[]>();
173
+ for (const file of Object.keys(metafile.outputs)) {
174
+ if (!file.endsWith('.js')) continue;
175
+ if (entries.includes(file) || dynamicTargets.has(file)) continue;
176
+ const mask = roots.map(root => (available.get(root)!.has(file) ? '1' : '0')).join('');
177
+ // A chunk no root can reach is dead output; leave it alone.
178
+ if (!mask.includes('1')) continue;
179
+ (groups.get(mask) ?? groups.set(mask, []).get(mask)!).push(file);
180
+ }
181
+ return [...groups.values()]
182
+ .filter(
183
+ group =>
184
+ group.length > 1 &&
185
+ // Only groups that touch a first-paint tier are worth a re-bundle; a
186
+ // purely deferred group is off-budget and pays build time for bytes
187
+ // nobody waits on.
188
+ entries.some(entry => available.get(entry)!.has(group[0]!)),
189
+ )
190
+ .map(group => [...group].sort());
191
+ }
192
+
193
+ /**
194
+ * What is guaranteed to be on the page when `file` runs: a root answers for
195
+ * itself, any other chunk with the intersection over every root that reaches it.
196
+ */
197
+ function availabilityAt(
198
+ available: Map<string, Set<string>>,
199
+ file: string,
200
+ memo: Map<string, Set<string>>,
201
+ ) {
202
+ const own = available.get(file);
203
+ if (own) return own;
204
+ // The fixpoint below asks for the same non-root importers once per round per
205
+ // target, and each answer scans every root's availability set — quadratic in
206
+ // roots. The memo is cleared whenever a round changes an answer.
207
+ const cached = memo.get(file);
208
+ if (cached) return cached;
209
+ let acc: Set<string> | undefined;
210
+ for (const set of available.values()) {
211
+ if (!set.has(file)) continue;
212
+ acc = acc === undefined ? new Set(set) : intersect(acc, set);
213
+ }
214
+ const answer = acc ?? new Set([file]);
215
+ memo.set(file, answer);
216
+ return answer;
217
+ }
218
+
219
+ /** Per load root, the chunks guaranteed to be on the page once it runs. */
220
+ function availability(metafile: Metafile, entries: string[], dynamicTargets: Set<string>) {
221
+ const available = new Map<string, Set<string>>();
222
+ const closures = new Map<string, Set<string>>();
223
+ for (const root of [...entries, ...dynamicTargets]) {
224
+ closures.set(root, staticClosure(metafile, root));
225
+ available.set(root, new Set(closures.get(root)));
226
+ }
227
+ const importersOf = new Map<string, string[]>();
228
+ for (const [file, output] of Object.entries(metafile.outputs)) {
229
+ for (const imported of output.imports ?? []) {
230
+ if (imported.kind !== 'dynamic-import') continue;
231
+ (importersOf.get(imported.path) ?? importersOf.set(imported.path, []).get(imported.path)!)
232
+ .push(file);
233
+ }
234
+ }
235
+ // `available` only ever grows, so iterating to a fixed point terminates; the
236
+ // bound is the number of roots (each round settles at least one).
237
+ const memo = new Map<string, Set<string>>();
238
+ for (let round = 0; round < dynamicTargets.size + 1; round += 1) {
239
+ let changed = false;
240
+ for (const target of dynamicTargets) {
241
+ const importers = importersOf.get(target) ?? [];
242
+ let inherited: Set<string> | undefined;
243
+ for (const importer of importers) {
244
+ const from = availabilityAt(available, importer, memo);
245
+ inherited = inherited === undefined ? new Set(from) : intersect(inherited, from);
246
+ }
247
+ const next = new Set([...closures.get(target)!, ...(inherited ?? [])]);
248
+ if (next.size !== available.get(target)!.size) {
249
+ available.set(target, next);
250
+ memo.clear();
251
+ changed = true;
252
+ }
253
+ }
254
+ if (!changed) break;
255
+ }
256
+ return available;
257
+ }
258
+
259
+ function staticClosure(metafile: Metafile, root: string) {
260
+ const seen = new Set([root]);
261
+ const queue = [root];
262
+ // `queue` grows inside the loop — a breadth-first walk of the chunk graph.
263
+ for (const current of queue) {
264
+ for (const imported of metafile.outputs[current]?.imports ?? []) {
265
+ if (imported.kind !== 'import-statement') continue;
266
+ if (seen.has(imported.path)) continue;
267
+ seen.add(imported.path);
268
+ queue.push(imported.path);
269
+ }
270
+ }
271
+ return seen;
272
+ }
273
+
274
+ function intersect(a: Set<string>, b: Set<string>) {
275
+ return new Set([...a].filter(item => b.has(item)));
276
+ }
277
+
278
+ /**
279
+ * Per-member `exported name -> alias`. Chunk exports are minified per chunk and
280
+ * collide across chunks, so the barrel renames them apart.
281
+ */
282
+ function memberAliases(memberNames: string[], sources: Map<string, string>) {
283
+ const aliases = new Map<string, Map<string, string>>();
284
+ for (const [index, name] of memberNames.entries()) {
285
+ const source = sources.get(name)!;
286
+ // A star re-export can't be aliased name-by-name; nothing to fold safely.
287
+ if (/export\s*\*/.test(source)) return undefined;
288
+ // The export clause is the chunk's last one — a linked sourcemap comment
289
+ // can follow it, so anchor on "last `export{…}` that isn't a re-export"
290
+ // rather than on end-of-file.
291
+ const clauses = [...source.matchAll(new RegExp(String.raw`export\s*${namedClause}\s*(?!from)`, 'g'))];
292
+ const clause = clauses.at(-1);
293
+ const names = new Map<string, string>();
294
+ for (const part of clause?.[1]?.split(',') ?? []) {
295
+ const exported = part.split(' as ').at(-1)?.trim();
296
+ if (exported) names.set(exported, `$${index}${exported}`);
297
+ }
298
+ aliases.set(name, names);
299
+ }
300
+ return aliases;
301
+ }
302
+
303
+ /**
304
+ * The rewrite for one group, compiled once: points a file's member imports at the
305
+ * merged chunk, renaming each binding to its barrel alias. Answers `null` when a
306
+ * member is referenced through a form the fold cannot rewrite.
307
+ */
308
+ function memberRewriter(
309
+ memberNames: string[],
310
+ aliases: Map<string, Map<string, string>>,
311
+ chunksDir: string,
312
+ ) {
313
+ const memberPattern = memberNames.map(escapeRegex).join('|');
314
+ const specifier = String.raw`"\.[^"]*(?:${memberPattern})"`;
315
+ // Forms that can't be expressed against a renaming barrel.
316
+ const vetoes = [
317
+ new RegExp(String.raw`import\s*\*\s*as\s*[\w$]+\s*from\s*${specifier}`),
318
+ new RegExp(String.raw`export\s*\*[^;]*from\s*${specifier}`),
319
+ new RegExp(String.raw`import\(\s*${specifier}`),
320
+ ];
321
+ const namedImport = new RegExp(
322
+ String.raw`(import|export)\s*${namedClause}\s*from\s*(${specifier})`,
323
+ 'g',
324
+ );
325
+ const sideEffectImport = new RegExp(String.raw`import\s*(${specifier})`, 'g');
326
+
327
+ const nameOf = (spec: string) => memberNames.find(name => spec.includes(name))!;
328
+ const rename = (clause: string, member: string) =>
329
+ clause
330
+ .split(',')
331
+ .map(part => {
332
+ const [imported, local] = part.split(' as ').map(item => item.trim());
333
+ if (!imported) return part;
334
+ const alias = aliases.get(member)?.get(imported);
335
+ if (!alias) return part;
336
+ return `${alias} as ${local ?? imported}`;
337
+ })
338
+ .join(',');
339
+
340
+ return (source: string, file: string) => {
341
+ for (const veto of vetoes) if (veto.test(source)) return null;
342
+ const mergedHref = path
343
+ .relative(path.dirname(file), path.join(chunksDir, MERGED_PLACEHOLDER))
344
+ .split(path.sep)
345
+ .join('/');
346
+ const href = mergedHref.startsWith('.') ? mergedHref : `./${mergedHref}`;
347
+ return source
348
+ .replace(
349
+ namedImport,
350
+ (_all, keyword: string, clause: string, spec: string) =>
351
+ `${keyword}{${rename(clause, nameOf(spec))}}from"${href}"`,
352
+ )
353
+ // Side-effect-only imports of a member collapse onto the merged chunk.
354
+ .replace(sideEffectImport, () => `import"${href}"`);
355
+ };
356
+ }
357
+
358
+ /** Stands in for the merged chunk's final (content-hashed) name. */
359
+ const MERGED_PLACEHOLDER = 'pnext-merged';
360
+
361
+ async function buildMergedChunk(
362
+ chunksDir: string,
363
+ memberNames: string[],
364
+ aliases: Map<string, Map<string, string>>,
365
+ buildOptions: FoldOptions['buildOptions'],
366
+ ) {
367
+ const barrel = memberNames
368
+ .map(name => {
369
+ const names = aliases.get(name)!;
370
+ const spec = JSON.stringify(`./${name}`);
371
+ if (names.size === 0) return `import ${spec};`;
372
+ const clause = [...names].map(([exported, alias]) => `${exported} as ${alias}`).join(',');
373
+ return `export {${clause}} from ${spec};`;
374
+ })
375
+ .join('\n');
376
+
377
+ const barrelPath = path.join(chunksDir, '__pnext-fold-barrel.js');
378
+ await writeText(barrelPath, barrel);
379
+ try {
380
+ const result = await build({
381
+ minify: buildOptions.minify,
382
+ sourcemap: buildOptions.sourcemap,
383
+ target: buildOptions.target,
384
+ format: buildOptions.format ?? 'esm',
385
+ bundle: true,
386
+ entryPoints: [barrelPath],
387
+ outfile: path.join(chunksDir, '__pnext-fold-merged.js'),
388
+ write: false,
389
+ // Only the members are pulled in; every other chunk reference (including
390
+ // the `import()`s that keep the idle tier deferred) passes through as-is.
391
+ plugins: [
392
+ {
393
+ name: 'pnext-fold-members',
394
+ setup(builder) {
395
+ builder.onResolve({ filter: /.*/ }, args => {
396
+ if (args.kind === 'entry-point') return undefined;
397
+ const base = path.basename(args.path);
398
+ if (memberNames.includes(base)) return { path: path.join(chunksDir, base) };
399
+ return { path: args.path, external: true };
400
+ });
401
+ },
402
+ },
403
+ ],
404
+ });
405
+ const js = result.outputFiles?.find(file => file.path.endsWith('.js'));
406
+ if (!js) return undefined;
407
+ const hash = createHash('sha256').update(js.contents).digest('hex').slice(0, 8).toUpperCase();
408
+ const name = `shared-init-${hash}.js`;
409
+ const map = result.outputFiles?.find(file => file.path.endsWith('.js.map'));
410
+ let contents = new TextDecoder().decode(js.contents);
411
+ if (map) {
412
+ contents = contents.replace(/# sourceMappingURL=.*$/m, `# sourceMappingURL=${name}.map`);
413
+ await writeText(path.join(chunksDir, `${name}.map`), new TextDecoder().decode(map.contents));
414
+ }
415
+ await writeText(path.join(chunksDir, name), contents);
416
+ return { name, text: contents, bytes: Buffer.byteLength(contents) };
417
+ } finally {
418
+ await unlink(barrelPath).catch(ignoreMissing);
419
+ }
420
+ }
421
+
422
+ /** An output file's text plus the relative specifiers it imports through. */
423
+ interface FoldOutput {
424
+ text: string;
425
+ specifiers: string[];
426
+ }
427
+
428
+ function foldOutput(text: string): FoldOutput {
429
+ return { text, specifiers: [...text.matchAll(/"\.[^"]*"/g)].map(match => match[0]) };
430
+ }
431
+
432
+ /** Every output's text plus a basename->referrers index, read once. Chunk names
433
+ * are content-hashed, so a specifier naming a member spells it as its basename. */
434
+ interface FoldState {
435
+ outputs: Map<string, FoldOutput>;
436
+ /** Files whose specifiers name any of `names`, each visited once. */
437
+ referrersOf(names: string[]): Iterable<string>;
438
+ set(file: string, output: FoldOutput): void;
439
+ remove(file: string): void;
440
+ }
441
+
442
+ async function createFoldState(outDir: string, metafile: Metafile): Promise<FoldState> {
443
+ const outputs = new Map<string, FoldOutput>();
444
+ const referrers = new Map<string, Set<string>>();
445
+
446
+ const index = (file: string, output: FoldOutput, add: boolean) => {
447
+ for (const spec of output.specifiers) {
448
+ const base = path.basename(spec.slice(1, -1));
449
+ const set = referrers.get(base) ?? referrers.set(base, new Set()).get(base)!;
450
+ if (add) set.add(file);
451
+ else set.delete(file);
452
+ }
453
+ };
454
+
455
+ for (const name of Object.keys(metafile.outputs)) {
456
+ if (!name.endsWith('.js')) continue;
457
+ const resolved = path.resolve(name);
458
+ const local = existsSync(resolved) ? resolved : path.join(outDir, path.basename(name));
459
+ if (outputs.has(local) || !existsSync(local)) continue;
460
+ const output = foldOutput(await readText(local));
461
+ outputs.set(local, output);
462
+ index(local, output, true);
463
+ }
464
+
465
+ return {
466
+ outputs,
467
+ referrersOf(names) {
468
+ const seen = new Set<string>();
469
+ for (const name of names) for (const file of referrers.get(name) ?? []) seen.add(file);
470
+ return seen;
471
+ },
472
+ set(file, output) {
473
+ const previous = outputs.get(file);
474
+ if (previous) index(file, previous, false);
475
+ outputs.set(file, output);
476
+ index(file, output, true);
477
+ },
478
+ remove(file) {
479
+ const previous = outputs.get(file);
480
+ if (previous) index(file, previous, false);
481
+ outputs.delete(file);
482
+ },
483
+ };
484
+ }
485
+
486
+ /** The shipped graph, with the members collapsed into the merged chunk. */
487
+ function mergeMetafile(
488
+ metafile: Metafile,
489
+ members: string[],
490
+ mergedPath: string,
491
+ bytes: number,
492
+ ): Metafile {
493
+ const memberSet = new Set(members);
494
+ const merged = path.relative(process.cwd(), mergedPath);
495
+ const outputs: Metafile['outputs'] = {};
496
+ const mergedImports: Metafile['outputs'][string]['imports'] = [];
497
+ const mergedInputs: Metafile['outputs'][string]['inputs'] = {};
498
+
499
+ for (const [file, output] of Object.entries(metafile.outputs)) {
500
+ const remap = (list: typeof output.imports) => {
501
+ const seen = new Set<string>();
502
+ return list.flatMap(imported => {
503
+ const target = memberSet.has(imported.path) ? merged : imported.path;
504
+ const key = `${target}:${imported.kind}`;
505
+ if (seen.has(key)) return [];
506
+ seen.add(key);
507
+ return [{ ...imported, path: target }];
508
+ });
509
+ };
510
+ if (memberSet.has(file)) {
511
+ Object.assign(mergedInputs, output.inputs);
512
+ for (const imported of remap(output.imports ?? [])) {
513
+ if (imported.path !== merged) mergedImports.push(imported);
514
+ }
515
+ continue;
516
+ }
517
+ outputs[file] = { ...output, imports: remap(output.imports ?? []) };
518
+ }
519
+ outputs[merged] = {
520
+ imports: mergedImports,
521
+ exports: [],
522
+ inputs: mergedInputs,
523
+ bytes,
524
+ };
525
+ return { ...metafile, outputs };
526
+ }