@wular/pnext 0.0.4 → 0.0.7

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 (395) hide show
  1. package/README.md +22 -16
  2. package/bin/pnext +6 -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 +87 -22
  8. package/reference/config.md +40 -93
  9. package/reference/css.md +42 -33
  10. package/reference/dev.md +26 -33
  11. package/reference/env.md +18 -20
  12. package/reference/getting-started.md +132 -0
  13. package/reference/metadata.md +31 -46
  14. package/reference/navigation.md +65 -105
  15. package/reference/performance.md +21 -142
  16. package/reference/rendering.md +78 -75
  17. package/reference/routing.md +97 -107
  18. package/reference/typegen.md +27 -29
  19. package/src/api/cache.ts +36 -37
  20. package/src/api/client-cache.ts +2 -2
  21. package/src/api/client-navigation.ts +121 -114
  22. package/src/api/dynamic.tsx +58 -55
  23. package/src/api/link.tsx +52 -53
  24. package/src/api/navigation.ts +59 -58
  25. package/src/api/server.ts +152 -144
  26. package/src/api/suspense.ts +4 -4
  27. package/src/cli/adapters/vercel-warm.ts +126 -121
  28. package/src/cli/adapters/vercel.ts +437 -443
  29. package/src/cli/analyze.ts +333 -144
  30. package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
  31. package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
  32. package/src/cli/build.ts +976 -978
  33. package/src/cli/create.ts +72 -64
  34. package/src/cli/dev.ts +116 -116
  35. package/src/cli/index.ts +94 -85
  36. package/src/cli/migrate/package-json.ts +73 -71
  37. package/src/cli/migrate/report.ts +32 -33
  38. package/src/cli/migrate/{index.ts → run.ts} +48 -49
  39. package/src/cli/migrate/scan.ts +46 -46
  40. package/src/cli/migrate/spinner.ts +9 -9
  41. package/src/cli/migrate/tsconfig.ts +35 -35
  42. package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
  43. package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
  44. package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
  45. package/src/cli/start.ts +65 -66
  46. package/src/{typegen.ts → cli/typegen.ts} +59 -59
  47. package/src/client/build.ts +802 -768
  48. package/src/client/chunk-fold.ts +245 -240
  49. package/src/client/{paths.ts → chunk-name.ts} +6 -6
  50. package/src/client/entry.ts +162 -147
  51. package/src/client/prebuilt.ts +231 -223
  52. package/src/client/profile.ts +29 -29
  53. package/src/client/react-compiler.ts +20 -15
  54. package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
  55. package/src/client/reference-stub.ts +51 -51
  56. package/src/client/reference.ts +19 -19
  57. package/src/{api → client}/router/events.ts +32 -17
  58. package/src/{api → client}/router/history.ts +16 -16
  59. package/src/{api → client}/router/hub.ts +52 -53
  60. package/src/{api/router.ts → client/router/index.ts} +61 -60
  61. package/src/{api → client}/router/policies.ts +24 -24
  62. package/src/{api → client}/router/runtime.ts +2022 -1995
  63. package/src/{api → client}/router/types.ts +98 -98
  64. package/src/compat/actions/client-plugin.ts +42 -43
  65. package/src/compat/actions/client-stub.ts +14 -14
  66. package/src/compat/actions/{action-client.ts → client.ts} +194 -193
  67. package/src/compat/actions/config.ts +63 -64
  68. package/src/compat/actions/detect.ts +68 -68
  69. package/src/compat/actions/discovery.ts +105 -105
  70. package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
  71. package/src/compat/actions/early-submit.ts +1 -1
  72. package/src/compat/actions/endpoint.ts +198 -198
  73. package/src/compat/actions/extensions.ts +811 -0
  74. package/src/compat/actions/flight.ts +23 -23
  75. package/src/compat/actions/form-state.ts +34 -34
  76. package/src/compat/actions/hoist.ts +382 -236
  77. package/src/compat/actions/ids.ts +7 -12
  78. package/src/compat/actions/index.ts +8 -8
  79. package/src/compat/actions/instances.ts +46 -46
  80. package/src/compat/actions/origin.ts +47 -48
  81. package/src/compat/actions/protocol.ts +22 -22
  82. package/src/compat/actions/registry.ts +19 -19
  83. package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
  84. package/src/compat/actions/rewrite.ts +125 -125
  85. package/src/compat/actions/{action-router.ts → router.ts} +10 -10
  86. package/src/compat/actions/serve.ts +132 -136
  87. package/src/compat/actions/server-tag.ts +4 -4
  88. package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
  89. package/src/compat/actions/unrecognized-error.ts +4 -4
  90. package/src/compat/{index.ts → aliases.ts} +112 -109
  91. package/src/compat/bundler/bun-externals.ts +18 -18
  92. package/src/compat/bundler/cjs-exports.ts +271 -223
  93. package/src/compat/bundler/config.ts +116 -111
  94. package/src/compat/bundler/externals.ts +12 -12
  95. package/src/compat/bundler/import-meta-url.ts +19 -19
  96. package/src/compat/bundler/modularize-imports.ts +36 -33
  97. package/src/compat/bundler/new-url-asset.ts +22 -24
  98. package/src/compat/bundler/optimize-package-imports.ts +111 -107
  99. package/src/compat/bundler/polyfill.ts +28 -28
  100. package/src/compat/bundler/react-compiler.ts +35 -29
  101. package/src/compat/bundler/react-profiler.tsx +11 -11
  102. package/src/compat/bundler/relay-transform.ts +48 -47
  103. package/src/compat/bundler/require-context.ts +119 -113
  104. package/src/compat/bundler/resolve-extensions.ts +19 -19
  105. package/src/compat/bundler/source-cache.ts +25 -25
  106. package/src/compat/bundler/static-imports.ts +7 -7
  107. package/src/compat/bundler/symlink-imports.ts +46 -46
  108. package/src/compat/bundler/tsconfig-paths.ts +13 -15
  109. package/src/compat/bundler/wasm.ts +58 -60
  110. package/src/compat/bundler/webpack-loaders.ts +254 -241
  111. package/src/compat/bundler/worker.ts +101 -104
  112. package/src/compat/cache/build-flags.ts +29 -29
  113. package/src/compat/cache/build-prerender-errors.ts +40 -44
  114. package/src/compat/cache/custom-handler.ts +60 -53
  115. package/src/compat/cache/fetch-patch.ts +240 -240
  116. package/src/compat/cache/handler.ts +27 -31
  117. package/src/compat/cache/modern-handler.ts +148 -126
  118. package/src/compat/cache/resume-data-cache.ts +47 -45
  119. package/src/compat/cache/revalidate.ts +233 -232
  120. package/src/compat/cache/runtime-error.ts +35 -35
  121. package/src/compat/cache/use-cache-transform.ts +442 -417
  122. package/src/compat/cache/use-cache.ts +650 -614
  123. package/src/compat/cache-control.ts +140 -142
  124. package/src/compat/client/base-path.ts +21 -20
  125. package/src/compat/client/css-order.ts +18 -18
  126. package/src/compat/client/errors/bare-boundary.ts +11 -11
  127. package/src/compat/client/errors/control-flow.ts +23 -23
  128. package/src/compat/client/errors/error-boundary.ts +61 -61
  129. package/src/compat/client/errors/global-error.ts +101 -91
  130. package/src/compat/client/errors/install.ts +71 -72
  131. package/src/compat/client/errors/lazy.ts +23 -23
  132. package/src/compat/client/errors/primitive-throw.ts +47 -45
  133. package/src/compat/client/errors/soft-refresh.ts +4 -4
  134. package/src/compat/client/link-status.ts +33 -33
  135. package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
  136. package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
  137. package/src/compat/client/navigation-scroll.ts +63 -63
  138. package/src/compat/client/optimistic-routing.ts +93 -88
  139. package/src/compat/client/prefetch-cache.ts +23 -24
  140. package/src/compat/client/route-announcer.ts +35 -35
  141. package/src/compat/client/segment-cache-policy.ts +20 -20
  142. package/src/compat/client/segment-cache.ts +309 -315
  143. package/src/compat/client/segment-prefetch.ts +127 -132
  144. package/src/compat/client/trailing-slash.ts +5 -4
  145. package/src/compat/css/chunking.ts +113 -116
  146. package/src/compat/css/inline-css.ts +21 -21
  147. package/src/compat/css/lightningcss.ts +37 -38
  148. package/src/compat/css/modules.ts +161 -175
  149. package/src/compat/css/nonce.ts +7 -7
  150. package/src/compat/css/sass-plugin.ts +18 -21
  151. package/src/compat/css/sass.ts +150 -152
  152. package/src/compat/css/styled-jsx-runtime.ts +27 -27
  153. package/src/compat/css/styled-jsx.ts +21 -21
  154. package/src/compat/edge-runtime.ts +27 -27
  155. package/src/compat/{adapter → export}/build-complete.ts +72 -75
  156. package/src/compat/export/client.ts +29 -31
  157. package/src/compat/export/{index.ts → emit.ts} +111 -110
  158. package/src/compat/export/standalone.ts +62 -54
  159. package/src/compat/image-optimizer/cache.ts +51 -49
  160. package/src/compat/image-optimizer/detect.ts +52 -52
  161. package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
  162. package/src/compat/image-optimizer/source.ts +77 -80
  163. package/src/compat/lifecycle/after-scope.ts +26 -26
  164. package/src/compat/lifecycle/after.ts +48 -45
  165. package/src/compat/lifecycle/error-funnel.ts +98 -102
  166. package/src/compat/lifecycle/error-serialize.ts +30 -26
  167. package/src/compat/lifecycle/error-ui.ts +67 -32
  168. package/src/compat/lifecycle/instrumentation-client.ts +36 -38
  169. package/src/compat/lifecycle/instrumentation.ts +85 -85
  170. package/src/compat/lifecycle/node-console.ts +11 -11
  171. package/src/compat/lifecycle/testmode.ts +160 -132
  172. package/src/compat/mdx/compile.ts +60 -57
  173. package/src/compat/mdx/plugin.ts +11 -11
  174. package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
  175. package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
  176. package/src/compat/metadata.ts +75 -81
  177. package/src/compat/next/cache.ts +68 -69
  178. package/src/compat/next/canonical-url.ts +9 -9
  179. package/src/compat/next/client-cache.ts +19 -19
  180. package/src/compat/next/client-navigation.ts +112 -116
  181. package/src/compat/next/client-only.ts +1 -1
  182. package/src/compat/next/client-script.tsx +99 -93
  183. package/src/compat/next/client-server.ts +10 -10
  184. package/src/compat/next/config-loader.ts +176 -173
  185. package/src/compat/next/config.ts +7 -7
  186. package/src/compat/next/constants.cjs +6 -6
  187. package/src/compat/next/constants.ts +6 -6
  188. package/src/compat/next/custom-server.ts +26 -24
  189. package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
  190. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
  191. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
  192. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
  193. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
  194. package/src/compat/next/dynamic.tsx +21 -18
  195. package/src/compat/next/error.tsx +52 -52
  196. package/src/compat/next/font/cache.ts +74 -74
  197. package/src/compat/next/font/google.ts +2 -2
  198. package/src/compat/next/font/index.ts +1 -1
  199. package/src/compat/next/font/local.ts +3 -3
  200. package/src/compat/next/font/runtime-client.ts +17 -15
  201. package/src/compat/next/font/runtime.ts +443 -408
  202. package/src/compat/next/font/shared.ts +120 -108
  203. package/src/compat/next/form.tsx +63 -63
  204. package/src/compat/next/head.tsx +2 -2
  205. package/src/compat/next/headers.ts +103 -95
  206. package/src/compat/next/image/client.tsx +220 -0
  207. package/src/compat/next/image/config.ts +56 -58
  208. package/src/compat/next/image/optimizer.ts +40 -36
  209. package/src/compat/next/image/patterns.ts +37 -40
  210. package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
  211. package/src/compat/next/image/shared.ts +65 -57
  212. package/src/compat/next/image/static-metadata.ts +98 -107
  213. package/src/compat/next/image/validate.ts +79 -88
  214. package/src/compat/next/image.tsx +19 -23
  215. package/src/compat/next/index.ts +1 -1
  216. package/src/compat/next/legacy-image.tsx +59 -60
  217. package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
  218. package/src/compat/next/link.tsx +158 -158
  219. package/src/compat/next/navigation.cjs +12 -3
  220. package/src/compat/next/navigation.ts +48 -50
  221. package/src/compat/next/offline.ts +27 -27
  222. package/src/compat/next/og.ts +121 -124
  223. package/src/compat/next/preferred-region.ts +13 -14
  224. package/src/compat/next/redirects.ts +58 -56
  225. package/src/compat/next/resource-hints.ts +73 -76
  226. package/src/compat/next/rewrites.ts +130 -133
  227. package/src/compat/next/root-params.ts +45 -45
  228. package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
  229. package/src/compat/next/router.cjs +4 -2
  230. package/src/compat/next/router.ts +58 -61
  231. package/src/compat/next/script.tsx +108 -108
  232. package/src/compat/next/server-only.ts +1 -1
  233. package/src/compat/next/server.ts +19 -19
  234. package/src/compat/next/svgr.ts +18 -17
  235. package/src/compat/next/telemetry.ts +24 -24
  236. package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
  237. package/src/compat/next/user-agent.ts +53 -49
  238. package/src/compat/next/web-vitals.ts +22 -24
  239. package/src/compat/otel/api.ts +41 -41
  240. package/src/compat/otel/client-trace-metadata.ts +25 -27
  241. package/src/compat/otel/fetch-span.ts +29 -29
  242. package/src/compat/otel/tracer.ts +331 -331
  243. package/src/compat/pages/api.ts +456 -0
  244. package/src/compat/pages/client-plugin.ts +36 -36
  245. package/src/compat/pages/router-state.ts +34 -34
  246. package/src/compat/pages/{index.ts → router.ts} +130 -135
  247. package/src/compat/ppr/io.ts +12 -12
  248. package/src/compat/ppr/missing-root-params.ts +34 -38
  249. package/src/compat/ppr/root-params-scan.ts +66 -66
  250. package/src/compat/ppr/root-params-transform.ts +24 -26
  251. package/src/compat/ppr/root-params.ts +30 -30
  252. package/src/compat/ppr/segment-config-incompat.ts +6 -7
  253. package/src/compat/protocol.ts +71 -70
  254. package/src/compat/react/action-state.ts +47 -48
  255. package/src/compat/react/client-lite.ts +15 -15
  256. package/src/compat/react/client.ts +4 -4
  257. package/src/compat/react/compiler-runtime.ts +11 -11
  258. package/src/compat/react/dom-client.ts +44 -44
  259. package/src/compat/react/dom-react-server.ts +10 -16
  260. package/src/compat/react/dom-server.ts +10 -10
  261. package/src/compat/react/dom.ts +52 -52
  262. package/src/compat/react/hooks-extra.ts +34 -35
  263. package/src/compat/react/parity.ts +64 -61
  264. package/src/compat/react/preact.ts +81 -82
  265. package/src/compat/react/react-server.ts +28 -28
  266. package/src/compat/react/router-shim.ts +1 -1
  267. package/src/compat/react/server-component-use.ts +8 -8
  268. package/src/compat/react/server-inserted-html.ts +30 -31
  269. package/src/compat/react/server.ts +53 -55
  270. package/src/compat/react/use.ts +32 -32
  271. package/src/compat/react/view-transition.ts +20 -20
  272. package/src/compat/register/actions.ts +35 -824
  273. package/src/compat/register/boot.ts +41 -41
  274. package/src/compat/register/build-tier.ts +5 -5
  275. package/src/compat/register/build.ts +74 -70
  276. package/src/compat/register/bundler.ts +161 -156
  277. package/src/compat/register/cache.ts +20 -20
  278. package/src/compat/register/client-errors.ts +3 -3
  279. package/src/compat/register/config.ts +6 -6
  280. package/src/compat/register/css-extras.ts +30 -34
  281. package/src/compat/register/edge-runtime.ts +3 -3
  282. package/src/compat/register/errors.ts +10 -12
  283. package/src/compat/register/export.ts +16 -16
  284. package/src/compat/register/font.ts +11 -11
  285. package/src/compat/register/hooks.ts +2 -2
  286. package/src/compat/register/image.ts +40 -40
  287. package/src/compat/register/index.ts +59 -62
  288. package/src/compat/register/instrumentation-client.ts +10 -10
  289. package/src/compat/register/lifecycle.ts +25 -28
  290. package/src/compat/register/mdx.ts +10 -10
  291. package/src/compat/register/middleware.ts +226 -16
  292. package/src/compat/register/otel.ts +80 -88
  293. package/src/compat/register/pages-api.ts +10 -463
  294. package/src/compat/register/ppr.ts +16 -16
  295. package/src/compat/register/protocol.ts +17 -18
  296. package/src/compat/register/proxy.ts +49 -51
  297. package/src/compat/register/render.ts +79 -76
  298. package/src/compat/register/routing.ts +162 -159
  299. package/src/compat/register/segment.ts +24 -1897
  300. package/src/compat/register/static-image.ts +3 -3
  301. package/src/compat/register/{misc.ts → taint.ts} +9 -9
  302. package/src/compat/register/typed-routes.ts +14 -14
  303. package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
  304. package/src/compat/register/validation.ts +18 -18
  305. package/src/compat/segment/loading-boundary.ts +43 -45
  306. package/src/compat/segment/page-slot.ts +69 -69
  307. package/src/compat/segment/serve.ts +1884 -0
  308. package/src/compat/segment/tree.ts +113 -112
  309. package/src/compat/segment/vary-key.ts +38 -38
  310. package/src/compat/segment/vary-params.ts +138 -142
  311. package/src/compat/static-params.ts +14 -12
  312. package/src/compat/tsconfig-defaults.ts +87 -91
  313. package/src/compat/typecheck/{index.ts → check.ts} +234 -212
  314. package/src/compat/typecheck/worker.ts +15 -12
  315. package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
  316. package/src/compat/typed-routes/manifest.ts +174 -170
  317. package/src/compat/typed-routes/typegen.ts +127 -110
  318. package/src/compat/validation/errors.ts +16 -19
  319. package/src/compat/validation/prerender-diagnostics.ts +521 -504
  320. package/src/compat/validation/{index.ts → validate.ts} +647 -648
  321. package/src/compat-bootstrap.ts +16 -16
  322. package/src/config.ts +86 -69
  323. package/src/css/build.ts +230 -227
  324. package/src/css/postcss.ts +79 -80
  325. package/src/css/worker.ts +14 -15
  326. package/src/dev/client-actions.ts +10 -10
  327. package/src/dev/client-chunk-store.ts +27 -27
  328. package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
  329. package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
  330. package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
  331. package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
  332. package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
  333. package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
  334. package/src/dev/server.ts +804 -820
  335. package/src/env.ts +46 -43
  336. package/src/extensions.ts +491 -478
  337. package/src/index.ts +8 -8
  338. package/src/internal.ts +20 -23
  339. package/src/{islands → render}/boundary-error.ts +3 -3
  340. package/src/render/hooks.ts +71 -71
  341. package/src/render/island-context.ts +14 -14
  342. package/src/render/metadata.ts +310 -310
  343. package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
  344. package/src/{ppr.ts → render/ppr.ts} +244 -245
  345. package/src/render/renderer.ts +2087 -2082
  346. package/src/render/resource-hints.ts +16 -17
  347. package/src/render/slots.tsx +224 -235
  348. package/src/{islands → render}/static-children.ts +9 -12
  349. package/src/{islands → render}/static-slots.ts +37 -37
  350. package/src/{cache/context.ts → request/cache.ts} +20 -20
  351. package/src/request/context.ts +107 -107
  352. package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
  353. package/src/resolve/engine.ts +90 -77
  354. package/src/resolve/imports.ts +475 -463
  355. package/src/resolve/scan-facts.ts +318 -296
  356. package/src/resolve/source-text.ts +37 -37
  357. package/src/{dynamic → resolve}/tree-shake.ts +132 -128
  358. package/src/routing/forwarded.ts +19 -19
  359. package/src/routing/handler.ts +84 -91
  360. package/src/routing/href.ts +87 -70
  361. package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
  362. package/src/{proxy.ts → routing/proxy.ts} +306 -312
  363. package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
  364. package/src/routing/routes.ts +827 -815
  365. package/src/routing/slots.ts +164 -160
  366. package/src/runtime/loader.ts +954 -0
  367. package/src/{dev → runtime}/module-cache.ts +309 -287
  368. package/src/{dev → runtime}/module-generations.ts +9 -9
  369. package/src/{dev → runtime}/module-transform.ts +81 -72
  370. package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
  371. package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
  372. package/src/runtime/vendor.ts +428 -406
  373. package/src/styles.d.ts +9 -0
  374. package/src/types.ts +324 -335
  375. package/src/utils/ansi.ts +5 -5
  376. package/src/utils/{source.ts → code.ts} +15 -12
  377. package/src/utils/content-type.ts +3 -3
  378. package/src/utils/decode.ts +2 -2
  379. package/src/utils/dev-profile.ts +13 -13
  380. package/src/utils/error-log.ts +6 -6
  381. package/src/utils/esbuild.ts +18 -18
  382. package/src/utils/fs-cache.ts +13 -13
  383. package/src/utils/fs.ts +57 -49
  384. package/src/utils/html.ts +20 -24
  385. package/src/utils/native-require.ts +8 -8
  386. package/src/utils/serialize.ts +139 -146
  387. package/src/utils/verbose.ts +18 -18
  388. package/reference/overview.md +0 -35
  389. package/src/cli/analyze-print.ts +0 -181
  390. package/src/compat/middleware/manifest.ts +0 -210
  391. package/src/compat/next/image-client.tsx +0 -215
  392. package/src/compat/next/link-usage.ts +0 -29
  393. package/src/css/index.ts +0 -2
  394. package/src/render/index.ts +0 -1
  395. package/src/style-modules.d.ts +0 -9
