@wular/pnext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1710 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile, readdir, rm, stat, watch } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {
6
+ DEFERRED_DYNAMIC_SIDECAR,
7
+ buildClientDynamicChunk,
8
+ buildClientEntry,
9
+ deferredDynamicRefById,
10
+ prebuiltRuntimeDir,
11
+ registerDeferredDynamicRef,
12
+ } from '../client/build';
13
+ import { drainPreplanBuilds } from '../runtime/vendor';
14
+ import { devClientCacheKey } from './client-key-cache';
15
+ import { clientChunkStoreDir, contentAddressDir, sweepClientChunkStore } from './client-chunk-store';
16
+ import { restartCacheEnabled } from './restart-cache';
17
+ import { installRouteFactsCache } from './route-facts-cache';
18
+ import { installGlobalCssCache } from './global-css-cache';
19
+ import { clientEntryName } from '../client/paths';
20
+ import { bootstrapCompat, bootstrapCompatBoot } from '../compat-bootstrap';
21
+ import type { ResolvedConfig } from '../config';
22
+ import {
23
+ buildClientReferenceCss,
24
+ buildGlobalCss,
25
+ buildRouteCss,
26
+ clearGlobalCssSourceCache,
27
+ isCssFile,
28
+ warmCssPipeline,
29
+ } from '../css';
30
+ import {
31
+ deferServerRuntimePlugins,
32
+ installServerRuntimePlugins,
33
+ registerServerRuntime,
34
+ serverBundleTargetForRuntime,
35
+ } from '../runtime/server';
36
+ import { applyProxyResponse, createProxyRunner, validateProxyFiles } from '../proxy';
37
+ import { renderGlobalNotFoundResponse, renderPageResponse } from '../render';
38
+ import { getFontExtensions } from '../render/hooks';
39
+ import { clearMetadataFileCaches, metadataRouteHandlerModule } from '../routing/metadata';
40
+ import { handleRouteModule, type RouteHandlerModule } from '../routing/handler';
41
+ import { malformedUrlResponse, trailingSlashRedirect } from '../routing/href';
42
+ import { withForwardedHeaders } from '../routing/forwarded';
43
+ import {
44
+ findLayouts,
45
+ matchRoute,
46
+ materializeRouteFactsPaced,
47
+ parseNavState,
48
+ routeFactsResolved,
49
+ routeFactsVersion,
50
+ scanRoutes,
51
+ selectRouteForRequest,
52
+ } from '../routing/routes';
53
+ import { writeTypegen } from '../typegen';
54
+ import { runWithCacheScope } from '../cache/context';
55
+ import {
56
+ finalizeResponse,
57
+ getAssetExtensions,
58
+ getBundlerExtensions,
59
+ getRequestExtensions,
60
+ getRouterProtocolExtensions,
61
+ reportRequestError,
62
+ runInitHooks,
63
+ runRequestWarmHooks,
64
+ withRouteRuntime,
65
+ } from '../extensions';
66
+ import {
67
+ flushWorkUnit,
68
+ flushWorkUnitOnClose,
69
+ getWorkUnit,
70
+ runWithWorkUnit,
71
+ setPhase,
72
+ setWorkUnitRoute,
73
+ } from '../request/context';
74
+ import { setRequestRuntime } from '../routing/request-runtime';
75
+ import { ensureDir, listFiles, writeText } from '../utils/fs';
76
+ import { contentType } from '../utils/content-type';
77
+ import {
78
+ flushDevProfileLines,
79
+ formatProfileDuration,
80
+ recordDevProfileLine,
81
+ } from '../utils/dev-profile';
82
+ import { clearResolverFsCache } from '../resolve/engine';
83
+ import { clearAppTreeResolutions, workspacePackageRoots } from '../resolve/imports';
84
+ import type { RouteManifestEntry, RouteParamValue } from '../types';
85
+ import {
86
+ devClientModuleHref,
87
+ devModuleGraph,
88
+ devRouteModuleLoaders,
89
+ devServerModuleHref,
90
+ warmDevModulePipeline,
91
+ } from './imports';
92
+ import { ssrClientReference } from '../client/reference';
93
+ import { cacheRoot } from './module-cache';
94
+ import { markBoot } from '../cli/boot-trace';
95
+
96
+ interface DevServerOptions {
97
+ config: ResolvedConfig;
98
+ port: number;
99
+ hostname: string;
100
+ /**
101
+ * Compile the entry route in the background as soon as the server is up, so the
102
+ * expensive cold build (esbuild cold start + Tailwind subprocess) overlaps with
103
+ * the browser launching instead of blocking the first click. The build caches
104
+ * dedup, so if the real request arrives mid-warm it awaits the same promise.
105
+ */
106
+ warm?: boolean;
107
+ }
108
+
109
+ const clientBuilds = new Map<string, Promise<string>>();
110
+ const clientChunks = new Map<string, string>();
111
+
112
+ // esbuild.stop() (the memory watchdog) can leave an in-flight build's promise permanently unsettled;
113
+ // anything awaiting it hangs and the dedup map keeps re-serving the poisoned promise. Waiters race against
114
+ // this registry so a service restart fails them fast instead - failed builds drop from the caches and the
115
+ // next request retries against the respawned service.
116
+ const clientBuildStopWaiters = new Set<(err: Error) => void>();
117
+ export function failInFlightClientBuilds(reason: string) {
118
+ const err = new Error(reason);
119
+ for (const reject of clientBuildStopWaiters) reject(err);
120
+ clientBuildStopWaiters.clear();
121
+ clientBuilds.clear();
122
+ }
123
+ // Client out-dirs whose chunk list is already in clientChunks — the dir name is
124
+ // a content key, so it never needs a second readdir.
125
+ const indexedClientDirs = new Set<string>();
126
+ // Layout chains per route file: findLayouts + an existsSync per level, which a
127
+ // warm request would otherwise redo every time.
128
+ const routeLayoutFiles = new Map<string, string[]>();
129
+ const assetBuilds = new Map<string, Promise<unknown>>();
130
+ const routeCacheKeys = new Map<string, Promise<string>>();
131
+ // Routes whose bundles this process has already compiled, so the "Compiling"
132
+ // banner prints once per cold route (like Next.js) and not on warm hits.
133
+ // Cleared on reload.
134
+ const compiledRoutes = new Set<string>();
135
+
136
+ interface DevEventClient {
137
+ controller: ReadableStreamDefaultController<Uint8Array>;
138
+ }
139
+
140
+ /**
141
+ * A save invalidates the changed files and their dependents - nothing else. Compiled modules are
142
+ * content-addressed, so every map keyed by an artifact path invalidates itself; only the memo caches keyed
143
+ * by route or source path need telling.
144
+ */
145
+ function invalidateDevCaches(config: ResolvedConfig, changed: string[], structural: boolean) {
146
+ devModuleGraph(config).invalidate(changed);
147
+ // oxc caches fs lookups (including misses) until cleared, so a file added or
148
+ // deleted since the last resolve stays invisible without this. A plain edit
149
+ // changes no lookup answer.
150
+ if (structural) {
151
+ clearResolverFsCache();
152
+ clearAppTreeResolutions();
153
+ clearMetadataFileCaches();
154
+ routeLayoutFiles.clear();
155
+ }
156
+ // Route CSS is derived from the whole source tree (Tailwind scans it), and
157
+ // the client cache key is a content hash whose inputs just moved.
158
+ clearGlobalCssSourceCache();
159
+ assetBuilds.clear();
160
+ routeCacheKeys.clear();
161
+ indexedClientDirs.clear();
162
+ compiledRoutes.clear();
163
+ // Long sessions accumulate one generation per save; sweep past the keep
164
+ // window here too, not just at boot. Best-effort and off the reload path.
165
+ void evictStaleClientCaches(config.outPath);
166
+ }
167
+
168
+ // Every edit writes a fresh content-keyed generation under cache/client/ and
169
+ // nothing removed the old ones — ~6.6 MB per save, 1.24 GB after a 200-edit
170
+ // session (DEV-MEMORY §disk). Keep the newest N generations; an evicted dir is
171
+ // just a cache miss. Runs off the boot path.
172
+ async function evictStaleClientCaches(outPath: string) {
173
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
174
+ const keep = Number(process.env.PNEXT_DEV_CLIENT_CACHE_KEEP || 32);
175
+ if (!Number.isFinite(keep) || keep <= 0) return;
176
+ const clientRoot = path.join(outPath, 'cache', 'client');
177
+ try {
178
+ const entries = await readdir(clientRoot);
179
+ if (entries.length <= keep) return;
180
+ const dated = await Promise.all(
181
+ entries.map(async entry => {
182
+ const full = path.join(clientRoot, entry);
183
+ const info = await stat(full).catch(() => null);
184
+ return { full, mtime: info?.mtimeMs ?? 0 };
185
+ }),
186
+ );
187
+ dated.sort((a, b) => b.mtime - a.mtime);
188
+ await Promise.allSettled(
189
+ dated.slice(keep).map(entry => rm(entry.full, { recursive: true, force: true })),
190
+ );
191
+ // Evicted generations may have been the last link to a store entry; sweep
192
+ // after removal so orphaned store files (nlink back down to 1) don't linger.
193
+ await sweepClientChunkStore(clientChunkStoreDir(outPath));
194
+ } catch {
195
+ // ignore: eviction is best-effort; worst case is disk growth, not breakage
196
+ }
197
+ }
198
+
199
+ // Left behind by pre-persistence versions of the dev server, which swept the
200
+ // cache into a `.cache-stale-*` dir on every boot. Nothing creates them now.
201
+ async function removeLeakedStaleCaches(outPath: string) {
202
+ try {
203
+ const entries = await readdir(outPath);
204
+ await Promise.allSettled(
205
+ entries
206
+ .filter(entry => entry.startsWith('.cache-stale-'))
207
+ .map(entry => rm(path.join(outPath, entry), { recursive: true, force: true })),
208
+ );
209
+ } catch {
210
+ // ignore: leftover stale dirs are only a disk-space nuisance
211
+ }
212
+ }
213
+
214
+ export async function startDevServer(options: DevServerOptions) {
215
+ const { config, port, hostname } = options;
216
+ // Spawn the CSS worker first: Tailwind's cold boot then runs off the event
217
+ // loop, alongside everything below, instead of on the first page's stylesheet.
218
+ warmCssPipeline(config, { dev: true });
219
+ markBoot('boot:css-warm');
220
+ // Compat plugin loader: the single gated seam that populates the core
221
+ // extension registries when compat is enabled (no-op for pure-core apps).
222
+ // Boot takes the cheap tier only — route conventions, aliases, proxy names,
223
+ // lifecycle hooks; the implementation graph loads on the first request.
224
+ await bootstrapCompatBoot(config);
225
+ markBoot('boot:compat');
226
+ // The compat implementation graph still has to load (first request / warmup).
227
+ // Registering the module plugins now would put that whole import through the
228
+ // plugin pipeline; arm them once compat is in, like build and start do.
229
+ deferServerRuntimePlugins();
230
+ registerServerRuntime(config);
231
+ // Compat installs the Next fetch-cache patch here (no-op for pure-core apps).
232
+ runInitHooks(config);
233
+ // No boot sweep: the compiled cache is content-addressed and persists across
234
+ // restarts, so validity is a name miss plus the
235
+ // cache marker, not a wipe.
236
+ void removeLeakedStaleCaches(config.outPath);
237
+ void evictStaleClientCaches(config.outPath);
238
+ // Route facts are the biggest single thing a restart's first page used to re-derive. Installed
239
+ // before the route table so the first touch - whoever forces it - reads the previous process's
240
+ // walk instead of repeating it.
241
+ installRouteFactsCache(config);
242
+ // Same deal for the root layout's CSS graph: 320 files read to find one
243
+ // stylesheet, and both the warm tier and the render force it at Ready+0.
244
+ installGlobalCssCache(config);
245
+ markBoot('boot:registry');
246
+ let routes = await scanRoutes(config.appPath);
247
+ markBoot('boot:scanRoutes');
248
+ await validateProxyFiles(config);
249
+ let proxyRunner = createProxyRunner(config);
250
+ markBoot('boot:proxy');
251
+ let devImportVersion = String(Date.now());
252
+ // The proxy runs serially in front of every request, so its compile+import is
253
+ // pure critical path when the first request pays it. Started here it overlaps
254
+ // the CSS worker warmup, typegen and the first route's own module pass.
255
+ if (restartCacheEnabled()) proxyRunner.warm({ dev: true, devImportVersion });
256
+ markBoot('boot:proxy-warm');
257
+ // Publish the live routing state the compat request interceptors (action
258
+ // dispatch, rewrites) + client-action discovery read. Compat re-runs action
259
+ // discovery lazily whenever the dev import version changes (see below), so the
260
+ // registry + client-stub set that startup/reload armed inline stay current.
261
+ publishRuntime();
262
+ const clients = new Set<DevEventClient>();
263
+ let watcher: DevWatcher | undefined;
264
+ /** Requests currently being served — background work defers to them. */
265
+ let inFlight = 0;
266
+
267
+ function publishRuntime() {
268
+ setRequestRuntime({ config, routes, dev: true, devImportVersion });
269
+ }
270
+
271
+ // Typegen's route walk is the longest CPU run dev does outside a compile, and the only one with no
272
+ // deadline, so it yields between routes and waits out anything the server is actually serving.
273
+ // Bounded: a dev server under continuous load would otherwise never emit the .d.ts at all, so a
274
+ // route waits out requests for at most this long and then takes its turn regardless.
275
+ // `PNEXT_DEV_TYPEGEN_PACE=0` restores the uninterrupted walk.
276
+ const TYPEGEN_MAX_WAIT_MS = 3_000;
277
+
278
+ async function typegenPause() {
279
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
280
+ if (process.env.PNEXT_DEV_TYPEGEN_PACE === '0') return;
281
+ const deadline = performance.now() + TYPEGEN_MAX_WAIT_MS;
282
+ do {
283
+ await new Promise(resolve => setTimeout(resolve, inFlight > 0 ? 25 : 0));
284
+ } while (inFlight > 0 && performance.now() < deadline);
285
+ }
286
+
287
+ async function typegenInBackground() {
288
+ const generation = routes;
289
+ try {
290
+ await writeTypegen(config, await materializeRouteFactsPaced(generation, typegenPause));
291
+ } catch (error) {
292
+ if (generation === routes) logDevPreloadError('typegen', error);
293
+ }
294
+ if (generation === routes) syncWatchRoots();
295
+ }
296
+
297
+ async function reload(change: DevChange) {
298
+ // Editors save atomically (write + rename), so a rename event alone does not
299
+ // mean the tree moved: it only did if a file is gone, or is one the module
300
+ // graph has never seen.
301
+ const structural =
302
+ change.renamed &&
303
+ change.files.some(file => !existsSync(file) || !devModuleGraph(config).knows(file));
304
+ invalidateDevCaches(config, change.files, structural);
305
+ // A burst that only touched plain stylesheets cannot have moved the module graph, the route
306
+ // table, the proxy or the client bundles - only the built CSS assets. The page swaps its <link>s
307
+ // in place instead of navigating.
308
+ if (isCssOnlyChange(change.files)) {
309
+ broadcast(clients, 'css-update');
310
+ return;
311
+ }
312
+ // The route table is built from file paths only, so only an added, removed
313
+ // or renamed file can change it — a plain save never does.
314
+ if (structural) routes = await scanRoutes(config.appPath);
315
+ await validateProxyFiles(config);
316
+ proxyRunner = createProxyRunner(config);
317
+ devImportVersion = `${Date.now()}`;
318
+ proxyRunner.warm({ dev: true, devImportVersion });
319
+ // Republish with the bumped version so compat re-discovers actions before
320
+ // the next action request / client build.
321
+ publishRuntime();
322
+ void typegenInBackground();
323
+ watchedFactsVersion = -1;
324
+ syncWatchRoots();
325
+ broadcast(clients, 'reload');
326
+ }
327
+
328
+ // Watch roots outside app/ come from route sourceFiles, which only exist once
329
+ // a route has resolved its deferred facts — so they are re-derived whenever
330
+ // another route materializes (its first compile), not once at boot.
331
+ let watchedFactsVersion = -1;
332
+ function syncWatchRoots() {
333
+ if (watchedFactsVersion === routeFactsVersion()) return;
334
+ watchedFactsVersion = routeFactsVersion();
335
+ watcher = refreshWatcher(config, routes, watcher, reload);
336
+ }
337
+
338
+ syncWatchRoots();
339
+ markBoot('boot:watcher');
340
+
341
+ const server = Bun.serve({
342
+ hostname,
343
+ port,
344
+ idleTimeout: 60,
345
+ fetch(request, server) {
346
+ // One work unit spans the whole request; its after-queue flushes once the
347
+ // response fully closes (stream end, redirect, notFound, error, abort).
348
+ inFlight++;
349
+ return runWithWorkUnit('render', () => handleDevRequest(request, server)).finally(() => {
350
+ inFlight--;
351
+ });
352
+ },
353
+ });
354
+
355
+ async function handleDevRequest(
356
+ request: Request,
357
+ server: { timeout(request: Request, seconds: number): void },
358
+ ): Promise<Response> {
359
+ const prologueStart = performance.now();
360
+ // The compat implementation graph loads here, on the first request, not at
361
+ // boot — everything a request touches (interceptors, render, compile) is
362
+ // registered by the time this resolves.
363
+ await bootstrapCompat(config, { serve: true });
364
+ installServerRuntimePlugins();
365
+ const unit = getWorkUnit();
366
+ const badRequest = malformedUrlResponse(request);
367
+ if (badRequest) return badRequest;
368
+ request = withForwardedHeaders(request);
369
+ let url = new URL(request.url);
370
+ const profile = devRequestProfile(request, url);
371
+ if (profile) logDevProfile(profile, 'prologue (bootstrap/headers/url)', prologueStart);
372
+ let pageLog = pendingDevPageLoadLog(routes, request, url);
373
+ // Work this request wants started, but only once its own response is out.
374
+ let afterResponse: (() => void) | undefined;
375
+ const logAbort = () => {
376
+ const pending = pageLog;
377
+ pageLog = undefined;
378
+ logDevPageAbort(pending);
379
+ };
380
+ request.signal.addEventListener('abort', logAbort, { once: true });
381
+ const finish = async (response: Response): Promise<Response> => {
382
+ const pending = pageLog;
383
+ pageLog = undefined;
384
+ request.signal.removeEventListener('abort', logAbort);
385
+ const finalized = await profileDevStep(profile, 'finalize response', () =>
386
+ finalizeResponse(
387
+ response,
388
+ { method: request.method, url: new URL(request.url), headers: request.headers },
389
+ { routeKind: unit?.routeKind ?? 'html', routeMode: unit?.routeMode },
390
+ ),
391
+ );
392
+ const logged = logDevPageResponse(pending, finalized);
393
+ // A timer, not a bare call: the response is only handed to Bun when this returns, so anything
394
+ // started synchronously here still races it. That is also why the watch-root sync moved in -
395
+ // a request that compiled a route just materialized its source graph, and re-deriving the
396
+ // roots from it is a synchronous walk the document does not need.
397
+ const tasks = afterResponse;
398
+ afterResponse = undefined;
399
+ setTimeout(() => {
400
+ tasks?.();
401
+ syncWatchRoots();
402
+ }, 0);
403
+ if (profile) flushDevProfileLines();
404
+ return flushWorkUnitOnClose(logged, unit, request.signal);
405
+ };
406
+
407
+ try {
408
+ const initialPrefetchResponse = maybeDevPagePrefetchResponse(routes, url.pathname, request);
409
+ if (initialPrefetchResponse) return finish(initialPrefetchResponse);
410
+
411
+ let proxyResponse: Response | undefined;
412
+ const proxyResult = await profileDevStep(profile, 'proxy', () =>
413
+ proxyRunner(request, { dev: true, devImportVersion }),
414
+ );
415
+ if (proxyResult instanceof Response) return finish(proxyResult);
416
+ if (proxyResult) {
417
+ request = proxyResult.request;
418
+ proxyResponse = proxyResult.response;
419
+ url = new URL(request.url);
420
+ pageLog = pendingDevPageLoadLog(routes, request, url, pageLog?.start);
421
+
422
+ const rewrittenPrefetchResponse = maybeDevPagePrefetchResponse(
423
+ routes,
424
+ url.pathname,
425
+ request,
426
+ );
427
+ if (rewrittenPrefetchResponse)
428
+ return finish(applyProxyResponse(rewrittenPrefetchResponse, proxyResponse));
429
+ }
430
+
431
+ if (url.pathname === '/__pnext/events') {
432
+ server.timeout(request, 0);
433
+ return finish(eventStream(clients));
434
+ }
435
+
436
+ const assetResponse = await profileDevStep(profile, 'built asset lookup', () =>
437
+ maybeBuiltAsset(config, routes, url.pathname),
438
+ );
439
+ if (assetResponse) return finish(applyProxyResponse(assetResponse, proxyResponse));
440
+
441
+ const clientChunkResponse = await profileDevStep(profile, 'client chunk lookup', () =>
442
+ maybeDevClientChunk(config, routes, url.pathname),
443
+ );
444
+ if (clientChunkResponse)
445
+ return finish(applyProxyResponse(clientChunkResponse, proxyResponse));
446
+
447
+ const runtimeResponse = maybePrebuiltRuntime(config, url.pathname);
448
+ if (runtimeResponse) return finish(applyProxyResponse(runtimeResponse, proxyResponse));
449
+
450
+ const dynChunkMatch = /^\/__pnext\/client-dyn\/([A-Za-z0-9-]+)\.js$/.exec(url.pathname);
451
+ if (dynChunkMatch?.[1]) {
452
+ const found = findDeferredDynamicReference(routes, dynChunkMatch[1]);
453
+ if (!found)
454
+ return finish(
455
+ applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
456
+ );
457
+ const file = await profileDevStep(profile, `client dyn chunk ${dynChunkMatch[1]}`, () =>
458
+ buildDevDynamicChunk(config, found.route, found.reference, devImportVersion),
459
+ );
460
+ return finish(
461
+ applyProxyResponse(
462
+ devResponse(await readFile(file), 'text/javascript; charset=utf-8'),
463
+ proxyResponse,
464
+ ),
465
+ );
466
+ }
467
+ const dynSharedChunk = /^\/__pnext\/client-dyn\/chunks\/(.+\.js)$/.exec(url.pathname);
468
+ if (dynSharedChunk?.[1]) {
469
+ const chunkFile = dynChunkFiles.get(dynSharedChunk[1]);
470
+ if (chunkFile && existsSync(chunkFile))
471
+ return finish(
472
+ applyProxyResponse(devChunkResponse(await readFile(chunkFile)), proxyResponse),
473
+ );
474
+ return finish(
475
+ applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
476
+ );
477
+ }
478
+
479
+ const clientMatch = /^\/__pnext\/client\/(.+)\.js$/.exec(url.pathname);
480
+ if (clientMatch?.[1]) {
481
+ const route = routes.find(
482
+ item =>
483
+ item.id === clientMatch[1] &&
484
+ (item.client || item.clientReferences.length > 0 || item.needsRouterEntry),
485
+ );
486
+ if (!route)
487
+ return finish(
488
+ applyProxyResponse(new Response('not found', { status: 404 }), proxyResponse),
489
+ );
490
+ const file = await profileDevStep(profile, `client build ${route.route}`, () =>
491
+ buildDevClient(config, route),
492
+ );
493
+ return finish(
494
+ applyProxyResponse(
495
+ devResponse(
496
+ await profileDevStep(profile, 'client read', () => readFile(file)),
497
+ 'text/javascript; charset=utf-8',
498
+ ),
499
+ proxyResponse,
500
+ ),
501
+ );
502
+ }
503
+
504
+ const staticResponse = await profileDevStep(profile, 'static lookup', () =>
505
+ maybeStaticFile(config.publicPath, url.pathname),
506
+ );
507
+ if (staticResponse) {
508
+ setWorkUnitRoute('static-asset', 'static');
509
+ return finish(applyProxyResponse(staticResponse, proxyResponse));
510
+ }
511
+ // Emitted assets land in the build output's public dir, not the project public dir.
512
+ if (
513
+ getAssetExtensions()
514
+ .staticAssetPublicPrefixes()
515
+ .some(prefix => url.pathname.startsWith(prefix))
516
+ ) {
517
+ const emitted = await maybeStaticFile(
518
+ path.join(config.outPath, 'public'),
519
+ url.pathname,
520
+ );
521
+ if (emitted) {
522
+ setWorkUnitRoute('static-asset', 'static');
523
+ return finish(applyProxyResponse(emitted, proxyResponse));
524
+ }
525
+ }
526
+ const staticAssetPathname = getNextStaticAssetPathname(
527
+ url.pathname,
528
+ config.basePath,
529
+ config.assetPrefix,
530
+ );
531
+ if (staticAssetPathname) {
532
+ const outPublicStatic = await maybeStaticFile(
533
+ path.join(config.outPath, 'public'),
534
+ staticAssetPathname,
535
+ );
536
+ if (outPublicStatic) {
537
+ setWorkUnitRoute('static-asset', 'static');
538
+ return finish(applyProxyResponse(outPublicStatic, proxyResponse));
539
+ }
540
+
541
+ // Standalone chunks a bundler extension supplies (compat: the
542
+ // no-module polyfills chunk). The prod build writes them to disk; dev
543
+ // has no client out-dir for them, so serve the contents directly.
544
+ const staticChunk = maybeStaticClientChunk(config, staticAssetPathname);
545
+ if (staticChunk) {
546
+ setWorkUnitRoute('static-asset', 'static');
547
+ return finish(applyProxyResponse(staticChunk, proxyResponse));
548
+ }
549
+
550
+ setWorkUnitRoute('static-asset', 'static');
551
+ return finish(
552
+ applyProxyResponse(
553
+ new Response('Not Found', {
554
+ status: 404,
555
+ headers: { 'content-type': 'text/plain' },
556
+ }),
557
+ proxyResponse,
558
+ ),
559
+ );
560
+ }
561
+
562
+ // The compat request interceptors run before route matching (registration order: action dispatch -
563
+ // POSTs to a page URL with the action id - then next.config rewrites). A Response short-circuits
564
+ // (wrapped with the proxy response); a `{ request }` swaps the request (a rewrite) and continues.
565
+ // The render keeps the ORIGINAL request URL as canonical; only matching and lookup follow the
566
+ // rewritten url. Pure-core apps register no interceptors.
567
+ const canonicalRequest = request;
568
+ const canonicalUrl = new URL(request.url);
569
+ for (const [index, interceptor] of getRequestExtensions().interceptors.entries()) {
570
+ const result = await profileDevStep(
571
+ profile,
572
+ `interceptor ${interceptor.name || index}`,
573
+ () => interceptor(request, { config }),
574
+ );
575
+ if (result instanceof Response) return finish(applyProxyResponse(result, proxyResponse));
576
+ if (result) {
577
+ request = result.request;
578
+ url = new URL(request.url);
579
+ }
580
+ }
581
+
582
+ const canonicalRedirect = trailingSlashRedirect(config, url, request.method.toUpperCase());
583
+ if (canonicalRedirect) return finish(applyProxyResponse(canonicalRedirect, proxyResponse));
584
+
585
+ const nav = parseNavState(request);
586
+ const matched = profileDevSyncStep(profile, 'match route', () =>
587
+ selectRouteForRequest(routes, url.pathname, nav),
588
+ );
589
+
590
+ if (!matched) {
591
+ return finish(
592
+ applyProxyResponse(
593
+ await renderGlobalNotFoundResponse({
594
+ config,
595
+ url,
596
+ request: canonicalRequest,
597
+ dev: true,
598
+ devImportVersion,
599
+ }),
600
+ proxyResponse,
601
+ ),
602
+ );
603
+ }
604
+
605
+ if (matched.route.kind === 'handler') {
606
+ setWorkUnitRoute('route-handler');
607
+ setPhase('handler');
608
+ return finish(
609
+ applyProxyResponse(
610
+ await profileDevStep(profile, `handler ${matched.route.route}`, () =>
611
+ handleRoute(
612
+ config,
613
+ matched.route,
614
+ canonicalRequest,
615
+ matched.params,
616
+ devImportVersion,
617
+ ),
618
+ ),
619
+ proxyResponse,
620
+ ),
621
+ );
622
+ }
623
+
624
+ setWorkUnitRoute('html', matched.route.mode === 'static' ? 'static' : 'dynamic');
625
+ pageRequestsSeen++;
626
+ noteDevCompileStart(matched.route);
627
+ noteDevRouteServed(config, matched.route);
628
+ // Stylesheets are a SEPARATE browser fetch - the HTML never awaits them - so building them here only
629
+ // takes cores off the import the response is blocked on. Handed to `finish` instead: the browser
630
+ // still has to receive and parse the document before it asks, and by then the build is running (or,
631
+ // on a warm hit, already memoized). The hydration chunk is a separate fetch on the same terms.
632
+ const eagerClient = !deferClientStage();
633
+ if (eagerClient) preloadDevClient(config, matched.route, profile);
634
+ const preloadAssets = () => {
635
+ preloadDevPageAssets(config, matched.route, profile);
636
+ if (!eagerClient) preloadDevClient(config, matched.route, profile);
637
+ };
638
+ afterResponse = preloadAssets;
639
+ if (!deferAssetPreload()) {
640
+ afterResponse = undefined;
641
+ preloadAssets();
642
+ }
643
+ const layoutFiles = profileDevSyncStep(profile, 'find layouts', () =>
644
+ devLayoutFiles(config, matched.route.file),
645
+ );
646
+ let loaders: Awaited<ReturnType<typeof devRouteModuleLoaders>> | undefined;
647
+ preloadDevClientReferences(config, matched.route, profile);
648
+ try {
649
+ loaders = await profileDevStep(
650
+ profile,
651
+ `route module loaders ${matched.route.route}`,
652
+ () => devRouteModuleLoaders(config, matched.route, layoutFiles),
653
+ );
654
+ } catch (error) {
655
+ logDevRouteLoaderFallback(matched.route, error);
656
+ }
657
+ // Second chance for the font stage: on most apps the fonts are declared
658
+ // by the root LAYOUT, which the route bundle brings in — so this is
659
+ // where they first exist. Memoized, so the earlier call is not repeated.
660
+ void preloadDevFonts(config, profile);
661
+ return finish(
662
+ applyProxyResponse(
663
+ await profileDevStep(profile, `render page ${matched.route.route}`, () =>
664
+ withRouteRuntime(matched.route.segmentConfig?.runtime, () =>
665
+ renderPageResponse({
666
+ config,
667
+ route: matched.route,
668
+ params: matched.params,
669
+ // The render keeps the ORIGINAL requested URL as canonical
670
+ // (usePathname/useSearchParams); a rewrite only steered matching.
671
+ url: canonicalUrl,
672
+ // Non-action POSTs to a page render the page (Next's MPA
673
+ // fallback for plain form posts / followed 307s), not a 405.
674
+ request:
675
+ request.method.toUpperCase() === 'POST'
676
+ ? new Request(canonicalRequest.url, { headers: canonicalRequest.headers })
677
+ : canonicalRequest,
678
+ dev: true,
679
+ devImportVersion,
680
+ layoutFiles,
681
+ moduleLoader: loaders?.moduleLoader,
682
+ clientModuleLoader: loaders?.clientModuleLoader,
683
+ ...(nav
684
+ ? {
685
+ nav: {
686
+ soft: true,
687
+ state: nav,
688
+ childrenPath: matched.childrenPath,
689
+ targetPath: matched.targetPath,
690
+ },
691
+ }
692
+ : {}),
693
+ }),
694
+ ),
695
+ ),
696
+ proxyResponse,
697
+ ),
698
+ );
699
+ } catch (error) {
700
+ request.signal.removeEventListener('abort', logAbort);
701
+ // The error funnel (compat classifies + reports) fires from this single
702
+ // catch, then dev rethrows so Bun surfaces its own overlay/500.
703
+ await reportRequestError(
704
+ error,
705
+ { method: request.method, url: request.url, headers: request.headers },
706
+ { phase: unit?.phase, routeKind: unit?.routeKind },
707
+ );
708
+ logDevPageError(pageLog);
709
+ flushWorkUnit(unit);
710
+ throw error;
711
+ }
712
+ }
713
+
714
+ const warmup = options.warm
715
+ ? afterReady(() => warmDev(config, routes)).catch(error => logDevPreloadError('warm', error))
716
+ : undefined;
717
+ // Typegen is the one consumer that needs EVERY route's content facts (mode,
718
+ // hydrated), so it runs last and off the critical path — after the entry
719
+ // route's warm compile. The .d.ts is for the editor, not for serving.
720
+ void Promise.resolve(warmup).then(typegenInBackground);
721
+
722
+ return Object.assign(server, { warmup });
723
+ }
724
+
725
+ /**
726
+ * The Ready banner is printed by the caller in the microtask that resolves
727
+ * `startDevServer`, so a timer callback is the earliest point that provably
728
+ * runs after it: warmup can never push the banner out.
729
+ */
730
+ function afterReady<T>(task: () => Promise<T>) {
731
+ return new Promise<T>(resolve => setTimeout(() => resolve(task()), 0));
732
+ }
733
+
734
+ async function warmDev(config: ResolvedConfig, routes: RouteManifestEntry[]) {
735
+ // The compat implementation graph — what the first request used to wait on —
736
+ // plus the compile pipeline's fixed first-use costs. Both are memoized at
737
+ // their source, so a request that beats us here joins the same promises
738
+ // instead of starting a second copy.
739
+ await bootstrapCompat(config, { serve: true });
740
+ installServerRuntimePlugins();
741
+ runRequestWarmHooks(config);
742
+ await warmDevModulePipeline(config);
743
+ await warmDevRoutes(config, routes);
744
+ }
745
+
746
+ // Warm what this developer actually opens, not `/` (core spec change 6). The
747
+ // routes they hit are recorded next to the compiled cache, so the first boot of
748
+ // a checkout compiles nothing and every later one warms the last routes served —
749
+ // which, with the cache persisted, is usually a disk read rather than a compile.
750
+ const WARM_ROUTES = 3;
751
+
752
+ // Page requests served since boot. The boot warmer reads it to stand down once
753
+ // the developer has asked for something concrete (see warmDevRoutes).
754
+ let pageRequestsSeen = 0;
755
+
756
+ async function warmDevRoutes(config: ResolvedConfig, routes: RouteManifestEntry[]) {
757
+ const wanted = readDevRouteHistory(config)
758
+ .map(id => routes.find(route => route.id === id && route.kind === 'page'))
759
+ .filter((route): route is RouteManifestEntry => Boolean(route))
760
+ .slice(0, WARM_ROUTES);
761
+ if (wanted.length === 0) return;
762
+ // Every route is warmed on its own, in history order, and the whole pass stops the moment a real
763
+ // page request arrives: a request is the developer saying which route they want, which beats the
764
+ // history guess outright, and warming a *different* route beside it only steals CPU from the page
765
+ // they are waiting on (a save landing on in-flight warm work invalidates it and the page pays for
766
+ // it twice). The stand-down covers the stylesheet too, not just the route loop: a page request
767
+ // that arrived first is blocked on its own import, and global.css is a build it does not consume.
768
+ if (deferAssetPreload() && pageRequestsSeen > 0) return;
769
+ await buildDevAsset('/assets/global.css', () => buildGlobalCss(config, { dev: true }));
770
+ for (const route of wanted) {
771
+ if (pageRequestsSeen > 0) return;
772
+ await warmDevRoute(config, route).catch(() => undefined);
773
+ }
774
+ }
775
+
776
+ async function warmDevRoute(config: ResolvedConfig, route: RouteManifestEntry) {
777
+ noteDevCompileStart(route);
778
+ const start = performance.now();
779
+ const layoutFiles = devLayoutFiles(config, route.file);
780
+ const hasClient = route.client || route.clientReferences.length > 0;
781
+ await Promise.allSettled([
782
+ route.cssImports.length > 0
783
+ ? buildDevAsset(`/assets/${route.id}.css`, () => buildRouteCss(config, route, { dev: true }))
784
+ : Promise.resolve(),
785
+ devRouteModuleLoaders(config, route, layoutFiles),
786
+ hasClient ? buildDevClient(config, route) : Promise.resolve(),
787
+ warmDevClientReferences(config, route),
788
+ warmDevPageModule(config, route),
789
+ ]);
790
+ noteDevCompileDone(route, performance.now() - start);
791
+ }
792
+
793
+ /**
794
+ * A `'use client'` page is the one module of the route the route bundle does NOT carry
795
+ * (`writeDevRouteBundle` leaves it out), so the render imports it on its own. Same href the render
796
+ * resolves, so both callers share one compile and one module registry entry.
797
+ */
798
+ async function warmDevPageModule(config: ResolvedConfig, route: RouteManifestEntry) {
799
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
800
+ if (process.env.PNEXT_DEV_WARM_PAGE_MODULE === '0' || !route.client || route.kind !== 'page') {
801
+ return;
802
+ }
803
+ const href = await devServerModuleHref(config, route.file, undefined, {
804
+ conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
805
+ });
806
+ await drainPreplanBuilds();
807
+ await import(href);
808
+ }
809
+
810
+ /**
811
+ * The client-SSR half of the route: `markClientReferences` imports every SSR-able reference through
812
+ * the CLIENT layer, a different artifact from the browser bundle `buildDevClient` warms, and naming
813
+ * those artifacts walks the reference's whole source graph. Resolving the href here is the same
814
+ * memoized call the render makes, so a request that beats us joins the work instead of repeating it.
815
+ */
816
+ async function warmDevClientReferences(config: ResolvedConfig, route: RouteManifestEntry) {
817
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
818
+ if (process.env.PNEXT_DEV_WARM_CLIENT_REFS === '0') return;
819
+ const target = serverBundleTargetForRuntime(route.segmentConfig?.runtime);
820
+ const files = new Set(
821
+ route.clientReferences.filter(ssrClientReference).map(reference => reference.file),
822
+ );
823
+ await Promise.allSettled(
824
+ // The version argument is unused by devClientModuleHref (artifacts are named
825
+ // by content), so there is nothing to keep in sync with the render's call.
826
+ [...files].map(file => devClientModuleHref(config, file, undefined, target)),
827
+ );
828
+ }
829
+
830
+ const HISTORY_LIMIT = 8;
831
+ let history: string[] | undefined;
832
+ let historyWrite: Timer | undefined;
833
+
834
+ function historyFile(config: ResolvedConfig) {
835
+ return path.join(cacheRoot(config.outPath), 'warm.json');
836
+ }
837
+
838
+ function readDevRouteHistory(config: ResolvedConfig): string[] {
839
+ if (history) return history;
840
+ try {
841
+ const parsed = JSON.parse(readFileSync(historyFile(config), 'utf8')) as unknown;
842
+ history = Array.isArray(parsed) ? parsed.filter(id => typeof id === 'string') : [];
843
+ } catch {
844
+ history = [];
845
+ }
846
+ return history;
847
+ }
848
+
849
+ /** Remember a served route, most recent first — the next boot's warm list. */
850
+ function noteDevRouteServed(config: ResolvedConfig, route: RouteManifestEntry) {
851
+ if (route.kind !== 'page') return;
852
+ const seen = readDevRouteHistory(config);
853
+ if (seen[0] === route.id) return;
854
+ history = [route.id, ...seen.filter(id => id !== route.id)].slice(0, HISTORY_LIMIT);
855
+ if (historyWrite) return;
856
+ historyWrite = setTimeout(() => {
857
+ historyWrite = undefined;
858
+ void writeText(historyFile(config), JSON.stringify(history ?? [])).catch(() => undefined);
859
+ }, 500);
860
+ historyWrite.unref?.();
861
+ }
862
+
863
+ function logDevRouteLoaderFallback(route: RouteManifestEntry, error: unknown) {
864
+ console.error(
865
+ [
866
+ `PNext dev route bundle failed for ${route.route}; falling back to per-module loading.`,
867
+ error instanceof Error ? (error.stack ?? error.message) : String(error),
868
+ ].join('\n'),
869
+ );
870
+ }
871
+
872
+ interface DevRequestProfile {
873
+ label: string;
874
+ start: number;
875
+ }
876
+
877
+ function devRequestProfile(request: Request, url: URL): DevRequestProfile | undefined {
878
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
879
+ if (!process.env.PNEXT_DEV_PROFILE) return undefined;
880
+ return {
881
+ label: `${request.method} ${url.pathname}`,
882
+ start: performance.now(),
883
+ };
884
+ }
885
+
886
+ async function profileDevStep<T>(
887
+ profile: DevRequestProfile | undefined,
888
+ label: string,
889
+ task: () => Promise<T>,
890
+ ) {
891
+ if (!profile) return task();
892
+ const start = performance.now();
893
+ try {
894
+ return await task();
895
+ } finally {
896
+ logDevProfile(profile, label, start);
897
+ }
898
+ }
899
+
900
+ function profileDevSyncStep<T>(
901
+ profile: DevRequestProfile | undefined,
902
+ label: string,
903
+ task: () => T,
904
+ ) {
905
+ if (!profile) return task();
906
+ const start = performance.now();
907
+ try {
908
+ return task();
909
+ } finally {
910
+ logDevProfile(profile, label, start);
911
+ }
912
+ }
913
+
914
+ function logDevProfile(profile: DevRequestProfile, label: string, start: number) {
915
+ const end = performance.now();
916
+ // Both offsets, not just the end: stages on this path run as concurrent
917
+ // workflows, and overlap is only readable from where each one STARTED.
918
+ recordDevProfileLine(
919
+ `dev-profile ${profile.label} ${label} in ${formatProfileDuration(end - start)} (@${formatProfileDuration(start - profile.start)}..+${formatProfileDuration(end - profile.start)})`,
920
+ );
921
+ }
922
+
923
+ async function handleRoute(
924
+ config: ResolvedConfig,
925
+ route: RouteManifestEntry,
926
+ request: Request,
927
+ params: Record<string, RouteParamValue>,
928
+ devImportVersion: string,
929
+ ) {
930
+ const routeHref = await devServerModuleHref(config, route.file, devImportVersion, {
931
+ conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
932
+ });
933
+ await drainPreplanBuilds();
934
+ const imported = (await import(routeHref)) as RouteHandlerModule &
935
+ Parameters<typeof metadataRouteHandlerModule>[0];
936
+ const module = (metadataRouteHandlerModule(imported, route) ?? imported) as RouteHandlerModule;
937
+ return withRouteRuntime(route.segmentConfig?.runtime, () =>
938
+ runWithCacheScope(() => handleRouteModule(module, request, params, { routeFile: route.file })),
939
+ );
940
+ }
941
+
942
+ function routeClientCacheKey(config: ResolvedConfig, route: RouteManifestEntry) {
943
+ const existing = routeCacheKeys.get(route.id);
944
+ if (existing) return existing;
945
+ const key = devClientCacheKey(config, route, Boolean(config.compat?.next));
946
+ routeCacheKeys.set(route.id, key);
947
+ return key;
948
+ }
949
+
950
+ function preloadDevClient(
951
+ config: ResolvedConfig,
952
+ route: RouteManifestEntry,
953
+ profile: DevRequestProfile | undefined,
954
+ ) {
955
+ if (!route.client && route.clientReferences.length === 0 && !route.needsRouterEntry) return;
956
+ void profileDevStep(profile, `client preload ${route.route}`, () =>
957
+ buildDevClient(config, route),
958
+ )
959
+ .catch(error => logDevPreloadError(`client build for ${route.route}`, error))
960
+ .finally(() => {
961
+ if (profile) flushDevProfileLines();
962
+ });
963
+ }
964
+
965
+ /**
966
+ * The client-SSR artifacts `markClientReferences` imports, named at the top of the request instead of
967
+ * inside the render. The boot warm already does this for a history route, but a first page that outran the
968
+ * warm used to reach it only after the route bundle had imported, so the two ran back to back. Fired here
969
+ * they overlap, and the render's own call joins the same memoized promises.
970
+ */
971
+ function preloadDevClientReferences(
972
+ config: ResolvedConfig,
973
+ route: RouteManifestEntry,
974
+ profile: DevRequestProfile | undefined,
975
+ ) {
976
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
977
+ if (process.env.PNEXT_DEV_PRELOAD_REFS === '0') return;
978
+ if (route.clientReferences.length > 0) {
979
+ void profileDevStep(profile, `client references preload ${route.route}`, () =>
980
+ warmDevClientReferences(config, route),
981
+ ).catch(error => logDevPreloadError(`client references for ${route.route}`, error));
982
+ }
983
+ void profileDevStep(profile, `page module preload ${route.route}`, () =>
984
+ warmDevPageModule(config, route),
985
+ )
986
+ .then(() => preloadDevFonts(config, profile))
987
+ .catch(error => logDevPreloadError(`page module for ${route.route}`, error));
988
+ }
989
+
990
+ /**
991
+ * Font resolution is network-bound on a cold checkout and the render only asks for it at the very
992
+ * end, so it used to run alone with the box idle. The definitions exist as soon as the app's font
993
+ * module evaluates, so it runs as its own stage alongside the compiles and the render then joins a
994
+ * settled memo.
995
+ */
996
+ function preloadDevFonts(config: ResolvedConfig, profile: DevRequestProfile | undefined) {
997
+ return profileDevStep(profile, 'font prewarm', () =>
998
+ getFontExtensions().prewarmFontAssets(config, { dev: true }),
999
+ ).catch(error => logDevPreloadError('font prewarm', error));
1000
+ }
1001
+
1002
+ /** Bisect seam: `PNEXT_DEV_CLIENT_STAGE=eager` restores the in-request firing. */
1003
+ function deferClientStage() {
1004
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1005
+ return process.env.PNEXT_DEV_CLIENT_STAGE !== 'eager';
1006
+ }
1007
+
1008
+ /** Bisect seam: `PNEXT_DEV_ASSET_PRELOAD=eager` restores the pre-request firing. */
1009
+ function deferAssetPreload() {
1010
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1011
+ return process.env.PNEXT_DEV_ASSET_PRELOAD !== 'eager';
1012
+ }
1013
+
1014
+ function preloadDevPageAssets(
1015
+ config: ResolvedConfig,
1016
+ route: RouteManifestEntry,
1017
+ profile: DevRequestProfile | undefined,
1018
+ ) {
1019
+ void profileDevStep(profile, 'asset preload /assets/global.css', () =>
1020
+ buildDevAsset('/assets/global.css', () => buildGlobalCss(config, { dev: true })),
1021
+ ).catch(error => logDevPreloadError('global css build', error));
1022
+
1023
+ if (route.cssImports.length === 0) return;
1024
+ void profileDevStep(profile, `asset preload /assets/${route.id}.css`, () =>
1025
+ buildDevAsset(`/assets/${route.id}.css`, () => buildRouteCss(config, route, { dev: true })),
1026
+ ).catch(error => logDevPreloadError(`route css build for ${route.route}`, error));
1027
+ }
1028
+
1029
+ /** The route's existing layout chain, memoized until a structural save. */
1030
+ function devLayoutFiles(config: ResolvedConfig, routeFile: string) {
1031
+ const cached = routeLayoutFiles.get(routeFile);
1032
+ if (cached) return cached;
1033
+ const files = findLayouts(config.appPath, routeFile).filter(file => existsSync(file));
1034
+ routeLayoutFiles.set(routeFile, files);
1035
+ return files;
1036
+ }
1037
+
1038
+ function logDevPreloadError(label: string, error: unknown) {
1039
+ console.error(
1040
+ [
1041
+ `PNext dev preload failed for ${label}.`,
1042
+ error instanceof Error ? (error.stack ?? error.message) : String(error),
1043
+ ].join('\n'),
1044
+ );
1045
+ }
1046
+
1047
+ /**
1048
+ * Deferred dynamic reference by chunk id: a route-scanned dynamic reference
1049
+ * (server-component dynamic()), else the pipeline registry (dynamic() inside a
1050
+ * 'use client' module, registered by the entry build's rewrite or a sidecar).
1051
+ */
1052
+ function findDeferredDynamicReference(routes: RouteManifestEntry[], id: string) {
1053
+ for (const route of routes) {
1054
+ const reference = route.clientReferences.find(
1055
+ item => item.id === id && item.dynamic && !ssrClientReference(item),
1056
+ );
1057
+ if (reference) return { route, reference };
1058
+ }
1059
+ const registered = deferredDynamicRefById(id);
1060
+ if (registered) return { route: undefined, reference: { id, ...registered } };
1061
+ return undefined;
1062
+ }
1063
+
1064
+ // On-demand dynamic chunk builds: keyed by (id, dev import version) so a save
1065
+ // invalidates; chunk files index feeds /__pnext/client-dyn/chunks/ requests.
1066
+ const dynChunkBuilds = new Map<string, Promise<string>>();
1067
+ const dynChunkFiles = new Map<string, string>();
1068
+
1069
+ async function buildDevDynamicChunk(
1070
+ config: ResolvedConfig,
1071
+ route: RouteManifestEntry | undefined,
1072
+ reference: { id: string; file: string; exportName: string },
1073
+ devImportVersion: string,
1074
+ ) {
1075
+ const key = `${reference.id}\0${devImportVersion}`;
1076
+ const existing = dynChunkBuilds.get(key);
1077
+ if (existing) return existing;
1078
+ const outDir = path.join(
1079
+ config.outPath,
1080
+ 'cache',
1081
+ 'client-dyn',
1082
+ `${reference.id}-${createHash('sha1').update(devImportVersion).digest('hex').slice(0, 8)}`,
1083
+ );
1084
+ const build = (async () => {
1085
+ const outfile = path.join(outDir, `${reference.id}.js`);
1086
+ if (!existsSync(outfile)) {
1087
+ await buildClientDynamicChunk({ config, route, reference, outDir });
1088
+ }
1089
+ const chunksDir = path.join(outDir, 'chunks');
1090
+ if (existsSync(chunksDir)) {
1091
+ for (const entry of await readdir(chunksDir)) {
1092
+ if (entry.endsWith('.js')) dynChunkFiles.set(entry, path.join(chunksDir, entry));
1093
+ }
1094
+ }
1095
+ return outfile;
1096
+ })().catch(error => {
1097
+ dynChunkBuilds.delete(key);
1098
+ throw error;
1099
+ });
1100
+ dynChunkBuilds.set(key, build);
1101
+ return build;
1102
+ }
1103
+
1104
+ /** A cached entry never re-ran the pipeline rewrite: recover its chunk-id map. */
1105
+ async function loadDeferredDynamicSidecar(outDir: string) {
1106
+ try {
1107
+ const parsed = JSON.parse(
1108
+ await readFile(path.join(outDir, DEFERRED_DYNAMIC_SIDECAR), 'utf8'),
1109
+ ) as Record<string, { file: string; exportName: string }>;
1110
+ for (const [id, ref] of Object.entries(parsed)) {
1111
+ if (ref?.file && ref.exportName) registerDeferredDynamicRef(id, ref);
1112
+ }
1113
+ } catch {
1114
+ // No sidecar: the entry has no deferred dynamic refs.
1115
+ }
1116
+ }
1117
+
1118
+ async function buildDevClient(config: ResolvedConfig, route: RouteManifestEntry) {
1119
+ const cacheKey = await routeClientCacheKey(config, route);
1120
+ const outDir = path.join(config.outPath, 'cache', 'client', cacheKey);
1121
+ const outFile = path.join(outDir, `${clientEntryName(route)}.js`);
1122
+
1123
+ // Dedup in-flight builds first. A single page view fires up to three requests
1124
+ // for the same route's client bundle — the background preload, the entry, and
1125
+ // each chunk — and they must share one build. Two esbuild runs into the same
1126
+ // outDir would race nameSharedClientChunks' rename and fail with ENOENT.
1127
+ const existing = clientBuilds.get(outDir);
1128
+ if (existing) return existing;
1129
+
1130
+ if (existsSync(outFile)) {
1131
+ // Index chunks so chunk requests skip the rebuild fallback — once per outDir,
1132
+ // not once per request: the dir name is a content key, so its chunk list is
1133
+ // fixed for as long as it exists.
1134
+ if (!indexedClientDirs.has(outDir)) {
1135
+ await indexClientChunks(outDir);
1136
+ await loadDeferredDynamicSidecar(outDir);
1137
+ indexedClientDirs.add(outDir);
1138
+ }
1139
+ return outFile;
1140
+ }
1141
+
1142
+ // Register the promise synchronously (no await between the get above and this
1143
+ // set) so concurrent callers can't both slip past the dedup check.
1144
+ const inner = (async () => {
1145
+ await ensureDir(outDir);
1146
+ const file = await buildClientEntry({ config, route, outDir, dev: true });
1147
+ // Dedup this generation's chunk bytes against the shared store before serving
1148
+ //: identical chunks recur across generations, and this is
1149
+ // the one place all of a generation's output exists before requests read it.
1150
+ await contentAddressDir(outDir, clientChunkStoreDir(config.outPath));
1151
+ await indexClientChunks(outDir);
1152
+ indexedClientDirs.add(outDir);
1153
+ return file;
1154
+ })();
1155
+ let unregister: () => void = () => undefined;
1156
+ const stopSignal = new Promise<never>((_, reject) => {
1157
+ clientBuildStopWaiters.add(reject);
1158
+ unregister = () => void clientBuildStopWaiters.delete(reject);
1159
+ });
1160
+ // If the watchdog wins the race, `inner` may settle later with no listener.
1161
+ inner.catch(() => undefined);
1162
+ const build = Promise.race([inner, stopSignal]).finally(() => {
1163
+ unregister();
1164
+ clientBuilds.delete(outDir);
1165
+ });
1166
+ clientBuilds.set(outDir, build);
1167
+ return build;
1168
+ }
1169
+
1170
+ async function maybeDevClientChunk(
1171
+ config: ResolvedConfig,
1172
+ routes: RouteManifestEntry[],
1173
+ pathname: string,
1174
+ ) {
1175
+ const chunkMatch = /^\/__pnext\/client\/chunks\/(.+\.js)$/.exec(pathname);
1176
+ if (!chunkMatch?.[1]) return null;
1177
+ const cached = clientChunks.get(chunkMatch[1]);
1178
+ if (cached && existsSync(cached)) {
1179
+ return devChunkResponse(await readFile(cached));
1180
+ }
1181
+
1182
+ for (const route of routes) {
1183
+ if (!route.client && route.clientReferences.length === 0) continue;
1184
+ const cacheKey = await routeClientCacheKey(config, route);
1185
+ const outDir = path.join(config.outPath, 'cache', 'client', cacheKey);
1186
+ await buildDevClient(config, route);
1187
+ const file = path.join(outDir, 'chunks', chunkMatch[1]);
1188
+ if (!isInside(path.join(outDir, 'chunks'), file) || !existsSync(file)) continue;
1189
+ return devChunkResponse(await readFile(file));
1190
+ }
1191
+
1192
+ return null;
1193
+ }
1194
+
1195
+ /**
1196
+ * Serve the prebuilt client runtime. It lives outside `config.outPath` on purpose - a cold page is defined
1197
+ * by that directory being wiped, and an artifact wiped with it would be rebuilt on exactly the request it
1198
+ * exists to make cheap - so it needs its own route rather than the built-asset lookup.
1199
+ */
1200
+ function maybePrebuiltRuntime(config: ResolvedConfig, pathname: string) {
1201
+ const match = /^\/__pnext\/runtime\/([0-9a-f]+)\/(.+\.js)$/.exec(pathname);
1202
+ if (!match?.[1] || !match[2]) return null;
1203
+ const dir = prebuiltRuntimeDir(config, match[1]);
1204
+ const file = path.join(dir, match[2]);
1205
+ if (!isInside(dir, file) || !existsSync(file)) return null;
1206
+ return devChunkResponse(readFileSync(file));
1207
+ }
1208
+
1209
+ /** Serve an extension-supplied standalone chunk by its `/_next/static/chunks/<name>` path. */
1210
+ function maybeStaticClientChunk(config: ResolvedConfig, staticAssetPathname: string) {
1211
+ const match = /^\/_next\/static\/chunks\/([^/]+\.js)$/.exec(staticAssetPathname);
1212
+ if (!match?.[1]) return null;
1213
+ const chunk = getBundlerExtensions()
1214
+ .staticClientChunks(config)
1215
+ .find(item => item.name === match[1]);
1216
+ return chunk ? devChunkResponse(chunk.contents) : null;
1217
+ }
1218
+
1219
+ async function indexClientChunks(outDir: string) {
1220
+ const chunksDir = path.join(outDir, 'chunks');
1221
+ if (!existsSync(chunksDir)) return;
1222
+ for (const file of await listFiles(chunksDir)) {
1223
+ if (file.endsWith('.js')) clientChunks.set(path.basename(file), file);
1224
+ }
1225
+ }
1226
+
1227
+ async function maybeStaticFile(publicPath: string, pathname: string) {
1228
+ const filePath = path.join(publicPath, pathname.replace(/^\/+/, ''));
1229
+ if (!isInside(publicPath, filePath) || !existsSync(filePath)) return null;
1230
+ const fileStat = await stat(filePath);
1231
+ if (!fileStat.isFile()) return null;
1232
+ return devResponse(await readFile(filePath), contentType(filePath));
1233
+ }
1234
+
1235
+ function getNextStaticAssetPathname(
1236
+ pathname: string,
1237
+ basePath: string | undefined,
1238
+ assetPrefix: string | undefined,
1239
+ ) {
1240
+ if (pathname.startsWith('/_next/static/')) return pathname;
1241
+ const normalizedBasePath = normalizePathPrefix(basePath);
1242
+ if (normalizedBasePath && pathname.startsWith(`${normalizedBasePath}/_next/static/`)) {
1243
+ return pathname.slice(normalizedBasePath.length);
1244
+ }
1245
+ const normalizedAssetPrefix = normalizePathPrefix(assetPrefix);
1246
+ if (normalizedAssetPrefix && pathname.startsWith(`${normalizedAssetPrefix}/_next/static/`)) {
1247
+ return pathname.slice(normalizedAssetPrefix.length);
1248
+ }
1249
+ return null;
1250
+ }
1251
+
1252
+ function normalizePathPrefix(prefix: string | undefined): string | null {
1253
+ if (!prefix) return null;
1254
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(prefix)) return null;
1255
+ const normalized = `/${prefix.replace(/^\/+|\/+$/g, '')}`;
1256
+ if (normalized === '/') return null;
1257
+ return normalized;
1258
+ }
1259
+
1260
+ function findClientReferenceCss(routes: RouteManifestEntry[], id: string) {
1261
+ return routes
1262
+ .flatMap(route => route.clientReferences)
1263
+ .find(reference => reference.id === id && reference.cssImports?.length);
1264
+ }
1265
+
1266
+ async function maybeBuiltAsset(
1267
+ config: ResolvedConfig,
1268
+ routes: RouteManifestEntry[],
1269
+ pathname: string,
1270
+ ) {
1271
+ if (!pathname.startsWith('/assets/')) return null;
1272
+ if (pathname === '/assets/global.css') {
1273
+ await buildDevAsset(pathname, () => buildGlobalCss(config, { dev: true }));
1274
+ } else {
1275
+ const cssMatch = /^\/assets\/(.+)\.css$/.exec(pathname);
1276
+ const route = cssMatch?.[1] ? routes.find(item => item.id === cssMatch[1]) : undefined;
1277
+ // Island CSS is only ever requested for a route that already rendered, so
1278
+ // the resolved routes are searched first — scanning the rest would compile
1279
+ // the whole app to answer one stylesheet request.
1280
+ const reference =
1281
+ !route && cssMatch?.[1]
1282
+ ? findClientReferenceCss(routes.filter(routeFactsResolved), cssMatch[1]) ??
1283
+ findClientReferenceCss(routes, cssMatch[1])
1284
+ : undefined;
1285
+ if (route) {
1286
+ await buildDevAsset(pathname, () => buildRouteCss(config, route, { dev: true }));
1287
+ } else if (reference) {
1288
+ await buildDevAsset(pathname, () =>
1289
+ buildClientReferenceCss(config, reference, { dev: true }),
1290
+ );
1291
+ }
1292
+ }
1293
+
1294
+ const outPath = config.outPath;
1295
+ const filePath = path.join(outPath, 'cache', pathname.replace(/^\/+/, ''));
1296
+ if (!isInside(path.join(outPath, 'cache'), filePath) || !existsSync(filePath)) return null;
1297
+ const fileStat = await stat(filePath);
1298
+ if (!fileStat.isFile()) return null;
1299
+ return new Response(await readFile(filePath), {
1300
+ headers: { 'content-type': contentType(filePath) },
1301
+ });
1302
+ }
1303
+
1304
+ function buildDevAsset(key: string, build: () => Promise<unknown>) {
1305
+ const existing = assetBuilds.get(key);
1306
+ if (existing) return existing;
1307
+ // Build each asset once per dev version (reload() clears it); drop on failure to allow retry.
1308
+ const next = build().catch(error => {
1309
+ assetBuilds.delete(key);
1310
+ throw error;
1311
+ });
1312
+ assetBuilds.set(key, next);
1313
+ return next;
1314
+ }
1315
+
1316
+ function maybeDevPagePrefetchResponse(
1317
+ routes: RouteManifestEntry[],
1318
+ pathname: string,
1319
+ request: Request,
1320
+ ) {
1321
+ if (!isDevPagePrefetchRequest(request)) return null;
1322
+ const matched = matchRoute(routes, pathname);
1323
+ if (matched?.route.kind !== 'page') return null;
1324
+ return devPagePrefetchResponse();
1325
+ }
1326
+
1327
+ export function isDevPagePrefetchRequest(request: Request) {
1328
+ if (request.method.toUpperCase() !== 'GET') return false;
1329
+ return (
1330
+ headerHasPrefetchToken(request.headers.get('sec-purpose')) ||
1331
+ headerHasPrefetchToken(request.headers.get('purpose')) ||
1332
+ getRouterProtocolExtensions()
1333
+ .prefetchRequestHeaders()
1334
+ .some(header => request.headers.get(header) === '1')
1335
+ );
1336
+ }
1337
+
1338
+ function headerHasPrefetchToken(value: string | null) {
1339
+ return Boolean(value?.split(/[;,\s]+/).some(token => token.toLowerCase() === 'prefetch'));
1340
+ }
1341
+
1342
+ function devPagePrefetchResponse() {
1343
+ return new Response(null, {
1344
+ status: 204,
1345
+ headers: {
1346
+ 'cache-control': 'no-store',
1347
+ 'x-pnext-dev-prefetch': 'skipped',
1348
+ },
1349
+ });
1350
+ }
1351
+
1352
+ interface PendingDevPageLoadLog {
1353
+ method: string;
1354
+ pathname: string;
1355
+ route: string;
1356
+ start: number;
1357
+ }
1358
+
1359
+ interface DevPageLoadLog {
1360
+ method: string;
1361
+ pathname: string;
1362
+ route: string;
1363
+ status: number;
1364
+ durationMs: number;
1365
+ }
1366
+
1367
+ function pendingDevPageLoadLog(
1368
+ routes: RouteManifestEntry[],
1369
+ request: Request,
1370
+ url: URL,
1371
+ start = performance.now(),
1372
+ ): PendingDevPageLoadLog | undefined {
1373
+ const matched = matchRoute(routes, url.pathname);
1374
+ if (matched?.route.kind !== 'page') return undefined;
1375
+ return {
1376
+ method: request.method,
1377
+ pathname: url.pathname,
1378
+ route: matched.route.route,
1379
+ start,
1380
+ };
1381
+ }
1382
+
1383
+ function logDevPageResponse(pending: PendingDevPageLoadLog | undefined, response: Response) {
1384
+ if (!pending) return response;
1385
+ logDevPageLoad({
1386
+ method: pending.method,
1387
+ pathname: pending.pathname,
1388
+ route: pending.route,
1389
+ status: response.status,
1390
+ durationMs: performance.now() - pending.start,
1391
+ });
1392
+ return response;
1393
+ }
1394
+
1395
+ function logDevPageError(pending: PendingDevPageLoadLog | undefined) {
1396
+ if (!pending) return;
1397
+ logDevPageLoad({
1398
+ method: pending.method,
1399
+ pathname: pending.pathname,
1400
+ route: pending.route,
1401
+ status: 500,
1402
+ durationMs: performance.now() - pending.start,
1403
+ });
1404
+ }
1405
+
1406
+ function logDevPageAbort(pending: PendingDevPageLoadLog | undefined) {
1407
+ if (!pending) return;
1408
+ logDevPageLoad({
1409
+ method: pending.method,
1410
+ pathname: pending.pathname,
1411
+ route: pending.route,
1412
+ status: 499,
1413
+ durationMs: performance.now() - pending.start,
1414
+ });
1415
+ }
1416
+
1417
+ // Print a "Compiling" banner the first time a route is hit in this process,
1418
+ // so the several-second cold esbuild + Tailwind build isn't a silent stall. The
1419
+ // timed `page GET ... in Xs` line follows once the response resolves.
1420
+ function noteDevCompileStart(route: RouteManifestEntry) {
1421
+ if (compiledRoutes.has(route.id)) return;
1422
+ compiledRoutes.add(route.id);
1423
+ console.log(`${cyan('○')} ${dim('Compiling')} ${cyan(route.route)} ${dim('...')}`);
1424
+ }
1425
+
1426
+ // Completion line for the warm compile, since no request log fires before the
1427
+ // browser opens. Mirrors the `page ... in Xs` timing style.
1428
+ function noteDevCompileDone(route: RouteManifestEntry, durationMs: number) {
1429
+ console.log(
1430
+ `${green('✓')} ${dim('Compiled')} ${cyan(route.route)} ${dim('in')} ${durationLabel(durationMs)}`,
1431
+ );
1432
+ }
1433
+
1434
+ function logDevPageLoad(entry: DevPageLoadLog) {
1435
+ console.log(formatDevPageLoadLog(entry));
1436
+ }
1437
+
1438
+ export function formatDevPageLoadLog(entry: DevPageLoadLog) {
1439
+ const route = entry.route === entry.pathname ? '' : dim(`(${entry.route})`);
1440
+ return [
1441
+ dim('page'),
1442
+ dim(entry.method),
1443
+ cyan(entry.pathname),
1444
+ route,
1445
+ statusLabel(entry.status),
1446
+ dim('in'),
1447
+ durationLabel(entry.durationMs),
1448
+ ]
1449
+ .filter(Boolean)
1450
+ .join(' ');
1451
+ }
1452
+
1453
+ function statusLabel(status: number) {
1454
+ const label = String(status);
1455
+ if (status >= 500) return red(label);
1456
+ if (status >= 400) return yellow(label);
1457
+ if (status >= 300) return cyan(label);
1458
+ return green(label);
1459
+ }
1460
+
1461
+ function durationLabel(durationMs: number) {
1462
+ const label = formatDuration(durationMs);
1463
+ if (durationMs >= 1000) return yellow(label);
1464
+ if (durationMs >= 250) return cyan(label);
1465
+ return green(label);
1466
+ }
1467
+
1468
+ function formatDuration(durationMs: number) {
1469
+ const ms = Math.max(0, durationMs);
1470
+ if (ms < 10) return `${ms.toFixed(1)}ms`;
1471
+ if (ms < 1000) return `${Math.round(ms)}ms`;
1472
+ return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
1473
+ }
1474
+
1475
+ function dim(value: string) {
1476
+ return color(2, 22, value);
1477
+ }
1478
+
1479
+ function red(value: string) {
1480
+ return color(31, 39, value);
1481
+ }
1482
+
1483
+ function yellow(value: string) {
1484
+ return color(33, 39, value);
1485
+ }
1486
+
1487
+ function green(value: string) {
1488
+ return color(32, 39, value);
1489
+ }
1490
+
1491
+ function cyan(value: string) {
1492
+ return color(36, 39, value);
1493
+ }
1494
+
1495
+ function color(open: number, close: number, value: string) {
1496
+ if (!process.stdout.isTTY) return value;
1497
+ return `\x1b[${open}m${value}\x1b[${close}m`;
1498
+ }
1499
+
1500
+ function devResponse(body: BodyInit, contentType: string) {
1501
+ return new Response(body, {
1502
+ headers: {
1503
+ 'content-type': contentType,
1504
+ 'cache-control': 'no-store',
1505
+ },
1506
+ });
1507
+ }
1508
+
1509
+ // Client chunk filenames are content-hashed (chunks/[name]-[hash].js), so they
1510
+ // are safe to cache permanently: a source change yields a new hash (new URL).
1511
+ // The page entry stays `no-store`, so a dev reload re-fetches only the entry and
1512
+ // any changed chunks instead of the whole 7MB+ graph on every navigation.
1513
+ function devChunkResponse(body: BodyInit) {
1514
+ return new Response(body, {
1515
+ headers: {
1516
+ 'content-type': 'text/javascript; charset=utf-8',
1517
+ 'cache-control': 'public, max-age=31536000, immutable',
1518
+ },
1519
+ });
1520
+ }
1521
+
1522
+ function isInside(root: string, file: string) {
1523
+ const relative = path.relative(root, file);
1524
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
1525
+ }
1526
+
1527
+ interface DevWatcher {
1528
+ rootsKey: string;
1529
+ stop(): void;
1530
+ }
1531
+
1532
+ function refreshWatcher(
1533
+ config: ResolvedConfig,
1534
+ routes: RouteManifestEntry[],
1535
+ current: DevWatcher | undefined,
1536
+ onChange: (change: DevChange) => Promise<void>,
1537
+ ) {
1538
+ const roots = devWatchRoots(config, routes);
1539
+ const rootsKey = roots.join('\0');
1540
+ if (current?.rootsKey === rootsKey) return current;
1541
+ current?.stop();
1542
+ return watchRoots(roots, rootsKey, onChange);
1543
+ }
1544
+
1545
+ function devWatchRoots(config: ResolvedConfig, routes: RouteManifestEntry[]) {
1546
+ const packageRoots = workspacePackageRoots(config.workspaceRoot);
1547
+ const roots = new Set([config.appPath]);
1548
+
1549
+ // Reading sourceFiles off an unresolved route would scan the whole app at
1550
+ // boot — exactly what the deferred route table exists to avoid.
1551
+ for (const route of routes.filter(routeFactsResolved)) {
1552
+ for (const file of route.sourceFiles) {
1553
+ if (isInside(config.appPath, file)) continue;
1554
+ const packageRoot = packageRoots.find(root => isInside(root, file));
1555
+ if (packageRoot) roots.add(packageWatchRoot(packageRoot, file));
1556
+ else {
1557
+ // An app that is not a declared workspace member has no package root,
1558
+ // which used to leave its own components/, lib/ and styles/ unwatched:
1559
+ // the server served fresh content but the open page was never told.
1560
+ const projectRoot = projectWatchRoot(config.root, file);
1561
+ if (projectRoot) roots.add(projectRoot);
1562
+ }
1563
+ }
1564
+ }
1565
+
1566
+ return [...roots].sort();
1567
+ }
1568
+
1569
+ /**
1570
+ * The top-level directory under the project root that `file` lives in. Watching
1571
+ * the root itself would pull in node_modules and .pnext, so dependencies, dot
1572
+ * directories and files sitting directly in the root are not covered.
1573
+ */
1574
+ function projectWatchRoot(root: string, file: string) {
1575
+ if (!isInside(root, file)) return undefined;
1576
+ const [segment, ...rest] = path.relative(root, file).split(path.sep);
1577
+ if (!segment || rest.length === 0) return undefined;
1578
+ if (segment === 'node_modules' || segment.startsWith('.')) return undefined;
1579
+ return path.join(root, segment);
1580
+ }
1581
+
1582
+ function packageWatchRoot(packageRoot: string, file: string) {
1583
+ const relative = path.relative(packageRoot, file);
1584
+ const [firstSegment] = relative.split(path.sep);
1585
+ if (!firstSegment || firstSegment === '..' || path.isAbsolute(relative)) return packageRoot;
1586
+ return path.join(packageRoot, firstSegment);
1587
+ }
1588
+
1589
+ /**
1590
+ * Can this burst be answered by re-fetching stylesheets alone? Only if every
1591
+ * touched file is a plain stylesheet that still exists: a CSS *module* feeds
1592
+ * class names into JS bundles, and a file that is gone (or a rename) can move
1593
+ * the route table's CSS imports, so both keep the full reload.
1594
+ */
1595
+ function isCssOnlyChange(files: string[]) {
1596
+ return (
1597
+ files.length > 0 &&
1598
+ files.every(file => isCssFile(file) && !/\.module\.[^.]+$/.test(file) && existsSync(file))
1599
+ );
1600
+ }
1601
+
1602
+ /** What a watcher burst touched. */
1603
+ interface DevChange {
1604
+ files: string[];
1605
+ /** At least one event was a create/delete/rename (an atomic save is one too). */
1606
+ renamed: boolean;
1607
+ }
1608
+
1609
+ function watchRoots(
1610
+ roots: string[],
1611
+ rootsKey: string,
1612
+ onChange: (change: DevChange) => Promise<void>,
1613
+ ): DevWatcher {
1614
+ const controller = new AbortController();
1615
+ let pending: Timer | undefined;
1616
+ let running = false;
1617
+ let batch: DevChange = { files: [], renamed: false };
1618
+
1619
+ async function flush() {
1620
+ if (running || batch.files.length === 0) return;
1621
+ const change = batch;
1622
+ batch = { files: [], renamed: false };
1623
+ running = true;
1624
+ try {
1625
+ await onChange(change);
1626
+ } finally {
1627
+ running = false;
1628
+ }
1629
+ if (batch.files.length > 0) await flush();
1630
+ }
1631
+
1632
+ function schedule(file: string, renamed: boolean) {
1633
+ batch.files.push(file);
1634
+ batch.renamed ||= renamed;
1635
+ if (pending) clearTimeout(pending);
1636
+ // The coalescing window exists so one save that lands as several events costs one recompile. A
1637
+ // stylesheet-only burst has no recompile to coalesce, so it waits a tenth as long; a JS file
1638
+ // arriving late just triggers the ordinary reload behind it.
1639
+ pending = setTimeout(
1640
+ () => {
1641
+ pending = undefined;
1642
+ void flush();
1643
+ },
1644
+ isCssOnlyChange(batch.files) ? 4 : 40,
1645
+ );
1646
+ }
1647
+
1648
+ for (const root of roots) {
1649
+ void watchRoot(root, controller.signal, schedule);
1650
+ }
1651
+
1652
+ return {
1653
+ rootsKey,
1654
+ stop() {
1655
+ if (pending) clearTimeout(pending);
1656
+ controller.abort();
1657
+ },
1658
+ };
1659
+ }
1660
+
1661
+ async function watchRoot(
1662
+ root: string,
1663
+ signal: AbortSignal,
1664
+ onChange: (file: string, structural: boolean) => void,
1665
+ ) {
1666
+ try {
1667
+ const watcher = watch(root, { recursive: true, signal });
1668
+ for await (const event of watcher) {
1669
+ // No filename (rare, platform-dependent) means we cannot scope the
1670
+ // invalidation: treat it as structural so everything is re-derived.
1671
+ if (!event.filename) onChange(root, true);
1672
+ else onChange(path.resolve(root, event.filename), event.eventType === 'rename');
1673
+ }
1674
+ } catch (error) {
1675
+ if (signal.aborted || (error instanceof Error && error.name === 'AbortError')) return;
1676
+ // Some platforms do not support recursive watch. Dev still works without live reload.
1677
+ }
1678
+ }
1679
+
1680
+ function eventStream(clients: Set<DevEventClient>) {
1681
+ let client: DevEventClient | undefined;
1682
+ const stream = new ReadableStream<Uint8Array>({
1683
+ start(controller) {
1684
+ client = { controller };
1685
+ clients.add(client);
1686
+ controller.enqueue(new TextEncoder().encode('event: ready\ndata: ok\n\n'));
1687
+ },
1688
+ cancel() {
1689
+ if (client) clients.delete(client);
1690
+ },
1691
+ });
1692
+ return new Response(stream, {
1693
+ headers: {
1694
+ 'content-type': 'text/event-stream',
1695
+ 'cache-control': 'no-cache',
1696
+ connection: 'keep-alive',
1697
+ },
1698
+ });
1699
+ }
1700
+
1701
+ function broadcast(clients: Set<DevEventClient>, event: string) {
1702
+ const payload = new TextEncoder().encode(`event: ${event}\ndata: ${Date.now()}\n\n`);
1703
+ for (const client of clients) {
1704
+ try {
1705
+ client.controller.enqueue(payload);
1706
+ } catch {
1707
+ clients.delete(client);
1708
+ }
1709
+ }
1710
+ }