@wular/pnext 0.0.3 → 0.0.6

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