@@ -1,7 +1,7 @@
1
- import { createHash } from 'node:crypto';
2
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
- import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises';
4
- import path from 'node:path';
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
3
+ import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises'
4
+ import path from 'node:path'
5
5
  import {
6
6
  build,
7
7
  type BuildOptions,
@@ -12,23 +12,27 @@ import {
12
12
  type Plugin,
13
13
  type PluginBuild,
14
14
  type ResolveResult,
15
- } from 'esbuild';
16
- import { foldInitialChunks } from './chunk-fold';
17
- import { clientEntryName } from './paths';
18
- import { clientProfile } from './profile';
19
- import { publicEnvDefines, type ResolvedConfig } from '../config';
20
- import { cssModuleClientPlugin } from '../css';
21
- import { withAssetPrefix } from '../css/build';
22
- import { devDynamicSplitEnabled, rewriteDeferredDynamicImports, rewriteLiteralDynamicCalls } from '../dynamic/source';
23
- import { scanFacts } from '../resolve/scan-facts';
24
- import { readSourceText } from '../resolve/source-text';
25
- import { ensureDir, listFiles, readText, writeText } from '../utils/fs';
26
- import { escapeRegex } from '../utils/source';
15
+ } from 'esbuild'
16
+ import { foldInitialChunks } from './chunk-fold'
17
+ import { clientEntryName } from './chunk-name'
18
+ import { clientProfile } from './profile'
19
+ import { frameworkRuntimeAliasEntries, publicEnvDefines, type ResolvedConfig } from '../config'
20
+ import { cssModuleClientPlugin } from '../css/build'
21
+ import { withAssetPrefix } from '../css/build'
22
+ import {
23
+ devDynamicSplitEnabled,
24
+ rewriteDeferredDynamicImports,
25
+ rewriteLiteralDynamicCalls,
26
+ } from '../resolve/dynamic'
27
+ import { scanFacts } from '../resolve/scan-facts'
28
+ import { readSourceText } from '../resolve/source-text'
29
+ import { ensureDir, listFiles, readText, writeText } from '../utils/fs'
30
+ import { escapeRegex } from '../utils/code'
27
31
  import {
28
32
  getExternalPackagePolicy,
29
33
  resolveImport,
30
34
  resolveLinkedPackageSpecifier,
31
- } from '../resolve/imports';
35
+ } from '../resolve/imports'
32
36
  import {
33
37
  CLIENT_RUNTIME_MODULE,
34
38
  DYN_SHARED_GLOBAL,
@@ -37,8 +41,8 @@ import {
37
41
  clientRuntimeFacts,
38
42
  clientRuntimeSource,
39
43
  type ClientRuntimeFacts,
40
- } from './entry';
41
- import { clientSuspenseFree } from './compat-surface';
44
+ } from './entry'
45
+ import { clientSuspenseFree } from './react-tier'
42
46
  import {
43
47
  ensurePrebuiltRuntime,
44
48
  prebuiltAssets,
@@ -48,13 +52,13 @@ import {
48
52
  prebuiltSpecifierFilter,
49
53
  settlePrebuiltRuntime,
50
54
  type PrebuiltRuntime,
51
- } from './prebuilt';
52
- import { findLayouts } from '../routing/routes';
53
- import { clientReferenceId, ssrClientReference, type ClientReference } from './reference';
54
- import { shouldReactCompile, transformReactCompiler } from './react-compiler';
55
- import { createVerboseLogger } from '../utils/verbose';
56
- import { dim as dimText } from '../utils/ansi';
57
- import type { RouteManifestEntry } from '../types';
55
+ } from './prebuilt'
56
+ import { findLayouts } from '../routing/routes'
57
+ import { clientReferenceId, ssrClientReference, type ClientReference } from './reference'
58
+ import { shouldReactCompile, transformReactCompiler } from './react-compiler'
59
+ import { createVerboseLogger, formatDuration } from '../utils/verbose'
60
+ import { dim as dimText } from '../utils/ansi'
61
+ import type { RouteManifestEntry } from '../types'
58
62
  import {
59
63
  applyClientSourceAsyncPreTransforms,
60
64
  applyClientSourceTransforms,
@@ -64,7 +68,7 @@ import {
64
68
  getImportAliasExtensions,
65
69
  hasClientSourceAsyncPreTransforms,
66
70
  type CompatModeExtensions,
67
- } from '../extensions';
71
+ } from '../extensions'
68
72
 
69
73
  /**
70
74
  * Write the standalone static chunks a bundler extension supplies (compat: the
@@ -73,44 +77,44 @@ import {
73
77
  * build. No-op for pure-core apps (the seam returns none).
74
78
  */
75
79
  export async function emitStaticClientChunks(config: ResolvedConfig, outDir: string) {
76
- const chunks = getBundlerExtensions().staticClientChunks(config);
77
- if (chunks.length === 0) return;
78
- const chunksDir = path.join(outDir, 'chunks');
79
- await ensureDir(chunksDir);
80
+ const chunks = getBundlerExtensions().staticClientChunks(config)
81
+ if (chunks.length === 0) return
82
+ const chunksDir = path.join(outDir, 'chunks')
83
+ await ensureDir(chunksDir)
80
84
  for (const chunk of chunks) {
81
- await writeText(path.join(chunksDir, chunk.name), chunk.contents);
85
+ await writeText(path.join(chunksDir, chunk.name), chunk.contents)
82
86
  }
83
87
  }
84
88
 
85
89
  interface ClientBuildOptions {
86
- config: ResolvedConfig;
87
- route: RouteManifestEntry;
88
- outDir: string;
89
- dev?: boolean;
90
+ config: ResolvedConfig
91
+ route: RouteManifestEntry
92
+ outDir: string
93
+ dev?: boolean
90
94
  }
91
95
 
92
96
  interface ClientBatchBuildOptions {
93
- config: ResolvedConfig;
94
- routes: RouteManifestEntry[];
95
- outDir: string;
96
- verbose?: boolean;
97
+ config: ResolvedConfig
98
+ routes: RouteManifestEntry[]
99
+ outDir: string
100
+ verbose?: boolean
97
101
  /**
98
102
  * The app defines at least one server action (the build's action manifest). Unset means "unknown" -
99
103
  * dev, where the manifest is discovered per request - and the action runtime is emitted, since only
100
104
  * prod bundles are budgeted.
101
105
  */
102
- hasServerActions?: boolean;
106
+ hasServerActions?: boolean
103
107
  /**
104
108
  * A pipeline already warmed by the caller (the build starts one under the
105
109
  * route-fact scan). Omitted — dev, tests — the stage opens its own.
106
110
  */
107
- pipeline?: ClientSourcePipeline;
111
+ pipeline?: ClientSourcePipeline
108
112
  }
109
113
 
110
- const virtualEntryNamespace = 'pnext-client-entry';
114
+ const virtualEntryNamespace = 'pnext-client-entry'
111
115
 
112
116
  function nextCompatEnabled(config: ResolvedConfig) {
113
- return getCompatModeExtensions().nextEnabled(config);
117
+ return getCompatModeExtensions().nextEnabled(config)
114
118
  }
115
119
 
116
120
  /**
@@ -119,13 +123,13 @@ function nextCompatEnabled(config: ResolvedConfig) {
119
123
  * directories once per route. Memoized for the duration of one build and cleared at each entry
120
124
  * point, so a dev rebuild re-stats after a file is added or removed.
121
125
  */
122
- const conventionCache = new Map<string, unknown>();
126
+ const conventionCache = new Map<string, unknown>()
123
127
 
124
128
  function memoConvention<T>(key: string, compute: () => T): T {
125
- if (conventionCache.has(key)) return conventionCache.get(key) as T;
126
- const value = compute();
127
- conventionCache.set(key, value);
128
- return value;
129
+ if (conventionCache.has(key)) return conventionCache.get(key) as T
130
+ const value = compute()
131
+ conventionCache.set(key, value)
132
+ return value
129
133
  }
130
134
 
131
135
  /**
@@ -136,10 +140,10 @@ function memoConvention<T>(key: string, compute: () => T): T {
136
140
  * drops it.
137
141
  */
138
142
  function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean) {
139
- if (hasServerActions !== false) return true;
143
+ if (hasServerActions !== false) return true
140
144
  return routes.some(route =>
141
145
  route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form'),
142
- );
146
+ )
143
147
  }
144
148
 
145
149
  /**
@@ -148,10 +152,10 @@ function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean
148
152
  * actions elsewhere.
149
153
  */
150
154
  function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
151
- if (!appActions) return false;
155
+ if (!appActions) return false
152
156
  return (
153
157
  route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form') === true
154
- );
158
+ )
155
159
  }
156
160
 
157
161
  /**
@@ -160,15 +164,15 @@ function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
160
164
  * file - has to be in the tree to catch it.
161
165
  */
162
166
  function routeThrowsClientControlFlow(route: RouteManifestEntry): boolean {
163
- return route.clientEntryReasons?.includes('control-flow') === true;
167
+ return route.clientEntryReasons?.includes('control-flow') === true
164
168
  }
165
169
 
166
170
  function routeErrorFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
167
- return routeConventionFile(config, route, 'error');
171
+ return routeConventionFile(config, route, 'error')
168
172
  }
169
173
 
170
174
  function routeNotFoundFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
171
- return routeConventionFile(config, route, 'not-found');
175
+ return routeConventionFile(config, route, 'not-found')
172
176
  }
173
177
 
174
178
  // Nearest segment convention file for a route: walked from the page's own
@@ -178,26 +182,26 @@ function routeConventionFile(
178
182
  route: RouteManifestEntry,
179
183
  name: string,
180
184
  ): string | undefined {
181
- if (!nextCompatEnabled(config)) return undefined;
182
- const appPath = path.resolve(config.appPath);
183
- const start = path.dirname(path.resolve(route.file));
184
- const relative = path.relative(appPath, start);
185
- if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined;
185
+ if (!nextCompatEnabled(config)) return undefined
186
+ const appPath = path.resolve(config.appPath)
187
+ const start = path.dirname(path.resolve(route.file))
188
+ const relative = path.relative(appPath, start)
189
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined
186
190
  // Memoized per directory, not per route: sibling routes walk the same chain,
187
191
  // and a parent's answer is the tail of every child's walk.
188
192
  return memoConvention(`${name}\0${start}`, () => {
189
- let dir = start;
193
+ let dir = start
190
194
  for (;;) {
191
195
  for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
192
- const candidate = path.join(dir, `${name}.${ext}`);
193
- if (existsSync(candidate)) return candidate;
196
+ const candidate = path.join(dir, `${name}.${ext}`)
197
+ if (existsSync(candidate)) return candidate
194
198
  }
195
- if (dir === appPath) return undefined;
196
- const parent = path.dirname(dir);
197
- if (parent === dir) return undefined;
198
- dir = parent;
199
+ if (dir === appPath) return undefined
200
+ const parent = path.dirname(dir)
201
+ if (parent === dir) return undefined
202
+ dir = parent
199
203
  }
200
- });
204
+ })
201
205
  }
202
206
 
203
207
  // The app-root global-error.* convention file (single per app, unlike error.*
@@ -205,15 +209,15 @@ function routeConventionFile(
205
209
  // can mount a user global-error component for client throws that escape every
206
210
  // route error boundary. Absent → the built-in fallback document is used.
207
211
  function globalErrorFile(config: ResolvedConfig): string | undefined {
208
- if (!nextCompatEnabled(config)) return undefined;
209
- const appPath = path.resolve(config.appPath);
212
+ if (!nextCompatEnabled(config)) return undefined
213
+ const appPath = path.resolve(config.appPath)
210
214
  return memoConvention(`global-error\0${appPath}`, () => {
211
215
  for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
212
- const candidate = path.join(appPath, `global-error.${ext}`);
213
- if (existsSync(candidate)) return candidate;
216
+ const candidate = path.join(appPath, `global-error.${ext}`)
217
+ if (existsSync(candidate)) return candidate
214
218
  }
215
- return undefined;
216
- });
219
+ return undefined
220
+ })
217
221
  }
218
222
 
219
223
  /**
@@ -225,10 +229,10 @@ function globalErrorFile(config: ResolvedConfig): string | undefined {
225
229
  * this is true, and the shell path depends on those markers.
226
230
  */
227
231
  function hasClientRootLayout(config: ResolvedConfig, route: RouteManifestEntry): boolean {
228
- if (route.client) return false;
229
- const rootLayout = shellLayoutOrder(config, route)[0];
230
- if (!rootLayout) return false;
231
- return route.clientReferences.some(reference => reference.file === rootLayout);
232
+ if (route.client) return false
233
+ const rootLayout = shellLayoutOrder(config, route)[0]
234
+ if (!rootLayout) return false
235
+ return route.clientReferences.some(reference => reference.file === rootLayout)
232
236
  }
233
237
 
234
238
  /**
@@ -240,7 +244,7 @@ function shellLayoutOrder(config: ResolvedConfig, route: RouteManifestEntry): st
240
244
  // Asked three times per route (shell order, client-root check, runtime facts).
241
245
  return memoConvention(`layouts\0${config.appPath}\0${route.file}`, () =>
242
246
  findLayouts(config.appPath, route.file),
243
- );
247
+ )
244
248
  }
245
249
 
246
250
  /**
@@ -256,24 +260,22 @@ function prebuiltLocation(config: ResolvedConfig, outDir: string, key: string, d
256
260
  dir: prebuiltRuntimeDir(config, key),
257
261
  publicPath: `/__pnext/runtime/${key}`,
258
262
  assetPath: `__pnext/runtime/${key}`,
259
- };
263
+ }
260
264
  }
261
265
  // The SERVED url, not a relative one: chunks sit a directory below the
262
266
  // entries, so no single relative path is correct from both — and esbuild
263
267
  // writes an external path through verbatim, the same string everywhere.
264
- const served = nextCompatEnabled(config)
265
- ? `/_next/static/rt/${key}`
266
- : `/assets/rt/${key}`;
268
+ const served = nextCompatEnabled(config) ? `/_next/static/rt/${key}` : `/assets/rt/${key}`
267
269
  return {
268
270
  dir: path.join(outDir, 'rt', key),
269
271
  publicPath: withAssetPrefix(config, served),
270
272
  assetPath: `assets/rt/${key}`,
271
- };
273
+ }
272
274
  }
273
275
 
274
276
  /** Where the dev server reads the artifact back from. */
275
277
  export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
276
- return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key);
278
+ return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key)
277
279
  }
278
280
 
279
281
  /**
@@ -285,7 +287,7 @@ export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
285
287
  */
286
288
  function prebuiltEnabled() {
287
289
  // eslint-disable-next-line turbo/no-undeclared-env-vars
288
- return process.env.PNEXT_PREBUILT_RUNTIME === '1';
290
+ return process.env.PNEXT_PREBUILT_RUNTIME === '1'
289
291
  }
290
292
 
291
293
  /**
@@ -296,7 +298,7 @@ function prebuiltEnabled() {
296
298
  * the same framework names.
297
299
  */
298
300
  function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, actions: boolean) {
299
- const nextCompat = nextCompatEnabled(config);
301
+ const nextCompat = nextCompatEnabled(config)
300
302
  const facts = clientRuntimeFacts(
301
303
  [
302
304
  {
@@ -306,7 +308,7 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
306
308
  ],
307
309
  nextCompat,
308
310
  !routeSuspenseFree(config, route),
309
- );
311
+ )
310
312
  return JSON.stringify([
311
313
  nextCompat,
312
314
  actions,
@@ -316,14 +318,14 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
316
318
  routeThrowsClientControlFlow(route) === true,
317
319
  Boolean(route.needsRouterEntry),
318
320
  facts,
319
- ]);
321
+ ])
320
322
  }
321
323
 
322
324
  interface PreparedPrebuilt {
323
- runtime: PrebuiltRuntime;
324
- plugin: Plugin;
325
+ runtime: PrebuiltRuntime
326
+ plugin: Plugin
325
327
  /** Fold what the build demanded into the artifact. */
326
- settle(): Promise<void>;
328
+ settle(): Promise<void>
327
329
  }
328
330
 
329
331
  /**
@@ -339,10 +341,10 @@ async function preparePrebuilt(
339
341
  signature = '',
340
342
  reactLite = false,
341
343
  ): Promise<PreparedPrebuilt | undefined> {
342
- if (!prebuiltEnabled()) return undefined;
343
- const buildOptions = baseClientBuildOptions(config, dev);
344
- const key = prebuiltRuntimeKey(config, buildOptions, signature);
345
- const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev);
344
+ if (!prebuiltEnabled()) return undefined
345
+ const buildOptions = baseClientBuildOptions(config, dev)
346
+ const key = prebuiltRuntimeKey(config, buildOptions, signature)
347
+ const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev)
346
348
  // The artifact build gets the same plugin chain — and therefore the same
347
349
  // aliases, transforms and asset seam — as the route build it stands in for.
348
350
  // Not the prebuilt plugin itself: inside the artifact, framework imports are
@@ -355,51 +357,51 @@ async function preparePrebuilt(
355
357
  assetPath,
356
358
  buildOptions,
357
359
  plugins: () => clientBuildPlugins(config, createClientSourcePipeline(config), [], reactLite),
358
- };
359
- const runtime = await ensurePrebuiltRuntime(options);
360
- const unprobed = new Set<string>();
360
+ }
361
+ const runtime = await ensurePrebuiltRuntime(options)
362
+ const unprobed = new Set<string>()
361
363
  return {
362
364
  runtime,
363
365
  plugin: prebuiltExternalPlugin(runtime, prebuiltSpecifierFilter(config), unprobed, sourceOf),
364
366
  async settle() {
365
- await settlePrebuiltRuntime(runtime, options, outDir, unprobed);
367
+ await settlePrebuiltRuntime(runtime, options, outDir, unprobed)
366
368
  },
367
- };
369
+ }
368
370
  }
369
371
 
370
372
  /** The served URL of a deferred dynamic reference's on-demand chunk (dev split). */
371
373
  export function deferredDynamicChunkHref(reference: Pick<ClientReference, 'id'>) {
372
- return `/__pnext/client-dyn/${reference.id}.js`;
374
+ return `/__pnext/client-dyn/${reference.id}.js`
373
375
  }
374
376
 
375
377
  export interface DeferredDynamicRef {
376
- file: string;
377
- exportName: string;
378
+ file: string
379
+ exportName: string
378
380
  }
379
381
 
380
382
  // Process-level registry the dev chunk endpoint resolves ids through. Entries
381
383
  // come from the pipeline rewrite below and from each out-dir's sidecar (a
382
384
  // restart serving a cached entry never re-ran the rewrite).
383
- const deferredDynamicRefs = new Map<string, DeferredDynamicRef>();
385
+ const deferredDynamicRefs = new Map<string, DeferredDynamicRef>()
384
386
 
385
387
  export function registerDeferredDynamicRef(id: string, ref: DeferredDynamicRef) {
386
- deferredDynamicRefs.set(id, ref);
388
+ deferredDynamicRefs.set(id, ref)
387
389
  }
388
390
 
389
391
  export function deferredDynamicRefById(id: string) {
390
- return deferredDynamicRefs.get(id);
392
+ return deferredDynamicRefs.get(id)
391
393
  }
392
394
 
393
395
  /** Sidecar name persisted next to a dev entry naming its deferred dynamic refs. */
394
- export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json';
396
+ export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json'
395
397
 
396
398
  /** Dev-only: deferred dynamic references load from the on-demand chunk endpoint. */
397
399
  function devDeferredDynamicHref(dev: boolean | undefined) {
398
- if (!dev || !devDynamicSplitEnabled()) return undefined;
400
+ if (!dev || !devDynamicSplitEnabled()) return undefined
399
401
  return (reference: ClientReference) =>
400
402
  reference.dynamic && !ssrClientReference(reference)
401
403
  ? deferredDynamicChunkHref(reference)
402
- : undefined;
404
+ : undefined
403
405
  }
404
406
 
405
407
  /** Deferred chunk URLs resolve at runtime, never inside the entry build. */
@@ -410,15 +412,15 @@ function deferredDynamicExternalPlugin(): Plugin {
410
412
  build.onResolve({ filter: /^\/__pnext\/client-dyn\// }, args => ({
411
413
  path: args.path,
412
414
  external: true,
413
- }));
415
+ }))
414
416
  },
415
- };
417
+ }
416
418
  }
417
419
 
418
420
  export async function buildClientEntry({ config, route, outDir, dev }: ClientBuildOptions) {
419
- conventionCache.clear();
420
- await ensureDir(outDir);
421
- const suspense = routeSuspenseFree(config, route) === false;
421
+ conventionCache.clear()
422
+ await ensureDir(outDir)
423
+ const suspense = routeSuspenseFree(config, route) === false
422
424
  const source = clientEntrySource({
423
425
  deferredDynamicHref: devDeferredDynamicHref(dev),
424
426
  pageFile: route.client ? route.file : undefined,
@@ -434,11 +436,11 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
434
436
  router: Boolean(route.needsRouterEntry),
435
437
  clientRootLayout: hasClientRootLayout(config, route),
436
438
  shellLayoutOrder: shellLayoutOrder(config, route),
437
- });
438
- const entryName = clientEntryName(route);
439
- const outfile = path.join(outDir, `${entryName}.js`);
440
- const pipeline = createClientSourcePipeline(config, dev === true);
441
- pipeline.warmRoutes([route]);
439
+ })
440
+ const entryName = clientEntryName(route)
441
+ const outfile = path.join(outDir, `${entryName}.js`)
442
+ const pipeline = createClientSourcePipeline(config, dev === true)
443
+ pipeline.warmRoutes([route])
442
444
  const runtimeFacts = clientRuntimeFacts(
443
445
  [
444
446
  {
@@ -448,65 +450,76 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
448
450
  ],
449
451
  nextCompatEnabled(config),
450
452
  suspense,
451
- );
452
- const prebuilt = await clientProfile.timeAsync('prebuilt', () => preparePrebuilt(
453
- config,
454
- outDir,
455
- dev === true,
456
- importer =>
457
- importer === CLIENT_RUNTIME_MODULE
458
- ? clientRuntimeSource(runtimeFacts)
459
- : (pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)),
460
- // One route is its own surface group, so the key is the same one the batch
461
- // build would give this route's group dev and prod derive it identically.
462
- surfaceSignature(config, route, true),
463
- !suspense,
464
- ));
465
-
466
- let metafile: Metafile | undefined;
453
+ )
454
+ const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
455
+ preparePrebuilt(
456
+ config,
457
+ outDir,
458
+ dev === true,
459
+ importer =>
460
+ importer === CLIENT_RUNTIME_MODULE
461
+ ? clientRuntimeSource(runtimeFacts)
462
+ : (pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)),
463
+ // One route is its own surface group, so the key is the same one the batch
464
+ // build would give this route's group — dev and prod derive it identically.
465
+ surfaceSignature(config, route, true),
466
+ !suspense,
467
+ ),
468
+ )
469
+
470
+ let metafile: Metafile | undefined
467
471
  try {
468
472
  const result = await profileClientBuild(route, dev, () =>
469
- clientProfile.timeAsync('esbuild', () => build({
470
- ...baseClientBuildOptions(config, dev),
471
- stdin: {
472
- contents: source,
473
- loader: 'ts',
474
- resolveDir: process.cwd(),
475
- sourcefile: `${route.id}.ts`,
476
- },
477
- outdir: outDir,
478
- entryNames: entryName,
479
- metafile: true,
480
- plugins: clientBuildPlugins(config, pipeline, [
481
- ...(prebuilt ? [prebuilt.plugin] : []),
482
- ...(devDeferredDynamicHref(dev) ? [deferredDynamicExternalPlugin()] : []),
483
- clientRuntimePlugin(runtimeFacts),
484
- ], !suspense),
485
- })),
486
- );
487
- metafile = result.metafile;
473
+ clientProfile.timeAsync('esbuild', () =>
474
+ build({
475
+ ...baseClientBuildOptions(config, dev),
476
+ stdin: {
477
+ contents: source,
478
+ loader: 'ts',
479
+ resolveDir: process.cwd(),
480
+ sourcefile: `${route.id}.ts`,
481
+ },
482
+ outdir: outDir,
483
+ entryNames: entryName,
484
+ metafile: true,
485
+ plugins: clientBuildPlugins(
486
+ config,
487
+ pipeline,
488
+ [
489
+ ...(prebuilt ? [prebuilt.plugin] : []),
490
+ ...(devDeferredDynamicHref(dev) ? [deferredDynamicExternalPlugin()] : []),
491
+ clientRuntimePlugin(runtimeFacts),
492
+ ],
493
+ !suspense,
494
+ ),
495
+ }),
496
+ ),
497
+ )
498
+ metafile = result.metafile
488
499
  } catch (error) {
489
- throw withClientImportTrace(error, config, route, source);
500
+ throw withClientImportTrace(error, config, route, source)
490
501
  }
491
- await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve());
502
+ await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
492
503
 
493
504
  if (metafile) {
494
- metafile = await clientProfile.timeAsync('fold', () => foldInitialChunks({
495
- outDir,
496
- metafile: metafile!,
497
- buildOptions: baseClientBuildOptions(config, dev),
498
- }));
505
+ metafile = await clientProfile.timeAsync('fold', () =>
506
+ foldInitialChunks({
507
+ outDir,
508
+ metafile: metafile!,
509
+ buildOptions: baseClientBuildOptions(config, dev),
510
+ }),
511
+ )
499
512
  }
500
- await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile));
513
+ await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile))
501
514
  if (dev && devDynamicSplitEnabled() && pipeline.deferredRefs().size > 0) {
502
515
  await writeText(
503
516
  path.join(outDir, DEFERRED_DYNAMIC_SIDECAR),
504
517
  JSON.stringify(Object.fromEntries(pipeline.deferredRefs())),
505
- );
518
+ )
506
519
  }
507
- clientProfile.report(`client entry ${route.id}`);
520
+ clientProfile.report(`client entry ${route.id}`)
508
521
 
509
- return outfile;
522
+ return outfile
510
523
  }
511
524
 
512
525
  /**
@@ -522,71 +535,73 @@ export async function buildClientDynamicChunk({
522
535
  reference,
523
536
  outDir,
524
537
  }: {
525
- config: ResolvedConfig;
526
- route?: RouteManifestEntry;
527
- reference: DeferredDynamicRef & { id: string };
528
- outDir: string;
538
+ config: ResolvedConfig
539
+ route?: RouteManifestEntry
540
+ reference: DeferredDynamicRef & { id: string }
541
+ outDir: string
529
542
  }) {
530
- await ensureDir(outDir);
531
- const pipeline = createClientSourcePipeline(config, true);
543
+ await ensureDir(outDir)
544
+ const pipeline = createClientSourcePipeline(config, true)
532
545
  const prebuilt = await preparePrebuilt(
533
546
  config,
534
547
  outDir,
535
548
  true,
536
549
  importer => pipeline.sourceOf(importer) ?? readTextSyncSafe(importer),
537
550
  route ? surfaceSignature(config, route, true) : '',
538
- );
551
+ )
539
552
  const source = [
540
553
  reference.exportName === 'default'
541
554
  ? `export { default } from ${JSON.stringify(reference.file)};`
542
555
  : '',
543
556
  `export * from ${JSON.stringify(reference.file)};`,
544
- ].join('\n');
545
- const outfile = path.join(outDir, `${reference.id}.js`);
546
- await clientProfile.timeAsync('dynChunk', () => build({
547
- ...baseClientBuildOptions(config, true),
548
- stdin: {
549
- contents: source,
550
- loader: 'ts',
551
- resolveDir: process.cwd(),
552
- sourcefile: `${reference.id}.dyn.ts`,
553
- },
554
- outdir: outDir,
555
- entryNames: reference.id,
556
- plugins: clientBuildPlugins(config, pipeline, [
557
- ...(prebuilt ? [prebuilt.plugin] : []),
558
- dynSharedVendorPlugin(nextCompatEnabled(config)),
559
- deferredDynamicExternalPlugin(),
560
- ]),
561
- }));
562
- await prebuilt?.settle();
563
- clientProfile.report(`client dynamic chunk ${reference.id}`);
564
- return outfile;
557
+ ].join('\n')
558
+ const outfile = path.join(outDir, `${reference.id}.js`)
559
+ await clientProfile.timeAsync('dynChunk', () =>
560
+ build({
561
+ ...baseClientBuildOptions(config, true),
562
+ stdin: {
563
+ contents: source,
564
+ loader: 'ts',
565
+ resolveDir: process.cwd(),
566
+ sourcefile: `${reference.id}.dyn.ts`,
567
+ },
568
+ outdir: outDir,
569
+ entryNames: reference.id,
570
+ plugins: clientBuildPlugins(config, pipeline, [
571
+ ...(prebuilt ? [prebuilt.plugin] : []),
572
+ dynSharedVendorPlugin(nextCompatEnabled(config)),
573
+ deferredDynamicExternalPlugin(),
574
+ ]),
575
+ }),
576
+ )
577
+ await prebuilt?.settle()
578
+ clientProfile.report(`client dynamic chunk ${reference.id}`)
579
+ return outfile
565
580
  }
566
581
 
567
582
  /** Shared-vendor shims: preact resolves to the entry's published namespace. */
568
583
  function dynSharedVendorPlugin(nextCompat: boolean): Plugin {
569
- const namespace = 'pnext-dyn-shared';
570
- const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`);
584
+ const namespace = 'pnext-dyn-shared'
585
+ const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`)
571
586
  return {
572
587
  name: 'pnext-dyn-shared-vendor',
573
588
  setup(build) {
574
589
  build.onResolve({ filter }, args =>
575
590
  args.namespace === namespace ? undefined : { path: args.path, namespace },
576
- );
591
+ )
577
592
  build.onLoad({ filter: /.*/, namespace }, async args => {
578
593
  const names = Object.keys((await import(args.path)) as Record<string, unknown>).filter(
579
594
  name => /^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default',
580
- );
595
+ )
581
596
  const lines = [
582
597
  `const m = window.${DYN_SHARED_GLOBAL}[${JSON.stringify(args.path)}];`,
583
598
  'export default (m && m.default);',
584
599
  ...names.map(name => `export const ${name} = m.${name};`),
585
- ];
586
- return { contents: lines.join('\n'), loader: 'js' };
587
- });
600
+ ]
601
+ return { contents: lines.join('\n'), loader: 'js' }
602
+ })
588
603
  },
589
- };
604
+ }
590
605
  }
591
606
 
592
607
  /**
@@ -603,84 +618,86 @@ export async function buildClientEntries({
603
618
  hasServerActions,
604
619
  pipeline: warmed,
605
620
  }: ClientBatchBuildOptions) {
606
- if (!warmed) conventionCache.clear();
607
- const actions = appUsesActions(routes, hasServerActions);
608
- const entries = clientProfile.time('entrySources', () => routes
609
- .filter(route => route.client || route.clientReferences.length > 0 || route.needsRouterEntry)
610
- .map(route => ({
611
- route,
612
- entryName: clientEntryName(route),
613
- source: clientEntrySource({
614
- pageFile: route.client ? route.file : undefined,
615
- clientReferences: route.clientReferences,
616
- nextCompat: nextCompatEnabled(config),
617
- suspense: !routeSuspenseFree(config, route),
618
- actions: routeUsesActions(route, actions),
619
- errorFile: routeErrorFile(config, route),
620
- globalErrorFile: globalErrorFile(config),
621
- notFoundFile: routeNotFoundFile(config, route),
622
- controlFlow: routeThrowsClientControlFlow(route),
623
- router: Boolean(route.needsRouterEntry),
624
- clientRootLayout: hasClientRootLayout(config, route),
625
- shellLayoutOrder: shellLayoutOrder(config, route),
626
- }),
627
- })));
628
- if (entries.length === 0) return;
621
+ if (!warmed) conventionCache.clear()
622
+ const actions = appUsesActions(routes, hasServerActions)
623
+ const entries = clientProfile.time('entrySources', () =>
624
+ routes
625
+ .filter(route => route.client || route.clientReferences.length > 0 || route.needsRouterEntry)
626
+ .map(route => ({
627
+ route,
628
+ entryName: clientEntryName(route),
629
+ source: clientEntrySource({
630
+ pageFile: route.client ? route.file : undefined,
631
+ clientReferences: route.clientReferences,
632
+ nextCompat: nextCompatEnabled(config),
633
+ suspense: !routeSuspenseFree(config, route),
634
+ actions: routeUsesActions(route, actions),
635
+ errorFile: routeErrorFile(config, route),
636
+ globalErrorFile: globalErrorFile(config),
637
+ notFoundFile: routeNotFoundFile(config, route),
638
+ controlFlow: routeThrowsClientControlFlow(route),
639
+ router: Boolean(route.needsRouterEntry),
640
+ clientRootLayout: hasClientRootLayout(config, route),
641
+ shellLayoutOrder: shellLayoutOrder(config, route),
642
+ }),
643
+ })),
644
+ )
645
+ if (entries.length === 0) return
629
646
  // Every route's client sources start their transform and React Compiler pass here, in one batch on
630
647
  // oxc's threadpool, while esbuild is still booting and walking the graph. A caller-warmed pipeline
631
648
  // has them in flight already; warming twice is a Map hit, so the same call covers both.
632
- const pipeline = warmed ?? createClientSourcePipeline(config);
633
- pipeline.warmRoutes(entries.map(entry => entry.route));
634
- await ensureDir(outDir);
635
- const groups = surfaceGroups(config, entries, actions);
636
- clientProfile.count('group#', groups.length);
637
- const built: BuiltEntryGroup[] = [];
649
+ const pipeline = warmed ?? createClientSourcePipeline(config)
650
+ pipeline.warmRoutes(entries.map(entry => entry.route))
651
+ await ensureDir(outDir)
652
+ const groups = surfaceGroups(config, entries, actions)
653
+ clientProfile.count('group#', groups.length)
654
+ const built: BuiltEntryGroup[] = []
638
655
  for (const group of groups) {
639
- built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }));
656
+ built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }))
640
657
  }
641
658
 
642
659
  // Fold and rename once over the finished output, never per group. Chunk names
643
660
  // are content hashes, so two groups emit the same file for the same chunk —
644
661
  // and the fold DELETES the members it merges, so a per-group fold would
645
662
  // unlink a chunk another group's manifest still points at.
646
- let merged: Metafile = { inputs: {}, outputs: {} };
663
+ let merged: Metafile = { inputs: {}, outputs: {} }
647
664
  for (const item of built) {
648
- if (!item.metafile) continue;
649
- Object.assign(merged.inputs, item.metafile.inputs);
650
- Object.assign(merged.outputs, item.metafile.outputs);
665
+ if (!item.metafile) continue
666
+ Object.assign(merged.inputs, item.metafile.inputs)
667
+ Object.assign(merged.outputs, item.metafile.outputs)
651
668
  }
652
669
  merged = await clientProfile.timeAsync('fold', () =>
653
670
  foldInitialChunks({ outDir, metafile: merged, buildOptions: baseClientBuildOptions(config) }),
654
- );
671
+ )
655
672
  const renames = await clientProfile.timeAsync('write', () =>
656
673
  nameSharedClientChunks(outDir, merged),
657
- );
674
+ )
658
675
  clientProfile.time('entryImports', () => {
659
676
  for (const item of built) {
660
677
  if (item.metafile) {
661
- assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt);
678
+ assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt)
662
679
  }
663
680
  }
664
- });
665
- clientProfile.report('client stage');
681
+ })
682
+ clientProfile.report('client stage')
666
683
  }
667
684
 
668
685
  interface ClientEntry {
669
- route: RouteManifestEntry;
670
- entryName: string;
671
- source: string;
686
+ route: RouteManifestEntry
687
+ entryName: string
688
+ source: string
672
689
  }
673
690
 
674
691
  interface SurfaceGroup {
675
- signature: string;
676
- facts: ClientRuntimeFacts;
677
- entries: ClientEntry[];
692
+ signature: string
693
+ facts: ClientRuntimeFacts
694
+ entries: ClientEntry[]
678
695
  }
679
696
 
680
697
  interface BuiltEntryGroup {
681
- group: SurfaceGroup;
682
- metafile: Metafile | undefined;
683
- prebuilt: PreparedPrebuilt | undefined;
698
+ group: SurfaceGroup
699
+ metafile: Metafile | undefined
700
+ prebuilt: PreparedPrebuilt | undefined
684
701
  }
685
702
 
686
703
  /**
@@ -704,16 +721,16 @@ function surfaceGroups(
704
721
  })),
705
722
  nextCompatEnabled(config),
706
723
  !members.every(entry => routeSuspenseFree(config, entry.route)),
707
- );
724
+ )
708
725
  // eslint-disable-next-line turbo/no-undeclared-env-vars
709
726
  if (!prebuiltEnabled() || process.env.PNEXT_PREBUILT_GROUPS === '0')
710
- return [{ signature: '', facts: facts(entries), entries }];
711
- const bySignature = new Map<string, ClientEntry[]>();
727
+ return [{ signature: '', facts: facts(entries), entries }]
728
+ const bySignature = new Map<string, ClientEntry[]>()
712
729
  for (const entry of entries) {
713
- const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions));
714
- const members = bySignature.get(signature) ?? [];
715
- bySignature.set(signature, members);
716
- members.push(entry);
730
+ const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions))
731
+ const members = bySignature.get(signature) ?? []
732
+ bySignature.set(signature, members)
733
+ members.push(entry)
717
734
  }
718
735
  return [...bySignature].map(([signature, members]) => ({
719
736
  signature,
@@ -722,7 +739,7 @@ function surfaceGroups(
722
739
  // no route in this group has.
723
740
  facts: facts(members),
724
741
  entries: members,
725
- }));
742
+ }))
726
743
  }
727
744
 
728
745
  async function buildEntryGroup({
@@ -733,62 +750,69 @@ async function buildEntryGroup({
733
750
  pipeline,
734
751
  verbose,
735
752
  }: {
736
- config: ResolvedConfig;
737
- outDir: string;
753
+ config: ResolvedConfig
754
+ outDir: string
738
755
  /** Every entry in the app: the batch fallback and `sourceOf` span groups. */
739
- entries: ClientEntry[];
740
- group: SurfaceGroup;
741
- pipeline: ClientSourcePipeline;
742
- verbose?: boolean;
756
+ entries: ClientEntry[]
757
+ group: SurfaceGroup
758
+ pipeline: ClientSourcePipeline
759
+ verbose?: boolean
743
760
  }): Promise<BuiltEntryGroup> {
744
- const reactLite = group.facts.suspense === false;
761
+ const reactLite = group.facts.suspense === false
745
762
  const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
746
763
  preparePrebuilt(
747
764
  config,
748
765
  outDir,
749
766
  false,
750
767
  importer => {
751
- if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts);
752
- const entry = entries.find(item => item.route.id === importer);
753
- return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer);
768
+ if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts)
769
+ const entry = entries.find(item => item.route.id === importer)
770
+ return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)
754
771
  },
755
772
  group.signature,
756
773
  reactLite,
757
774
  ),
758
- );
775
+ )
759
776
 
760
- let metafile: Metafile | undefined;
777
+ let metafile: Metafile | undefined
761
778
  try {
762
- const result = await clientProfile.timeAsync('esbuild', () => build({
763
- ...baseClientBuildOptions(config),
764
- entryPoints: group.entries.map(entry => ({
765
- in: virtualEntryPath(entry.route.id),
766
- out: entry.entryName,
767
- })),
768
- outdir: outDir,
769
- metafile: true,
770
- plugins: clientBuildPlugins(config, pipeline, [
771
- ...(prebuilt ? [prebuilt.plugin] : []),
772
- virtualEntryPlugin(group.entries),
773
- clientRuntimePlugin(group.facts),
774
- ], reactLite),
775
- }));
776
- metafile = result.metafile;
779
+ const result = await clientProfile.timeAsync('esbuild', () =>
780
+ build({
781
+ ...baseClientBuildOptions(config),
782
+ entryPoints: group.entries.map(entry => ({
783
+ in: virtualEntryPath(entry.route.id),
784
+ out: entry.entryName,
785
+ })),
786
+ outdir: outDir,
787
+ metafile: true,
788
+ plugins: clientBuildPlugins(
789
+ config,
790
+ pipeline,
791
+ [
792
+ ...(prebuilt ? [prebuilt.plugin] : []),
793
+ virtualEntryPlugin(group.entries),
794
+ clientRuntimePlugin(group.facts),
795
+ ],
796
+ reactLite,
797
+ ),
798
+ }),
799
+ )
800
+ metafile = result.metafile
777
801
  } catch (error) {
778
802
  // A batched build cannot attribute a "Could not resolve" error to a single route, so fall back to
779
803
  // per-route builds - the offending route then throws with its precise client import trace. Other
780
804
  // diagnostics already carry a clear message from the batch build; re-throw rather than let a
781
805
  // per-route build surface a more confusing downstream error.
782
- if (!hasUnresolvedImportError(error)) throw error;
806
+ if (!hasUnresolvedImportError(error)) throw error
783
807
  for (const { route } of group.entries) {
784
- await buildClientEntry({ config, route, outDir });
808
+ await buildClientEntry({ config, route, outDir })
785
809
  }
786
- throw error;
810
+ throw error
787
811
  }
788
- await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve());
812
+ await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
789
813
 
790
- if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir);
791
- return { group, metafile, prebuilt };
814
+ if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir)
815
+ return { group, metafile, prebuilt }
792
816
  }
793
817
 
794
818
  /**
@@ -802,11 +826,11 @@ async function buildEntryGroup({
802
826
  * Memoized per build; a group of routes qualifies iff every member does.
803
827
  */
804
828
  function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): boolean {
805
- if (!nextCompatEnabled(config)) return false;
829
+ if (!nextCompatEnabled(config)) return false
806
830
  return memoConvention(`suspenseFree:${route.id}`, () => {
807
- const seeds: string[] = [];
808
- if (route.client) seeds.push(route.file);
809
- for (const reference of route.clientReferences) seeds.push(reference.file);
831
+ const seeds: string[] = []
832
+ if (route.client) seeds.push(route.file)
833
+ for (const reference of route.clientReferences) seeds.push(reference.file)
810
834
  for (const file of [
811
835
  routeErrorFile(config, route),
812
836
  globalErrorFile(config),
@@ -814,19 +838,19 @@ function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): b
814
838
  // Shell layouts hydrate client-side only when the root layout is 'use client'.
815
839
  ...(hasClientRootLayout(config, route) ? shellLayoutOrder(config, route) : []),
816
840
  ]) {
817
- if (file) seeds.push(file);
841
+ if (file) seeds.push(file)
818
842
  }
819
- return clientSuspenseFree(seeds);
820
- });
843
+ return clientSuspenseFree(seeds)
844
+ })
821
845
  }
822
846
 
823
847
  function routeClientSources(routes: RouteManifestEntry[]) {
824
- const files = new Set<string>();
848
+ const files = new Set<string>()
825
849
  for (const route of routes) {
826
- if (route.client) files.add(route.file);
827
- for (const reference of route.clientReferences) files.add(reference.file);
850
+ if (route.client) files.add(route.file)
851
+ for (const reference of route.clientReferences) files.add(reference.file)
828
852
  }
829
- return files;
853
+ return files
830
854
  }
831
855
 
832
856
  // Record each entry's static chunk closure on the route so rendered pages can
@@ -839,58 +863,58 @@ function assignClientEntryImports(
839
863
  renames: Map<string, string>,
840
864
  prebuilt: PreparedPrebuilt | undefined,
841
865
  ) {
842
- const outputs = metafile.outputs;
866
+ const outputs = metafile.outputs
843
867
  // One basename index for the whole entry set: the linear scan this replaces
844
868
  // was O(entries × outputs), which on a route-per-page app is the entire
845
869
  // output list walked once per route.
846
- const byBasename = new Map<string, string>();
870
+ const byBasename = new Map<string, string>()
847
871
  for (const item of Object.keys(outputs)) {
848
872
  // First match wins, as the linear scan did.
849
- const basename = path.basename(item);
850
- if (!byBasename.has(basename)) byBasename.set(basename, item);
873
+ const basename = path.basename(item)
874
+ if (!byBasename.has(basename)) byBasename.set(basename, item)
851
875
  }
852
876
  for (const { route, entryName } of entries) {
853
- const entryOutput = byBasename.get(`${entryName}.js`);
854
- if (!entryOutput) continue;
855
- const staticImports = new Set<string>();
856
- const dynamicImports = new Set<string>();
857
- const visited = new Set([`${entryOutput}:static`]);
858
- const queue = [{ output: entryOutput, dynamic: false }];
877
+ const entryOutput = byBasename.get(`${entryName}.js`)
878
+ if (!entryOutput) continue
879
+ const staticImports = new Set<string>()
880
+ const dynamicImports = new Set<string>()
881
+ const visited = new Set([`${entryOutput}:static`])
882
+ const queue = [{ output: entryOutput, dynamic: false }]
859
883
  // eslint-disable-next-line @typescript-eslint/prefer-for-of
860
884
  for (let head = 0; head < queue.length; head += 1) {
861
- const current = queue[head];
862
- if (!current) continue;
885
+ const current = queue[head]
886
+ if (!current) continue
863
887
  for (const imported of outputs[current.output]?.imports ?? []) {
864
- if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue;
865
- const dynamic = current.dynamic || imported.kind === 'dynamic-import';
866
- const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`;
867
- if (visited.has(visitKey)) continue;
868
- visited.add(visitKey);
888
+ if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue
889
+ const dynamic = current.dynamic || imported.kind === 'dynamic-import'
890
+ const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`
891
+ if (visited.has(visitKey)) continue
892
+ visited.add(visitKey)
869
893
  // Prebuilt-runtime shims are external: already a url relative to the
870
894
  // assets root, and with no output in THIS metafile to walk into. Their
871
895
  // own chunk closure comes from the artifact's manifest instead, so the
872
896
  // page preloads the whole set rather than discovering chunks one
873
897
  // round trip after the shim lands.
874
898
  if (imported.external) {
875
- if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue;
876
- const assets = prebuiltAssets(prebuilt.runtime);
877
- for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset);
878
- for (const asset of assets.dynamic) dynamicImports.add(asset);
879
- continue;
899
+ if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue
900
+ const assets = prebuiltAssets(prebuilt.runtime)
901
+ for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset)
902
+ for (const asset of assets.dynamic) dynamicImports.add(asset)
903
+ continue
880
904
  }
881
- queue.push({ output: imported.path, dynamic });
882
- const basename = path.basename(imported.path);
883
- const relative = path.relative(outDir, path.resolve(imported.path));
905
+ queue.push({ output: imported.path, dynamic })
906
+ const basename = path.basename(imported.path)
907
+ const relative = path.relative(outDir, path.resolve(imported.path))
884
908
  const publicRelative = path
885
909
  .join('assets', path.dirname(relative), renames.get(basename) ?? basename)
886
910
  .split(path.sep)
887
- .join('/');
888
- (dynamic ? dynamicImports : staticImports).add(publicRelative);
911
+ .join('/')
912
+ ;(dynamic ? dynamicImports : staticImports).add(publicRelative)
889
913
  }
890
914
  }
891
- if (staticImports.size > 0) route.clientEntryImports = [...staticImports];
892
- for (const asset of staticImports) dynamicImports.delete(asset);
893
- if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports];
915
+ if (staticImports.size > 0) route.clientEntryImports = [...staticImports]
916
+ for (const asset of staticImports) dynamicImports.delete(asset)
917
+ if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports]
894
918
  }
895
919
  }
896
920
 
@@ -905,62 +929,62 @@ function reportClientBundleSizes(
905
929
  metafile: Metafile,
906
930
  outDir: string,
907
931
  ) {
908
- const log = createVerboseLogger(true, 'client');
909
- const outputs = metafile.outputs;
932
+ const log = createVerboseLogger(true, 'client')
933
+ const outputs = metafile.outputs
910
934
  const rows = entries
911
935
  .map(({ route, entryName }) => {
912
- const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}`;
936
+ const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}`
913
937
  const output = outputs[key]
914
938
  ? key
915
- : Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`);
916
- const bytes = output ? reachableOutputBytes(output, outputs) : 0;
917
- return { route: route.route, bytes };
939
+ : Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`)
940
+ const bytes = output ? reachableOutputBytes(output, outputs) : 0
941
+ return { route: route.route, bytes }
918
942
  })
919
- .sort((a, b) => b.bytes - a.bytes);
943
+ .sort((a, b) => b.bytes - a.bytes)
920
944
 
921
945
  for (const row of rows) {
922
- log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`);
946
+ log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`)
923
947
  }
924
948
  }
925
949
 
926
950
  function reachableOutputBytes(entry: string, outputs: Metafile['outputs']) {
927
- const visited = new Set<string>();
928
- const queue = [entry];
929
- let bytes = 0;
951
+ const visited = new Set<string>()
952
+ const queue = [entry]
953
+ let bytes = 0
930
954
 
931
955
  while (queue.length > 0) {
932
- const current = queue.shift();
933
- if (!current || visited.has(current)) continue;
934
- visited.add(current);
935
- const output = outputs[current];
936
- if (!output) continue;
937
- bytes += output.bytes;
956
+ const current = queue.shift()
957
+ if (!current || visited.has(current)) continue
958
+ visited.add(current)
959
+ const output = outputs[current]
960
+ if (!output) continue
961
+ bytes += output.bytes
938
962
  for (const imported of output.imports) {
939
963
  if (imported.kind === 'import-statement' || imported.kind === 'dynamic-import') {
940
- queue.push(imported.path);
964
+ queue.push(imported.path)
941
965
  }
942
966
  }
943
967
  }
944
968
 
945
- return bytes;
969
+ return bytes
946
970
  }
947
971
 
948
972
  function formatBytes(bytes: number) {
949
- if (bytes < 1024) return `${bytes} B`;
950
- const units = ['KB', 'MB'] as const;
951
- let value = bytes / 1024;
952
- let unitIndex = 0;
973
+ if (bytes < 1024) return `${bytes} B`
974
+ const units = ['KB', 'MB'] as const
975
+ let value = bytes / 1024
976
+ let unitIndex = 0
953
977
  while (value >= 1024 && unitIndex < units.length - 1) {
954
- value /= 1024;
955
- unitIndex += 1;
978
+ value /= 1024
979
+ unitIndex += 1
956
980
  }
957
- return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}`;
981
+ return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}`
958
982
  }
959
983
 
960
984
  function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptions {
961
- const extensions = getBundlerExtensions();
962
- const pure = extensions.clientPureFunctions(config);
963
- const inject = extensions.clientInjects();
985
+ const extensions = getBundlerExtensions()
986
+ const pure = extensions.clientPureFunctions(config)
987
+ const inject = extensions.clientInjects()
964
988
  return {
965
989
  ...(inject.length > 0 ? { inject } : {}),
966
990
  chunkNames: 'chunks/[name]-[hash]',
@@ -1003,15 +1027,15 @@ function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptio
1003
1027
  '.otf': 'file',
1004
1028
  '.eot': 'file',
1005
1029
  },
1006
- };
1030
+ }
1007
1031
  }
1008
1032
 
1009
1033
  function deploymentIdDefine(): Record<string, string> {
1010
1034
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1011
- const deploymentId = process.env.NEXT_DEPLOYMENT_ID;
1035
+ const deploymentId = process.env.NEXT_DEPLOYMENT_ID
1012
1036
  return deploymentId === undefined
1013
1037
  ? {}
1014
- : { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) };
1038
+ : { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) }
1015
1039
  }
1016
1040
 
1017
1041
  /**
@@ -1021,28 +1045,29 @@ function deploymentIdDefine(): Record<string, string> {
1021
1045
  * slow" from "we are answering 40k questions". Off unless PNEXT_CLIENT_PROFILE.
1022
1046
  */
1023
1047
  function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
1024
- if (!clientProfile.enabled) return plugins;
1048
+ if (!clientProfile.enabled) return plugins
1025
1049
  return plugins.map(plugin => ({
1026
1050
  name: plugin.name,
1027
1051
  setup(build) {
1028
- const wrap = (kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
1052
+ const wrap =
1053
+ (kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
1029
1054
  (options: any, callback: any) =>
1030
1055
  register(options, async (args: any) => {
1031
- clientProfile.count(`${plugin.name}:${kind}#`);
1032
- const start = performance.now();
1056
+ clientProfile.count(`${plugin.name}:${kind}#`)
1057
+ const start = performance.now()
1033
1058
  try {
1034
- return await (callback as (a: unknown) => unknown)(args);
1059
+ return await (callback as (a: unknown) => unknown)(args)
1035
1060
  } finally {
1036
- clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start);
1061
+ clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start)
1037
1062
  }
1038
- });
1063
+ })
1039
1064
  return plugin.setup({
1040
1065
  ...build,
1041
1066
  onResolve: wrap('resolve', build.onResolve.bind(build)),
1042
1067
  onLoad: wrap('load', build.onLoad.bind(build)),
1043
- });
1068
+ })
1044
1069
  },
1045
- }));
1070
+ }))
1046
1071
  }
1047
1072
 
1048
1073
  /**
@@ -1057,49 +1082,52 @@ function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
1057
1082
  */
1058
1083
  export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[] {
1059
1084
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1060
- if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins;
1085
+ if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins
1061
1086
  interface Registration {
1062
- plugin: string;
1063
- filter: RegExp;
1064
- namespace?: string;
1065
- callback: (args: OnResolveArgs) => unknown;
1087
+ plugin: string
1088
+ filter: RegExp
1089
+ namespace?: string
1090
+ callback: (args: OnResolveArgs) => unknown
1066
1091
  }
1067
1092
  return [
1068
1093
  {
1069
1094
  name,
1070
1095
  async setup(build) {
1071
- const registered: Registration[] = [];
1096
+ const registered: Registration[] = []
1072
1097
  // Registrations made after setup (rare, and esbuild allows it) keep
1073
1098
  // their own hook: the chain's order is already fixed by then.
1074
- let sealed = false;
1099
+ let sealed = false
1075
1100
  /** The chain, minus one plugin's own hooks — see `resolve` below. */
1076
1101
  const dispatch = async (args: OnResolveArgs, skip?: string) => {
1077
1102
  for (const entry of registered) {
1078
- if (entry.plugin === skip) continue;
1079
- if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue;
1080
- if (!entry.filter.test(args.path)) continue;
1081
- const result = (await entry.callback(args)) as OnResolveResult | null | undefined;
1082
- if (result == null) continue;
1103
+ if (entry.plugin === skip) continue
1104
+ if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue
1105
+ if (!entry.filter.test(args.path)) continue
1106
+ const result = (await entry.callback(args)) as OnResolveResult | null | undefined
1107
+ if (result == null) continue
1083
1108
  for (const message of [...(result.errors ?? []), ...(result.warnings ?? [])]) {
1084
- message.pluginName ??= entry.plugin;
1109
+ message.pluginName ??= entry.plugin
1085
1110
  }
1086
- return result;
1111
+ return result
1087
1112
  }
1088
- return undefined;
1089
- };
1113
+ return undefined
1114
+ }
1090
1115
  for (const plugin of plugins) {
1091
1116
  const proxy: PluginBuild = {
1092
1117
  ...build,
1093
1118
  onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) => unknown) {
1094
- if (sealed) return build.onResolve(options, callback as never);
1119
+ if (sealed) return build.onResolve(options, callback as never)
1095
1120
  registered.push({
1096
1121
  plugin: plugin.name,
1097
1122
  // Drop /g and /y: a sticky or global regex carries lastIndex
1098
1123
  // between tests, which would make dispatch order-dependent.
1099
- filter: new RegExp(options.filter.source, options.filter.flags.replace(/[gy]/g, '')),
1124
+ filter: new RegExp(
1125
+ options.filter.source,
1126
+ options.filter.flags.replace(/[gy]/g, ''),
1127
+ ),
1100
1128
  namespace: options.namespace,
1101
1129
  callback,
1102
- });
1130
+ })
1103
1131
  },
1104
1132
  // esbuild's own `resolve` re-runs every plugin EXCEPT the caller's (its recursion guard,
1105
1133
  // keyed on plugin name). Sharing one name would make it skip the whole chain and fall
@@ -1122,16 +1150,16 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
1122
1150
  ? resolvedFromHook(specifier, result)
1123
1151
  : build.resolve(specifier, { ...options, pluginName: name }),
1124
1152
  ),
1125
- };
1126
- await plugin.setup(proxy);
1153
+ }
1154
+ await plugin.setup(proxy)
1127
1155
  }
1128
- sealed = true;
1129
- if (registered.length === 0) return;
1130
- const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'));
1131
- build.onResolve({ filter }, args => dispatch(args));
1156
+ sealed = true
1157
+ if (registered.length === 0) return
1158
+ const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'))
1159
+ build.onResolve({ filter }, args => dispatch(args))
1132
1160
  },
1133
1161
  },
1134
- ];
1162
+ ]
1135
1163
  }
1136
1164
 
1137
1165
  /**
@@ -1139,7 +1167,7 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
1139
1167
  * this specifier, do not bundle it" - esbuild's own rule, verified against the binary.
1140
1168
  */
1141
1169
  function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveResult {
1142
- const resolved = result.path ?? (result.external ? specifier : '');
1170
+ const resolved = result.path ?? (result.external ? specifier : '')
1143
1171
  return {
1144
1172
  errors: (result.errors ?? []) as ResolveResult['errors'],
1145
1173
  warnings: (result.warnings ?? []) as ResolveResult['warnings'],
@@ -1149,7 +1177,7 @@ function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveRe
1149
1177
  namespace: result.namespace ?? (result.path ? 'file' : ''),
1150
1178
  suffix: result.suffix ?? '',
1151
1179
  pluginData: result.pluginData as unknown,
1152
- };
1180
+ }
1153
1181
  }
1154
1182
 
1155
1183
  /**
@@ -1163,7 +1191,7 @@ function resolveHookSettled(result: OnResolveResult) {
1163
1191
  result.external !== undefined ||
1164
1192
  (result.errors?.length ?? 0) > 0 ||
1165
1193
  (result.warnings?.length ?? 0) > 0
1166
- );
1194
+ )
1167
1195
  }
1168
1196
 
1169
1197
  function clientBuildPlugins(
@@ -1172,55 +1200,66 @@ function clientBuildPlugins(
1172
1200
  extra: Plugin[] = [],
1173
1201
  reactLite = false,
1174
1202
  ): Plugin[] {
1175
- return coalesceResolveHooks(instrumentedPlugins([
1176
- serverOnlyClientImportPlugin(),
1177
- // The virtual-module plugins (`extra`: generated entries + the shared route
1178
- // runtime) go first so they claim their own synthetic specifiers before the
1179
- // resolve-chain plugins are asked about them one entry point per route is
1180
- // otherwise offered to every plugin in the chain that only ever declines it.
1181
- ...extra,
1182
- ...getBundlerExtensions().clientEsbuildPlugins(config),
1183
- linkedPackageClientResolvePlugin(config),
1184
- clientStaticAssetPlugin(config),
1185
- pipeline.plugin(),
1186
- importAliasPlugin(config, reactLite),
1187
- cssModuleClientPlugin(),
1188
- ]), 'pnext-client-resolve-chain');
1203
+ return coalesceResolveHooks(
1204
+ instrumentedPlugins([
1205
+ serverOnlyClientImportPlugin(),
1206
+ // The virtual-module plugins (`extra`: generated entries + the shared route
1207
+ // runtime) go first so they claim their own synthetic specifiers before the
1208
+ // resolve-chain plugins are asked about them one entry point per route is
1209
+ // otherwise offered to every plugin in the chain that only ever declines it.
1210
+ ...extra,
1211
+ ...getBundlerExtensions().clientEsbuildPlugins(config),
1212
+ linkedPackageClientResolvePlugin(config),
1213
+ clientStaticAssetPlugin(config),
1214
+ pipeline.plugin(),
1215
+ importAliasPlugin(config, reactLite),
1216
+ cssModuleClientPlugin(),
1217
+ ]),
1218
+ 'pnext-client-resolve-chain',
1219
+ )
1189
1220
  }
1190
1221
 
1191
1222
  function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
1192
- const aliases = { ...getImportAliasExtensions().aliases(config, 'client') };
1223
+ // preact core/hooks/jsx-runtime are single-instance, compat or not: an app with its
1224
+ // own `preact` in node_modules otherwise bundles a second physical copy next to the
1225
+ // framework's, and hooks called from framework components (Link) read a null current
1226
+ // component off options the app's copy never installed. The server runtime pins the
1227
+ // same set unconditionally (loader's coreAliases); the client build must match.
1228
+ const aliases: Record<string, string> = {
1229
+ ...frameworkRuntimeAliasEntries(),
1230
+ ...getImportAliasExtensions().aliases(config, 'client'),
1231
+ }
1193
1232
  // Suspense-free tier: the app's `react` imports resolve to the compat-free lite shim, so the
1194
1233
  // bundle ships preact core + hooks without preact/compat (see clientSuspenseFree).
1195
1234
  if (reactLite && aliases.react) {
1196
- aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts');
1235
+ aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
1197
1236
  }
1198
- const specifiers = Object.keys(aliases);
1237
+ const specifiers = Object.keys(aliases)
1199
1238
  return {
1200
1239
  name: 'pnext-import-alias',
1201
1240
  setup(build) {
1202
1241
  build.onResolve({ filter: /^(?:react(?:-dom)?|next)(?:\/|$)/ }, args => {
1203
- if (aliases[args.path]) return undefined;
1204
- const message = getImportAliasExtensions().missingImportError(config, args.path);
1205
- return message ? { errors: [{ text: message }] } : undefined;
1206
- });
1207
- if (specifiers.length === 0) return;
1242
+ if (aliases[args.path]) return undefined
1243
+ const message = getImportAliasExtensions().missingImportError(config, args.path)
1244
+ return message ? { errors: [{ text: message }] } : undefined
1245
+ })
1246
+ if (specifiers.length === 0) return
1208
1247
  build.onResolve(
1209
1248
  { filter: new RegExp(`^(${specifiers.map(escapeRegex).join('|')})$`) },
1210
1249
  async args => {
1211
- const target = aliases[args.path];
1212
- if (!target) return undefined;
1213
- if (path.isAbsolute(target)) return { path: target };
1250
+ const target = aliases[args.path]
1251
+ if (!target) return undefined
1252
+ if (path.isAbsolute(target)) return { path: target }
1214
1253
  return build.resolve(target, {
1215
1254
  kind: args.kind,
1216
1255
  importer: args.importer,
1217
1256
  namespace: args.namespace,
1218
1257
  resolveDir: args.resolveDir,
1219
- });
1258
+ })
1220
1259
  },
1221
- );
1260
+ )
1222
1261
  },
1223
- };
1262
+ }
1224
1263
  }
1225
1264
 
1226
1265
  // preact core + preact/compat + react/react-dom/next are single-instance: they must ALWAYS resolve
@@ -1230,11 +1269,11 @@ function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
1230
1269
  // `preact` to the linked copy while pnext-internal compat code keeps resolving to the alias -
1231
1270
  // shipping TWO physical preact cores. That breaks single-instance option hooks, context identity and
1232
1271
  // error interception. Never let the linked-package resolver shadow these; the alias owns them.
1233
- const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/;
1272
+ const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/
1234
1273
 
1235
1274
  function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1236
- const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
1237
- const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts');
1275
+ const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts')
1276
+ const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts')
1238
1277
  return {
1239
1278
  name: 'pnext-linked-package-client-resolve',
1240
1279
  setup(build) {
@@ -1243,36 +1282,36 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1243
1282
  // outputs under node_modules paths, e.g. @next/third-parties client
1244
1283
  // components) where no sibling ./preact exists; only a real sibling
1245
1284
  // file (an app's own ./preact.*) opts out.
1246
- if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact };
1285
+ if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
1247
1286
  const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
1248
1287
  existsSync(path.join(args.resolveDir, `preact${ext}`)),
1249
- );
1250
- if (hasRealSibling) return undefined;
1251
- return { path: compatPreact };
1252
- });
1288
+ )
1289
+ if (hasRealSibling) return undefined
1290
+ return { path: compatPreact }
1291
+ })
1253
1292
  // One build's worth of memo, keyed on exactly what esbuild hands us: the resolver walks
1254
1293
  // node_modules and probes package targets per call, and a directory of components importing
1255
1294
  // the same few packages asks the same question repeatedly. Per-BUILD, so a linked package's
1256
1295
  // layout changing between builds is still seen - esbuild already assumes it is fixed within one.
1257
- const linked = new Map<string, string | undefined>();
1296
+ const linked = new Map<string, string | undefined>()
1258
1297
  build.onResolve({ filter: /^[^./#][^:]*$/ }, args => {
1259
- if (frameworkAliasedSpecifier.test(args.path)) return undefined;
1260
- const dir = args.resolveDir || config.root;
1261
- const key = `${dir}\0${args.path}`;
1262
- let resolved = linked.get(key);
1298
+ if (frameworkAliasedSpecifier.test(args.path)) return undefined
1299
+ const dir = args.resolveDir || config.root
1300
+ const key = `${dir}\0${args.path}`
1301
+ let resolved = linked.get(key)
1263
1302
  if (resolved === undefined && !linked.has(key)) {
1264
1303
  resolved = resolveLinkedPackageSpecifier(
1265
1304
  config.root,
1266
1305
  path.join(dir, 'pnext-resolve.ts'),
1267
1306
  args.path,
1268
1307
  ['browser', 'style', 'import', 'default'],
1269
- );
1270
- linked.set(key, resolved);
1308
+ )
1309
+ linked.set(key, resolved)
1271
1310
  }
1272
- return resolved ? { path: resolved } : undefined;
1273
- });
1311
+ return resolved ? { path: resolved } : undefined
1312
+ })
1274
1313
  },
1275
- };
1314
+ }
1276
1315
  }
1277
1316
 
1278
1317
  // Mirror of the server build's static-asset seam for the CLIENT bundle. A static image import from a
@@ -1280,7 +1319,7 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1280
1319
  // object - with the asset emitted under `/_next/static/media/...` - rather than a bare `file` loader
1281
1320
  // URL string. Uses core's `staticAssetModule` extension seam so compat (next/image) supplies the
1282
1321
  // descriptor and pure-core apps fall back to the generic URL module.
1283
- const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/;
1322
+ const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/
1284
1323
 
1285
1324
  function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
1286
1325
  return {
@@ -1291,66 +1330,66 @@ function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
1291
1330
  // `*.svg`) preempts the generic static-image resolver: defer so the
1292
1331
  // compat loader-rule plugin runs the chain instead.
1293
1332
  if (getAssetExtensions().hasLoaderRuleFor(args.path)) {
1294
- return undefined;
1333
+ return undefined
1295
1334
  }
1296
1335
  return {
1297
1336
  path: resolveAssetPath(args.path, args.resolveDir),
1298
1337
  namespace: 'pnext-client-static-asset',
1299
- };
1300
- });
1338
+ }
1339
+ })
1301
1340
  build.onLoad({ filter: /.*/, namespace: 'pnext-client-static-asset' }, async args => ({
1302
1341
  contents: await staticImageModuleSource(config, args.path),
1303
1342
  loader: 'js',
1304
- }));
1343
+ }))
1305
1344
  },
1306
- };
1345
+ }
1307
1346
  }
1308
1347
 
1309
1348
  function resolveAssetPath(specifier: string, resolveDir: string) {
1310
- const { sourcePath, hash } = splitAssetHash(specifier);
1311
- const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath);
1312
- return `${resolved}${hash}`;
1349
+ const { sourcePath, hash } = splitAssetHash(specifier)
1350
+ const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)
1351
+ return `${resolved}${hash}`
1313
1352
  }
1314
1353
 
1315
1354
  function splitAssetHash(specifier: string) {
1316
- const index = specifier.search(/[?#]/);
1355
+ const index = specifier.search(/[?#]/)
1317
1356
  return index === -1
1318
1357
  ? { sourcePath: specifier, hash: '' }
1319
- : { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) };
1358
+ : { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
1320
1359
  }
1321
1360
 
1322
1361
  async function staticImageModuleSource(config: ResolvedConfig, file: string) {
1323
- const { sourcePath } = splitAssetHash(file);
1324
- const bytes = new Uint8Array(await readFile(sourcePath));
1325
- const emitted: string[] = [];
1362
+ const { sourcePath } = splitAssetHash(file)
1363
+ const bytes = new Uint8Array(await readFile(sourcePath))
1364
+ const emitted: string[] = []
1326
1365
  const emit = (relative: string) => {
1327
- emitted.push(relative);
1328
- return `/${relative}`;
1329
- };
1330
- const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit });
1331
- const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit);
1366
+ emitted.push(relative)
1367
+ return `/${relative}`
1368
+ }
1369
+ const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
1370
+ const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
1332
1371
  for (const relative of emitted) {
1333
- const target = path.join(config.outPath, 'public', ...relative.split('/'));
1334
- await mkdir(path.dirname(target), { recursive: true });
1335
- if (!existsSync(target)) await copyFile(sourcePath, target);
1372
+ const target = path.join(config.outPath, 'public', ...relative.split('/'))
1373
+ await mkdir(path.dirname(target), { recursive: true })
1374
+ if (!existsSync(target)) await copyFile(sourcePath, target)
1336
1375
  }
1337
- return source;
1376
+ return source
1338
1377
  }
1339
1378
 
1340
1379
  // Core's generic static-asset module (no compat override): emit under a hashed
1341
1380
  // `/_next/static/media` URL and export the URL string as default. Mirrors the
1342
- // identically named helper in runtime/server.ts + dev/imports.ts.
1381
+ // identically named helper in runtime/loader.ts + runtime/modules.ts.
1343
1382
  function coreStaticAssetModule(
1344
1383
  sourcePath: string,
1345
1384
  bytes: Uint8Array,
1346
1385
  emit: (relative: string) => string,
1347
1386
  ): string {
1348
- const ext = path.extname(sourcePath).toLowerCase() || '.bin';
1349
- const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
1350
- const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-');
1351
- const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`);
1352
- const src = emit(relative);
1353
- return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`;
1387
+ const ext = path.extname(sourcePath).toLowerCase() || '.bin'
1388
+ const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8)
1389
+ const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-')
1390
+ const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`)
1391
+ const src = emit(relative)
1392
+ return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`
1354
1393
  }
1355
1394
 
1356
1395
  /**
@@ -1366,91 +1405,93 @@ function coreStaticAssetModule(
1366
1405
  * loader; scoped to the source roots, so third-party node_modules .js keeps esbuild's default loader.
1367
1406
  */
1368
1407
  function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1369
- const sourceRewrite = nextCompatEnabled(config);
1370
- const compiler = getCompatModeExtensions().reactCompilerOptions(config);
1371
- const asyncPre = hasClientSourceAsyncPreTransforms();
1408
+ const sourceRewrite = nextCompatEnabled(config)
1409
+ const compiler = getCompatModeExtensions().reactCompilerOptions(config)
1410
+ const asyncPre = hasClientSourceAsyncPreTransforms()
1372
1411
  // Under a source root, and under a root only first-party source plus transpiled packages - said in
1373
1412
  // the FILTER, not the callback: a dependency's files are the bulk of a real graph, so a callback
1374
1413
  // would decline most of them one at a time.
1375
- const roots = clientSourceRoots(config).map(root => escapeRegex(root)).join('|');
1376
- const sep = escapeRegex(path.sep);
1414
+ const roots = clientSourceRoots(config)
1415
+ .map(root => escapeRegex(root))
1416
+ .join('|')
1417
+ const sep = escapeRegex(path.sep)
1377
1418
  const filter = new RegExp(
1378
1419
  `^(?:${roots})(?:${sep}${nonSegmentPattern('node_modules', sep)})*${sep}[^${sep}]*\\.(?:[cm]?[jt]sx?)$`,
1379
- );
1380
- const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined;
1420
+ )
1421
+ const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
1381
1422
  // One in-flight pipeline per file for the whole build: `warm()` and the
1382
1423
  // onLoad hook share it, so a warmed file is already done (or in flight on
1383
1424
  // oxc's threadpool) by the time esbuild asks for it, and a component shared
1384
1425
  // by twenty routes is prepared once.
1385
- const prepared = new Map<string, Promise<OnLoadOutput>>();
1426
+ const prepared = new Map<string, Promise<OnLoadOutput>>()
1386
1427
  // Deferred dynamic refs THIS pipeline rewrote — persisted as the out-dir sidecar.
1387
- const deferredRefs = new Map<string, DeferredDynamicRef>();
1428
+ const deferredRefs = new Map<string, DeferredDynamicRef>()
1388
1429
  // The text esbuild actually parsed, kept so the prebuilt seam can read a
1389
1430
  // file's framework imports off the POST-transform source — a compat rewrite
1390
1431
  // can add or rename one, and the seam must offer exactly what is there.
1391
- const loaded = new Map<string, string>();
1432
+ const loaded = new Map<string, string>()
1392
1433
 
1393
1434
  function prepare(resolved: string): Promise<OnLoadOutput> {
1394
- const existing = prepared.get(resolved);
1395
- if (existing) return existing;
1396
- const pending = runPipeline(resolved);
1397
- prepared.set(resolved, pending);
1398
- return pending;
1435
+ const existing = prepared.get(resolved)
1436
+ if (existing) return existing
1437
+ const pending = runPipeline(resolved)
1438
+ prepared.set(resolved, pending)
1439
+ return pending
1399
1440
  }
1400
1441
 
1401
1442
  async function runPipeline(resolved: string): Promise<OnLoadOutput> {
1402
- const inNodeModules = resolved.split(path.sep).includes('node_modules');
1443
+ const inNodeModules = resolved.split(path.sep).includes('node_modules')
1403
1444
  // A nested node_modules under a source root is a dependency, not app
1404
1445
  // source: only an explicitly transpiled package opts into the rewrites,
1405
1446
  // and none of them are ever React-Compiled.
1406
- if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined;
1407
- clientProfile.count('loadFiles');
1447
+ if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
1448
+ clientProfile.count('loadFiles')
1408
1449
  // Build-scoped cache: the route-fact walk has already read most of these,
1409
1450
  // so this is a map lookup rather than a second read of the same file.
1410
- const original = await clientProfile.timeAsync('read', () => readSourceText(resolved));
1411
- let contents = original;
1451
+ const original = await clientProfile.timeAsync('read', () => readSourceText(resolved))
1452
+ let contents = original
1412
1453
  // Worker bundling runs first and awaits: it consumes `import.meta.url`,
1413
1454
  // which the sync chain inlines.
1414
1455
  if (asyncPre) {
1415
- contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root);
1456
+ contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
1416
1457
  }
1417
1458
  contents = clientProfile.time('chain', () =>
1418
1459
  applyClientSourceTransforms(contents, resolved, config.root),
1419
- );
1420
- contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved));
1460
+ )
1461
+ contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
1421
1462
  if (dev && devDynamicSplitEnabled()) {
1422
1463
  contents = rewriteDeferredDynamicImports(
1423
1464
  contents,
1424
1465
  resolved,
1425
1466
  specifier => resolveImport(rootFromFile(resolved), resolved, specifier),
1426
1467
  target => {
1427
- const id = clientReferenceId(target.file, target.exportName);
1428
- registerDeferredDynamicRef(id, target);
1429
- deferredRefs.set(id, target);
1430
- return deferredDynamicChunkHref({ id });
1468
+ const id = clientReferenceId(target.file, target.exportName)
1469
+ registerDeferredDynamicRef(id, target)
1470
+ deferredRefs.set(id, target)
1471
+ return deferredDynamicChunkHref({ id })
1431
1472
  },
1432
- );
1473
+ )
1433
1474
  }
1434
1475
  if (compiler && !inNodeModules && shouldReactCompile(contents, resolved)) {
1435
1476
  contents = await clientProfile.timeAsync('reactCompiler', () =>
1436
1477
  reactCompiled(resolved, contents, compiler),
1437
- );
1478
+ )
1438
1479
  }
1439
1480
  // Unchanged sources go back to esbuild's native loader; only .js/.mjs
1440
1481
  // must be claimed to force the jsx parse.
1441
- loaded.set(resolved, contents);
1442
- if (contents === original && !/\.m?js$/.test(resolved)) return undefined;
1443
- return { contents, loader: clientSourceLoader(resolved) };
1482
+ loaded.set(resolved, contents)
1483
+ if (contents === original && !/\.m?js$/.test(resolved)) return undefined
1484
+ return { contents, loader: clientSourceLoader(resolved) }
1444
1485
  }
1445
1486
 
1446
1487
  return {
1447
1488
  /** The parsed text of a file this build has loaded, if it has. */
1448
1489
  sourceOf(file: string) {
1449
- return loaded.get(file);
1490
+ return loaded.get(file)
1450
1491
  },
1451
1492
  /** Deferred dynamic refs rewritten during this build (dev split). */
1452
1493
  deferredRefs() {
1453
- return deferredRefs;
1494
+ return deferredRefs
1454
1495
  },
1455
1496
  /**
1456
1497
  * Start the pipeline for sources the route table already names, before esbuild runs. Without it
@@ -1459,15 +1500,15 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1459
1500
  */
1460
1501
  /** `warm` for whole routes - every client source the route table names. */
1461
1502
  warmRoutes(routes: RouteManifestEntry[]) {
1462
- this.warm(routeClientSources(routes));
1503
+ this.warm(routeClientSources(routes))
1463
1504
  },
1464
1505
  warm(files: Iterable<string>) {
1465
1506
  for (const file of files) {
1466
- const resolved = path.resolve(file);
1467
- if (!filter.test(resolved) && !transpiled?.test(resolved)) continue;
1507
+ const resolved = path.resolve(file)
1508
+ if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
1468
1509
  // Errors surface at the onLoad await, where esbuild attributes them to
1469
1510
  // the importing module; nothing must reject on the warm path.
1470
- prepare(resolved).catch(() => undefined);
1511
+ prepare(resolved).catch(() => undefined)
1471
1512
  }
1472
1513
  },
1473
1514
  /**
@@ -1483,24 +1524,24 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1483
1524
  // namespace: 'file' — an onLoad without a namespace matches EVERY
1484
1525
  // namespace, which would hijack virtual modules (e.g. the server-action
1485
1526
  // client stub) whose paths end in .js and feed back the original source.
1486
- build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)));
1527
+ build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)))
1487
1528
  // Second hook, not a wider first one: `transpilePackages` opts a
1488
1529
  // dependency's own files into the same pipeline, and they are the only
1489
1530
  // node_modules paths that ever produce contents.
1490
1531
  if (transpiled) {
1491
1532
  build.onLoad({ filter: transpiled, namespace: 'file' }, args =>
1492
1533
  prepare(path.resolve(args.path)),
1493
- );
1534
+ )
1494
1535
  }
1495
1536
  },
1496
- };
1537
+ }
1497
1538
  },
1498
- };
1539
+ }
1499
1540
  }
1500
1541
 
1501
- type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined;
1542
+ type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
1502
1543
 
1503
- export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>;
1544
+ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
1504
1545
 
1505
1546
  /**
1506
1547
  * Open the client-source pipeline early so the build can feed it routes as the fact scan resolves
@@ -1509,64 +1550,64 @@ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
1509
1550
  * otherwise create for itself.
1510
1551
  */
1511
1552
  export function startClientSources(config: ResolvedConfig): ClientSourcePipeline {
1512
- conventionCache.clear();
1513
- return createClientSourcePipeline(config);
1553
+ conventionCache.clear()
1554
+ return createClientSourcePipeline(config)
1514
1555
  }
1515
1556
 
1516
1557
  // The compiled-source cache is keyed on the file and a hash of its post-rewrite
1517
1558
  // input, so a rebuild of an unchanged file skips the oxc pass. It holds the
1518
1559
  // PROMISE, so two concurrent loads of the same source (two routes sharing a
1519
1560
  // component) share one compile instead of racing two.
1520
- const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>();
1561
+ const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
1521
1562
 
1522
1563
  function reactCompiled(
1523
1564
  file: string,
1524
1565
  source: string,
1525
1566
  options: NonNullable<ReturnType<CompatModeExtensions['reactCompilerOptions']>>,
1526
1567
  ) {
1527
- const hash = Bun.hash.wyhash(`${options.target}\0${source}`);
1528
- const cached = reactCompiledCache.get(file);
1529
- if (cached?.hash === hash) return cached.code;
1530
- const code = transformReactCompiler(source, file, options);
1531
- reactCompiledCache.set(file, { hash, code });
1532
- return code;
1568
+ const hash = Bun.hash.wyhash(`${options.target}\0${source}`)
1569
+ const cached = reactCompiledCache.get(file)
1570
+ if (cached?.hash === hash) return cached.code
1571
+ const code = transformReactCompiler(source, file, options)
1572
+ reactCompiledCache.set(file, { hash, code })
1573
+ return code
1533
1574
  }
1534
1575
 
1535
1576
  // config.root can sit outside the workspace root (and vice versa), so both
1536
1577
  // anchor the filter — as does each one's realpath, since esbuild reports
1537
1578
  // resolved paths through the OS realpath.
1538
1579
  function clientSourceRoots(config: ResolvedConfig) {
1539
- const roots = new Set<string>();
1580
+ const roots = new Set<string>()
1540
1581
  for (const root of [config.root, config.workspaceRoot]) {
1541
- const resolved = path.resolve(root);
1542
- roots.add(resolved);
1582
+ const resolved = path.resolve(root)
1583
+ roots.add(resolved)
1543
1584
  try {
1544
- roots.add(realpathSync.native(resolved));
1585
+ roots.add(realpathSync.native(resolved))
1545
1586
  } catch {
1546
1587
  // A root that cannot be realpath'd is already covered by its literal form.
1547
1588
  }
1548
1589
  }
1549
- return [...roots];
1590
+ return [...roots]
1550
1591
  }
1551
1592
 
1552
1593
  // `.cjs` keeps the plain js loader: CommonJS never carries JSX, and parsing a
1553
1594
  // `a < b` comparison as JSX would fail the build.
1554
1595
  function clientSourceLoader(file: string): 'js' | 'ts' | 'tsx' | 'jsx' {
1555
- if (file.endsWith('.tsx')) return 'tsx';
1556
- if (/\.[cm]?ts$/.test(file)) return 'ts';
1557
- return file.endsWith('.cjs') ? 'js' : 'jsx';
1596
+ if (file.endsWith('.tsx')) return 'tsx'
1597
+ if (/\.[cm]?ts$/.test(file)) return 'ts'
1598
+ return file.endsWith('.cjs') ? 'js' : 'jsx'
1558
1599
  }
1559
1600
 
1560
1601
  // A path segment that is NOT `word`, spelled positively: esbuild's Go regexp has
1561
1602
  // no lookahead, so the negation enumerates "differs at some position / ends
1562
1603
  // early / runs longer".
1563
1604
  function nonSegmentPattern(word: string, sep: string) {
1564
- const parts = [`${word}[^${sep}]+`];
1605
+ const parts = [`${word}[^${sep}]+`]
1565
1606
  for (let index = 0; index < word.length; index += 1) {
1566
- const prefix = word.slice(0, index);
1567
- parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`);
1607
+ const prefix = word.slice(0, index)
1608
+ parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`)
1568
1609
  }
1569
- return `(?:${parts.join('|')})`;
1610
+ return `(?:${parts.join('|')})`
1570
1611
  }
1571
1612
 
1572
1613
  /**
@@ -1575,45 +1616,45 @@ function nonSegmentPattern(word: string, sep: string) {
1575
1616
  * path on the hook.
1576
1617
  */
1577
1618
  function transpiledPackageFilter(roots: string): RegExp | undefined {
1578
- const sep = escapeRegex(path.sep);
1579
- const names = getExternalPackagePolicy().transpiled?.();
1580
- if (names?.length === 0) return undefined;
1619
+ const sep = escapeRegex(path.sep)
1620
+ const names = getExternalPackagePolicy().transpiled?.()
1621
+ if (names?.length === 0) return undefined
1581
1622
  const packages = names
1582
1623
  ? `(?:${names.map(name => escapeRegex(name).split('/').join(sep)).join('|')})${sep}`
1583
- : '';
1584
- return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`);
1624
+ : ''
1625
+ return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
1585
1626
  }
1586
1627
 
1587
1628
  function isTranspiledPackageFile(file: string) {
1588
- const parts = file.split(path.sep);
1589
- const nodeModules = parts.lastIndexOf('node_modules');
1590
- if (nodeModules === -1) return false;
1591
- const first = parts[nodeModules + 1];
1592
- if (!first) return false;
1593
- const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first;
1594
- return getExternalPackagePolicy().transpile(name);
1629
+ const parts = file.split(path.sep)
1630
+ const nodeModules = parts.lastIndexOf('node_modules')
1631
+ if (nodeModules === -1) return false
1632
+ const first = parts[nodeModules + 1]
1633
+ if (!first) return false
1634
+ const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first
1635
+ return getExternalPackagePolicy().transpile(name)
1595
1636
  }
1596
1637
 
1597
1638
  function virtualEntryPath(routeId: string) {
1598
- return `${virtualEntryNamespace}:${routeId}`;
1639
+ return `${virtualEntryNamespace}:${routeId}`
1599
1640
  }
1600
1641
 
1601
1642
  function virtualEntryPlugin(entries: { route: RouteManifestEntry; source: string }[]): Plugin {
1602
- const sources = new Map(entries.map(entry => [entry.route.id, entry.source]));
1643
+ const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
1603
1644
  return {
1604
1645
  name: 'pnext-virtual-client-entry',
1605
1646
  setup(build) {
1606
1647
  build.onResolve({ filter: new RegExp(`^${escapeRegex(virtualEntryNamespace)}:`) }, args => ({
1607
1648
  path: args.path.slice(virtualEntryNamespace.length + 1),
1608
1649
  namespace: virtualEntryNamespace,
1609
- }));
1650
+ }))
1610
1651
  build.onLoad({ filter: /.*/, namespace: virtualEntryNamespace }, args => {
1611
- const contents = sources.get(args.path);
1612
- if (contents === undefined) return undefined;
1613
- return { contents, loader: 'ts', resolveDir: process.cwd() };
1614
- });
1652
+ const contents = sources.get(args.path)
1653
+ if (contents === undefined) return undefined
1654
+ return { contents, loader: 'ts', resolveDir: process.cwd() }
1655
+ })
1615
1656
  },
1616
- };
1657
+ }
1617
1658
  }
1618
1659
 
1619
1660
  // The shared route runtime: one virtual module per build, imported by every
@@ -1626,14 +1667,14 @@ function clientRuntimePlugin(facts: ClientRuntimeFacts): Plugin {
1626
1667
  build.onResolve({ filter: new RegExp(`^${escapeRegex(CLIENT_RUNTIME_MODULE)}$`) }, () => ({
1627
1668
  path: CLIENT_RUNTIME_MODULE,
1628
1669
  namespace: CLIENT_RUNTIME_MODULE,
1629
- }));
1670
+ }))
1630
1671
  build.onLoad({ filter: /.*/, namespace: CLIENT_RUNTIME_MODULE }, () => ({
1631
1672
  contents: clientRuntimeSource(facts),
1632
1673
  loader: 'ts',
1633
1674
  resolveDir: process.cwd(),
1634
- }));
1675
+ }))
1635
1676
  },
1636
- };
1677
+ }
1637
1678
  }
1638
1679
 
1639
1680
  async function profileClientBuild<T>(
@@ -1642,25 +1683,18 @@ async function profileClientBuild<T>(
1642
1683
  task: () => Promise<T>,
1643
1684
  ) {
1644
1685
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1645
- if (!process.env.PNEXT_DEV_PROFILE) return task();
1646
- const start = performance.now();
1686
+ if (!process.env.PNEXT_DEV_PROFILE) return task()
1687
+ const start = performance.now()
1647
1688
  try {
1648
- return await task();
1689
+ return await task()
1649
1690
  } finally {
1650
- const mode = dev ? 'dev' : 'prod';
1691
+ const mode = dev ? 'dev' : 'prod'
1651
1692
  console.log(
1652
1693
  `dev-profile client build ${mode} ${route.route} in ${formatDuration(performance.now() - start)}`,
1653
- );
1694
+ )
1654
1695
  }
1655
1696
  }
1656
1697
 
1657
- function formatDuration(durationMs: number) {
1658
- const ms = Math.max(0, durationMs);
1659
- if (ms < 10) return `${ms.toFixed(1)}ms`;
1660
- if (ms < 1000) return `${Math.round(ms)}ms`;
1661
- return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
1662
- }
1663
-
1664
1698
  function serverOnlyClientImportPlugin(): Plugin {
1665
1699
  return {
1666
1700
  name: 'pnext-server-only-client-import',
@@ -1674,26 +1708,26 @@ function serverOnlyClientImportPlugin(): Plugin {
1674
1708
  },
1675
1709
  ],
1676
1710
  }),
1677
- );
1711
+ )
1678
1712
  },
1679
- };
1713
+ }
1680
1714
  }
1681
1715
 
1682
1716
  interface EsbuildLikeError extends Error {
1683
1717
  errors?: {
1684
- text?: string;
1718
+ text?: string
1685
1719
  location?: {
1686
- file?: string;
1687
- };
1688
- }[];
1720
+ file?: string
1721
+ }
1722
+ }[]
1689
1723
  }
1690
1724
 
1691
1725
  interface TraceParent {
1692
- file: string;
1693
- specifier: string;
1726
+ file: string
1727
+ specifier: string
1694
1728
  }
1695
1729
 
1696
- const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css'];
1730
+ const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
1697
1731
 
1698
1732
  function withClientImportTrace(
1699
1733
  error: unknown,
@@ -1701,16 +1735,16 @@ function withClientImportTrace(
1701
1735
  route: RouteManifestEntry,
1702
1736
  entrySource: string,
1703
1737
  ) {
1704
- const message = error instanceof Error ? error.message : String(error);
1705
- const trace = clientImportTrace(config, route, entrySource, error);
1706
- if (!trace) return error;
1707
- const next = new Error(`${message}\n\n${trace}`);
1738
+ const message = error instanceof Error ? error.message : String(error)
1739
+ const trace = clientImportTrace(config, route, entrySource, error)
1740
+ if (!trace) return error
1741
+ const next = new Error(`${message}\n\n${trace}`)
1708
1742
  if (error instanceof Error) {
1709
- next.name = error.name;
1710
- next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack;
1711
- Object.assign(next, error);
1743
+ next.name = error.name
1744
+ next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack
1745
+ Object.assign(next, error)
1712
1746
  }
1713
- return next;
1747
+ return next
1714
1748
  }
1715
1749
 
1716
1750
  function clientImportTrace(
@@ -1719,51 +1753,51 @@ function clientImportTrace(
1719
1753
  entrySource: string,
1720
1754
  error: unknown,
1721
1755
  ) {
1722
- const unresolved = unresolvedImportFromError(error);
1723
- if (!unresolved) return undefined;
1756
+ const unresolved = unresolvedImportFromError(error)
1757
+ if (!unresolved) return undefined
1724
1758
 
1725
- const entryFile = `${route.id}.ts`;
1726
- const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined;
1727
- const parents = new Map<string, TraceParent>();
1728
- const visited = new Set<string>();
1729
- const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }];
1730
- let unresolvedImporter: string | undefined;
1759
+ const entryFile = `${route.id}.ts`
1760
+ const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined
1761
+ const parents = new Map<string, TraceParent>()
1762
+ const visited = new Set<string>()
1763
+ const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }]
1764
+ let unresolvedImporter: string | undefined
1731
1765
 
1732
1766
  while (queue.length > 0) {
1733
- const current = queue.shift();
1734
- if (!current || visited.has(current.file)) continue;
1735
- visited.add(current.file);
1736
- if (visited.size > 2000) break;
1767
+ const current = queue.shift()
1768
+ if (!current || visited.has(current.file)) continue
1769
+ visited.add(current.file)
1770
+ if (visited.size > 2000) break
1737
1771
 
1738
1772
  for (const specifier of localImports(current.file, current.source)) {
1739
- const resolved = resolveTraceImport(config.root, current.file, specifier);
1773
+ const resolved = resolveTraceImport(config.root, current.file, specifier)
1740
1774
  if (!resolved) {
1741
1775
  if (
1742
1776
  specifier === unresolved.specifier &&
1743
1777
  (!failedFile || sameFile(current.file, failedFile))
1744
1778
  ) {
1745
- unresolvedImporter = current.file;
1746
- queue.length = 0;
1747
- break;
1779
+ unresolvedImporter = current.file
1780
+ queue.length = 0
1781
+ break
1748
1782
  }
1749
- continue;
1783
+ continue
1750
1784
  }
1751
1785
 
1752
- if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier });
1786
+ if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
1753
1787
  if (failedFile && sameFile(resolved, failedFile)) {
1754
- unresolvedImporter = resolved;
1755
- queue.length = 0;
1756
- break;
1788
+ unresolvedImporter = resolved
1789
+ queue.length = 0
1790
+ break
1757
1791
  }
1758
1792
  if (!visited.has(resolved) && !isAssetLike(resolved)) {
1759
- queue.push({ file: resolved, source: readTextSyncSafe(resolved) });
1793
+ queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
1760
1794
  }
1761
1795
  }
1762
1796
  }
1763
1797
 
1764
- if (!unresolvedImporter) return undefined;
1765
- const trace = tracePath(entryFile, unresolvedImporter, parents);
1766
- if (trace.length === 0) return undefined;
1798
+ if (!unresolvedImporter) return undefined
1799
+ const trace = tracePath(entryFile, unresolvedImporter, parents)
1800
+ if (trace.length === 0) return undefined
1767
1801
 
1768
1802
  return [
1769
1803
  `pnext client import trace for route ${route.route}:`,
@@ -1771,135 +1805,135 @@ function clientImportTrace(
1771
1805
  (file, index) => `${index === 0 ? ' ' : ' -> '}${displayTraceFile(config, file)}`,
1772
1806
  ),
1773
1807
  ` -> ${unresolved.specifier} (unresolved in browser client bundle)`,
1774
- ].join('\n');
1808
+ ].join('\n')
1775
1809
  }
1776
1810
 
1777
1811
  /** True when the build failed because an import could not be resolved. */
1778
1812
  function hasUnresolvedImportError(error: unknown): boolean {
1779
- const errors = (error as EsbuildLikeError).errors;
1813
+ const errors = (error as EsbuildLikeError).errors
1780
1814
  if (Array.isArray(errors)) {
1781
- return errors.some(item => item.text?.startsWith('Could not resolve '));
1815
+ return errors.some(item => item.text?.startsWith('Could not resolve '))
1782
1816
  }
1783
- const message = error instanceof Error ? error.message : String(error);
1784
- return message.includes('Could not resolve ');
1817
+ const message = error instanceof Error ? error.message : String(error)
1818
+ return message.includes('Could not resolve ')
1785
1819
  }
1786
1820
 
1787
1821
  function unresolvedImportFromError(error: unknown) {
1788
- const buildError = error as EsbuildLikeError;
1789
- const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '));
1790
- const message = error instanceof Error ? error.message : String(error);
1791
- const text = diagnostic?.text ?? message;
1792
- const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1];
1793
- if (!specifier) return undefined;
1822
+ const buildError = error as EsbuildLikeError
1823
+ const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '))
1824
+ const message = error instanceof Error ? error.message : String(error)
1825
+ const text = diagnostic?.text ?? message
1826
+ const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1]
1827
+ if (!specifier) return undefined
1794
1828
  return {
1795
1829
  specifier,
1796
1830
  file: diagnostic?.location?.file ?? errorFileFromMessage(message),
1797
- };
1831
+ }
1798
1832
  }
1799
1833
 
1800
1834
  function errorFileFromMessage(message: string) {
1801
- const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message);
1802
- return match?.[1];
1835
+ const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
1836
+ return match?.[1]
1803
1837
  }
1804
1838
 
1805
1839
  function resolveTraceImport(root: string, fromFile: string, specifier: string) {
1806
- if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier);
1807
- return resolveImport(root, fromFile, specifier);
1840
+ if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
1841
+ return resolveImport(root, fromFile, specifier)
1808
1842
  }
1809
1843
 
1810
1844
  function resolveTraceFile(file: string) {
1811
1845
  return canonicalTraceFile(
1812
1846
  path.isAbsolute(file) ? path.resolve(file) : path.resolve(process.cwd(), file),
1813
- );
1847
+ )
1814
1848
  }
1815
1849
 
1816
1850
  function resolveTraceCandidate(base: string) {
1817
- const ext = path.extname(base);
1851
+ const ext = path.extname(base)
1818
1852
  const candidates = ext
1819
1853
  ? [base]
1820
1854
  : [
1821
1855
  ...traceSourceExtensions.map(extension => `${base}${extension}`),
1822
1856
  ...traceSourceExtensions.map(extension => path.join(base, `index${extension}`)),
1823
1857
  base,
1824
- ];
1825
- const resolved = candidates.find(isTraceFile);
1826
- return resolved ? canonicalTraceFile(resolved) : undefined;
1858
+ ]
1859
+ const resolved = candidates.find(isTraceFile)
1860
+ return resolved ? canonicalTraceFile(resolved) : undefined
1827
1861
  }
1828
1862
 
1829
1863
  function isTraceFile(file: string) {
1830
1864
  try {
1831
- return statSync(file).isFile();
1865
+ return statSync(file).isFile()
1832
1866
  } catch {
1833
- return false;
1867
+ return false
1834
1868
  }
1835
1869
  }
1836
1870
 
1837
1871
  function readTextSyncSafe(file: string) {
1838
1872
  try {
1839
- return existsSync(file) ? readFileSync(file, 'utf8') : '';
1873
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
1840
1874
  } catch {
1841
- return '';
1875
+ return ''
1842
1876
  }
1843
1877
  }
1844
1878
 
1845
1879
  function tracePath(entryFile: string, targetFile: string, parents: Map<string, TraceParent>) {
1846
- const pathItems = [targetFile];
1847
- let current = targetFile;
1880
+ const pathItems = [targetFile]
1881
+ let current = targetFile
1848
1882
 
1849
1883
  while (current !== entryFile) {
1850
- const parent = parents.get(current);
1851
- if (!parent) return [];
1852
- current = parent.file;
1853
- pathItems.push(current);
1884
+ const parent = parents.get(current)
1885
+ if (!parent) return []
1886
+ current = parent.file
1887
+ pathItems.push(current)
1854
1888
  }
1855
1889
 
1856
- return pathItems.reverse();
1890
+ return pathItems.reverse()
1857
1891
  }
1858
1892
 
1859
1893
  function sameFile(a: string, b: string) {
1860
- return canonicalTraceFile(a) === canonicalTraceFile(b);
1894
+ return canonicalTraceFile(a) === canonicalTraceFile(b)
1861
1895
  }
1862
1896
 
1863
1897
  function displayTraceFile(config: ResolvedConfig, file: string) {
1864
- if (!path.isAbsolute(file)) return `${file} (generated client entry)`;
1865
- const root = canonicalTraceFile(config.root);
1866
- const workspaceRoot = canonicalTraceFile(config.workspaceRoot);
1867
- const resolvedFile = canonicalTraceFile(file);
1868
- const rootRelative = path.relative(root, resolvedFile);
1869
- if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative;
1870
- const workspaceRelative = path.relative(workspaceRoot, resolvedFile);
1898
+ if (!path.isAbsolute(file)) return `${file} (generated client entry)`
1899
+ const root = canonicalTraceFile(config.root)
1900
+ const workspaceRoot = canonicalTraceFile(config.workspaceRoot)
1901
+ const resolvedFile = canonicalTraceFile(file)
1902
+ const rootRelative = path.relative(root, resolvedFile)
1903
+ if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative
1904
+ const workspaceRelative = path.relative(workspaceRoot, resolvedFile)
1871
1905
  if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative))
1872
- return workspaceRelative;
1873
- return resolvedFile;
1906
+ return workspaceRelative
1907
+ return resolvedFile
1874
1908
  }
1875
1909
 
1876
1910
  function canonicalTraceFile(file: string) {
1877
1911
  try {
1878
- return realpathSync.native(file);
1912
+ return realpathSync.native(file)
1879
1913
  } catch {
1880
- return path.resolve(file);
1914
+ return path.resolve(file)
1881
1915
  }
1882
1916
  }
1883
1917
 
1884
1918
  /** One walked client source, with the stat that lets a later boot skip reading it. */
1885
1919
  export interface ClientCacheSource {
1886
- file: string;
1887
- mtimeMs: number;
1888
- size: number;
1889
- hash: string;
1920
+ file: string
1921
+ mtimeMs: number
1922
+ size: number
1923
+ hash: string
1890
1924
  }
1891
1925
 
1892
1926
  export interface ClientCacheKeyParts {
1893
- key: string;
1894
- staticHash: string;
1895
- sources: ClientCacheSource[];
1927
+ key: string
1928
+ staticHash: string
1929
+ sources: ClientCacheSource[]
1896
1930
  }
1897
1931
 
1898
1932
  /**
1899
1933
  * The half of the key that is not a walked source: the client build pipeline's own sources, the
1900
1934
  * generated entry, and the route's reference list. A handful of small reads, so it is cheap to
1901
1935
  * recompute on every lookup - which is what lets the expensive source walk be validated from a
1902
- * persisted index instead (see dev/client-key-cache.ts).
1936
+ * persisted index instead (see dev/restart/client-key.ts).
1903
1937
  */
1904
1938
  export async function clientCacheStaticHash(
1905
1939
  route: RouteManifestEntry,
@@ -1908,18 +1942,18 @@ export async function clientCacheStaticHash(
1908
1942
  ) {
1909
1943
  // The key must observe convention files added since the last build, so it
1910
1944
  // never reads a memo an earlier call left behind.
1911
- conventionCache.clear();
1912
- const hash = createHash('sha256');
1945
+ conventionCache.clear()
1946
+ const hash = createHash('sha256')
1913
1947
  for (const file of clientBuildPipelineFiles()) {
1914
- hash.update(file);
1915
- hash.update('\0');
1916
- hash.update(await readText(file));
1917
- hash.update('\0');
1948
+ hash.update(file)
1949
+ hash.update('\0')
1950
+ hash.update(await readText(file))
1951
+ hash.update('\0')
1918
1952
  }
1919
1953
 
1920
1954
  hash.update(
1921
1955
  clientEntrySource({
1922
- // Dev-only callers (client-key-cache): the key must move with the split
1956
+ // Dev-only callers (restart/client-key): the key must move with the split
1923
1957
  // flag or a toggled PNEXT_DYNAMIC_SPLIT would serve the other arm's entry.
1924
1958
  deferredDynamicHref: devDeferredDynamicHref(true),
1925
1959
  pageFile: route.client ? route.file : undefined,
@@ -1933,11 +1967,11 @@ export async function clientCacheStaticHash(
1933
1967
  clientRootLayout: config ? hasClientRootLayout(config, route) : false,
1934
1968
  shellLayoutOrder: config ? shellLayoutOrder(config, route) : [],
1935
1969
  }),
1936
- );
1937
- hash.update('\0');
1938
- hash.update(route.client ? route.file : '');
1939
- hash.update(JSON.stringify(route.clientReferences));
1940
- return hash.digest('hex');
1970
+ )
1971
+ hash.update('\0')
1972
+ hash.update(route.client ? route.file : '')
1973
+ hash.update(JSON.stringify(route.clientReferences))
1974
+ return hash.digest('hex')
1941
1975
  }
1942
1976
 
1943
1977
  /** The out-dir name: the static half plus every walked source's content hash. */
@@ -1945,18 +1979,18 @@ export function clientCacheKeyFrom(
1945
1979
  staticHash: string,
1946
1980
  sources: readonly { file: string; hash: string }[],
1947
1981
  ) {
1948
- const hash = createHash('sha256').update(staticHash).update('\0');
1982
+ const hash = createHash('sha256').update(staticHash).update('\0')
1949
1983
  for (const source of sources) {
1950
- hash.update(source.file);
1951
- hash.update('\0');
1952
- hash.update(source.hash);
1953
- hash.update('\0');
1984
+ hash.update(source.file)
1985
+ hash.update('\0')
1986
+ hash.update(source.hash)
1987
+ hash.update('\0')
1954
1988
  }
1955
- return hash.digest('hex').slice(0, 16);
1989
+ return hash.digest('hex').slice(0, 16)
1956
1990
  }
1957
1991
 
1958
1992
  export function clientSourceHash(source: string) {
1959
- return Bun.hash(source).toString(36);
1993
+ return Bun.hash(source).toString(36)
1960
1994
  }
1961
1995
 
1962
1996
  /**
@@ -1965,13 +1999,13 @@ export function clientSourceHash(source: string) {
1965
1999
  * hash that stat never described.
1966
2000
  */
1967
2001
  export async function readClientCacheSource(file: string): Promise<ClientCacheSource> {
1968
- const stats = await stat(file).catch(() => undefined);
2002
+ const stats = await stat(file).catch(() => undefined)
1969
2003
  return {
1970
2004
  file,
1971
2005
  mtimeMs: stats?.mtimeMs ?? 0,
1972
2006
  size: stats?.size ?? 0,
1973
2007
  hash: clientSourceHash(await readText(file)),
1974
- };
2008
+ }
1975
2009
  }
1976
2010
 
1977
2011
  export async function clientCacheKeyParts(
@@ -1980,11 +2014,11 @@ export async function clientCacheKeyParts(
1980
2014
  config?: ResolvedConfig,
1981
2015
  staticHash?: string,
1982
2016
  ): Promise<ClientCacheKeyParts> {
1983
- const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config));
2017
+ const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
1984
2018
  const sources = await Promise.all(
1985
2019
  (await clientSourceFiles(route)).map(file => readClientCacheSource(file)),
1986
- );
1987
- return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources };
2020
+ )
2021
+ return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
1988
2022
  }
1989
2023
 
1990
2024
  export async function clientCacheKey(
@@ -1992,7 +2026,7 @@ export async function clientCacheKey(
1992
2026
  nextCompat?: boolean,
1993
2027
  config?: ResolvedConfig,
1994
2028
  ) {
1995
- return (await clientCacheKeyParts(route, nextCompat, config)).key;
2029
+ return (await clientCacheKeyParts(route, nextCompat, config)).key
1996
2030
  }
1997
2031
 
1998
2032
  function clientBuildPipelineFiles() {
@@ -2004,61 +2038,61 @@ function clientBuildPipelineFiles() {
2004
2038
  path.join(import.meta.dirname, 'build.ts'),
2005
2039
  path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts'),
2006
2040
  path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts'),
2007
- ].filter(file => existsSync(file));
2041
+ ].filter(file => existsSync(file))
2008
2042
  }
2009
2043
 
2010
2044
  async function clientSourceFiles(route: RouteManifestEntry) {
2011
2045
  const entryFiles = [
2012
2046
  ...(route.client ? [route.file] : []),
2013
2047
  ...route.clientReferences.map(reference => reference.file),
2014
- ];
2015
- const files = new Set<string>();
2048
+ ]
2049
+ const files = new Set<string>()
2016
2050
 
2017
- await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)));
2051
+ await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
2018
2052
 
2019
- return [...files].sort();
2053
+ return [...files].sort()
2020
2054
  }
2021
2055
 
2022
2056
  // The walk fans out: every file joins `files` before its own read starts, so
2023
2057
  // concurrent branches still visit each file once. Serially this was the whole
2024
2058
  // cost of a route's client cache key (~1.8 s on a 300-module route).
2025
2059
  async function collectClientSourceFile(file: string, files: Set<string>) {
2026
- if (files.has(file) || !existsSync(file)) return;
2027
- files.add(file);
2028
- if (isAssetLike(file)) return;
2060
+ if (files.has(file) || !existsSync(file)) return
2061
+ files.add(file)
2062
+ if (isAssetLike(file)) return
2029
2063
 
2030
- const source = rewriteLiteralDynamicCalls(await readText(file), file);
2064
+ const source = rewriteLiteralDynamicCalls(await readText(file), file)
2031
2065
  await Promise.all(
2032
2066
  [...localImports(file, source)].map(specifier => {
2033
- const resolved = resolveImport(rootFromFile(file), file, specifier);
2034
- return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve();
2067
+ const resolved = resolveImport(rootFromFile(file), file, specifier)
2068
+ return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
2035
2069
  }),
2036
- );
2070
+ )
2037
2071
  }
2038
2072
 
2039
2073
  // Value imports, re-exports and literal `import()` of one module, off the
2040
2074
  // memoized parse (PERF-REWRITES #21) — type-only imports bind nothing and are
2041
2075
  // already excluded there.
2042
2076
  export function localImports(file: string, source: string) {
2043
- const facts = scanFacts(file, source);
2044
- const imports = new Set<string>();
2045
- for (const edge of facts.imports) imports.add(edge.specifier);
2046
- for (const edge of facts.dynamicImports) imports.add(edge.specifier);
2047
- return imports;
2077
+ const facts = scanFacts(file, source)
2078
+ const imports = new Set<string>()
2079
+ for (const edge of facts.imports) imports.add(edge.specifier)
2080
+ for (const edge of facts.dynamicImports) imports.add(edge.specifier)
2081
+ return imports
2048
2082
  }
2049
2083
 
2050
2084
  function isAssetLike(file: string) {
2051
- return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file);
2085
+ return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
2052
2086
  }
2053
2087
 
2054
2088
  function rootFromFile(file: string) {
2055
- const parts = file.split(path.sep);
2056
- const srcApp = parts.lastIndexOf('src');
2089
+ const parts = file.split(path.sep)
2090
+ const srcApp = parts.lastIndexOf('src')
2057
2091
  if (srcApp !== -1 && parts[srcApp + 1] === 'app')
2058
- return parts.slice(0, srcApp).join(path.sep) || path.sep;
2059
- const app = parts.lastIndexOf('app');
2060
- if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep;
2061
- return path.dirname(file);
2092
+ return parts.slice(0, srcApp).join(path.sep) || path.sep
2093
+ const app = parts.lastIndexOf('app')
2094
+ if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep
2095
+ return path.dirname(file)
2062
2096
  }
2063
2097
 
2064
2098
  // esbuild names every shared chunk "chunk-<hash>", which makes bundle reports
@@ -2068,96 +2102,96 @@ function rootFromFile(file: string) {
2068
2102
  async function nameSharedClientChunks(outDir: string, metafile?: Metafile) {
2069
2103
  // eslint-disable-next-line turbo/no-undeclared-env-vars
2070
2104
  if (metafile && process.env.PNEXT_CLIENT_METAFILE) {
2071
- await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile));
2105
+ await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
2072
2106
  }
2073
- const chunksDir = path.join(outDir, 'chunks');
2074
- const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'));
2075
- const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>();
2076
- const renames = new Map<string, string>();
2107
+ const chunksDir = path.join(outDir, 'chunks')
2108
+ const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'))
2109
+ const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>()
2110
+ const renames = new Map<string, string>()
2077
2111
 
2078
2112
  for (const file of files) {
2079
- const basename = path.basename(file);
2080
- if (!basename.startsWith('chunk-')) continue;
2081
- renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`);
2113
+ const basename = path.basename(file)
2114
+ if (!basename.startsWith('chunk-')) continue
2115
+ renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`)
2082
2116
  }
2083
2117
 
2084
- if (renames.size === 0) return renames;
2118
+ if (renames.size === 0) return renames
2085
2119
 
2086
2120
  await Promise.all(
2087
2121
  [...renames].map(async ([from, to]) => {
2088
- const fromPath = path.join(chunksDir, from);
2089
- const toPath = path.join(chunksDir, to);
2090
- if (from === to || !existsSync(fromPath)) return;
2122
+ const fromPath = path.join(chunksDir, from)
2123
+ const toPath = path.join(chunksDir, to)
2124
+ if (from === to || !existsSync(fromPath)) return
2091
2125
  try {
2092
- await rename(fromPath, toPath);
2126
+ await rename(fromPath, toPath)
2093
2127
  } catch (error) {
2094
2128
  // Tolerate a concurrent build having already moved this chunk (ENOENT);
2095
2129
  // the reference rewrite below still points at the renamed file.
2096
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
2130
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
2097
2131
  }
2098
2132
  // Move the external sourcemap alongside its chunk. The `sourceMappingURL`
2099
2133
  // comment in the renamed chunk is rewritten below (replaceChunkNames matches
2100
2134
  // the `chunk-<hash>.js` prefix inside `chunk-<hash>.js.map`), so the map file
2101
2135
  // must adopt the new name to keep the link resolvable.
2102
- const fromMap = `${fromPath}.map`;
2103
- if (!existsSync(fromMap)) return;
2136
+ const fromMap = `${fromPath}.map`
2137
+ if (!existsSync(fromMap)) return
2104
2138
  try {
2105
- await rename(fromMap, `${toPath}.map`);
2139
+ await rename(fromMap, `${toPath}.map`)
2106
2140
  } catch (error) {
2107
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
2141
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
2108
2142
  }
2109
2143
  }),
2110
- );
2144
+ )
2111
2145
 
2112
2146
  // One read+rewrite per output file. An app with a route per page has hundreds
2113
2147
  // of them, so they go out concurrently rather than one await at a time.
2114
- const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'));
2148
+ const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'))
2115
2149
  await Promise.all(
2116
2150
  outputs.map(async file => {
2117
- const source = await readText(file);
2118
- const next = replaceChunkNames(source, renames);
2119
- if (next !== source) await writeText(file, next);
2151
+ const source = await readText(file)
2152
+ const next = replaceChunkNames(source, renames)
2153
+ if (next !== source) await writeText(file, next)
2120
2154
  }),
2121
- );
2155
+ )
2122
2156
 
2123
- return renames;
2157
+ return renames
2124
2158
  }
2125
2159
 
2126
2160
  function hashSuffix(file: string) {
2127
- return path.basename(file, '.js').split('-').at(-1) ?? 'chunk';
2161
+ return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
2128
2162
  }
2129
2163
 
2130
2164
  function chunkLabelsFromMetafile(metafile: Metafile) {
2131
- const labels = new Map<string, string>();
2165
+ const labels = new Map<string, string>()
2132
2166
  for (const [outputPath, output] of Object.entries(metafile.outputs)) {
2133
- const basename = path.basename(outputPath);
2134
- if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue;
2135
- const weights = new Map<string, number>();
2167
+ const basename = path.basename(outputPath)
2168
+ if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue
2169
+ const weights = new Map<string, number>()
2136
2170
  for (const [input, { bytesInOutput }] of Object.entries(output.inputs)) {
2137
- const label = chunkInputLabel(input);
2138
- if (!label) continue;
2139
- weights.set(label, (weights.get(label) ?? 0) + bytesInOutput);
2171
+ const label = chunkInputLabel(input)
2172
+ if (!label) continue
2173
+ weights.set(label, (weights.get(label) ?? 0) + bytesInOutput)
2140
2174
  }
2141
- const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0];
2142
- if (top) labels.set(basename, top[0]);
2175
+ const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
2176
+ if (top) labels.set(basename, top[0])
2143
2177
  }
2144
- return labels;
2178
+ return labels
2145
2179
  }
2146
2180
 
2147
2181
  function chunkInputLabel(input: string) {
2148
- const posix = input.split(path.sep).join('/');
2149
- const packageIndex = posix.lastIndexOf('node_modules/');
2182
+ const posix = input.split(path.sep).join('/')
2183
+ const packageIndex = posix.lastIndexOf('node_modules/')
2150
2184
  if (packageIndex !== -1) {
2151
- const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/');
2152
- const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0];
2153
- return sanitizeChunkLabel(name ?? '');
2185
+ const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/')
2186
+ const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0]
2187
+ return sanitizeChunkLabel(name ?? '')
2154
2188
  }
2155
2189
  return sanitizeChunkLabel(
2156
2190
  posix
2157
2191
  .split('/')
2158
2192
  .at(-1)
2159
2193
  ?.replace(/\.[^.]+$/, '') ?? '',
2160
- );
2194
+ )
2161
2195
  }
2162
2196
 
2163
2197
  function sanitizeChunkLabel(label: string) {
@@ -2165,11 +2199,11 @@ function sanitizeChunkLabel(label: string) {
2165
2199
  .replace(/^@/, '')
2166
2200
  .replace(/[^a-zA-Z0-9._-]+/g, '-')
2167
2201
  .replace(/^[-.]+|[-.]+$/g, '')
2168
- .slice(0, 40);
2169
- return clean || undefined;
2202
+ .slice(0, 40)
2203
+ return clean || undefined
2170
2204
  }
2171
2205
 
2172
2206
  function replaceChunkNames(source: string, renames: Map<string, string>) {
2173
- const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g');
2174
- return source.replace(pattern, match => renames.get(match) ?? match);
2207
+ const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g')
2208
+ return source.replace(pattern, match => renames.get(match) ?? match)
2175
2209
  }