@nifrajs/web 3.3.0 → 4.0.0

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 (407) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +35 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +89 -35
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +18 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +97 -17
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +72 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +411 -98
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +21 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +226 -15
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/css-contract.d.ts +21 -0
  40. package/dist/css-contract.d.ts.map +1 -0
  41. package/dist/css-contract.js +28 -0
  42. package/dist/css-contract.js.map +1 -0
  43. package/dist/dev-client.d.ts +76 -0
  44. package/dist/dev-client.d.ts.map +1 -0
  45. package/dist/dev-client.js +280 -0
  46. package/dist/dev-client.js.map +1 -0
  47. package/dist/dev-diagnostics.d.ts +26 -2
  48. package/dist/dev-diagnostics.d.ts.map +1 -1
  49. package/dist/dev-diagnostics.js +38 -7
  50. package/dist/dev-diagnostics.js.map +1 -1
  51. package/dist/dev-error.d.ts +4 -13
  52. package/dist/dev-error.d.ts.map +1 -1
  53. package/dist/dev-error.js +46 -4
  54. package/dist/dev-error.js.map +1 -1
  55. package/dist/dev-feed.d.ts +298 -0
  56. package/dist/dev-feed.d.ts.map +1 -0
  57. package/dist/dev-feed.js +797 -0
  58. package/dist/dev-feed.js.map +1 -0
  59. package/dist/dev-indicator.d.ts +38 -0
  60. package/dist/dev-indicator.d.ts.map +1 -0
  61. package/dist/dev-indicator.js +93 -0
  62. package/dist/dev-indicator.js.map +1 -0
  63. package/dist/dev-session.d.ts +77 -0
  64. package/dist/dev-session.d.ts.map +1 -0
  65. package/dist/dev-session.js +580 -0
  66. package/dist/dev-session.js.map +1 -0
  67. package/dist/dev-sourcemap.d.ts +27 -0
  68. package/dist/dev-sourcemap.d.ts.map +1 -0
  69. package/dist/dev-sourcemap.js +235 -0
  70. package/dist/dev-sourcemap.js.map +1 -0
  71. package/dist/dev-ssr-graph.d.ts.map +1 -1
  72. package/dist/dev-ssr-graph.js +13 -5
  73. package/dist/dev-ssr-graph.js.map +1 -1
  74. package/dist/dev.d.ts +15 -2
  75. package/dist/dev.d.ts.map +1 -1
  76. package/dist/dev.js +69 -44
  77. package/dist/dev.js.map +1 -1
  78. package/dist/diagnostic-catalog.d.ts +35 -0
  79. package/dist/diagnostic-catalog.d.ts.map +1 -0
  80. package/dist/diagnostic-catalog.js +203 -0
  81. package/dist/diagnostic-catalog.js.map +1 -0
  82. package/dist/diagnostic-prompt.d.ts +49 -0
  83. package/dist/diagnostic-prompt.d.ts.map +1 -0
  84. package/dist/diagnostic-prompt.js +303 -0
  85. package/dist/diagnostic-prompt.js.map +1 -0
  86. package/dist/diagnostic.d.ts +17 -23
  87. package/dist/diagnostic.d.ts.map +1 -1
  88. package/dist/diagnostic.js +31 -39
  89. package/dist/diagnostic.js.map +1 -1
  90. package/dist/draft.d.ts.map +1 -1
  91. package/dist/draft.js +4 -1
  92. package/dist/draft.js.map +1 -1
  93. package/dist/fn.d.ts +5 -0
  94. package/dist/fn.d.ts.map +1 -1
  95. package/dist/fn.js +13 -3
  96. package/dist/fn.js.map +1 -1
  97. package/dist/fs.d.ts.map +1 -1
  98. package/dist/fs.js +4 -2
  99. package/dist/fs.js.map +1 -1
  100. package/dist/index.d.ts +15 -15
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +13 -12
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  105. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  106. package/dist/internal/cdn/cloudflare.js +94 -0
  107. package/dist/internal/cdn/cloudflare.js.map +1 -0
  108. package/dist/internal/cdn/core.d.ts +91 -0
  109. package/dist/internal/cdn/core.d.ts.map +1 -0
  110. package/dist/internal/cdn/core.js +220 -0
  111. package/dist/internal/cdn/core.js.map +1 -0
  112. package/dist/internal/cdn/fastly.d.ts +17 -0
  113. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  114. package/dist/internal/cdn/fastly.js +43 -0
  115. package/dist/internal/cdn/fastly.js.map +1 -0
  116. package/dist/internal/cdn/vercel.d.ts +31 -0
  117. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  118. package/dist/internal/cdn/vercel.js +56 -0
  119. package/dist/internal/cdn/vercel.js.map +1 -0
  120. package/dist/internal/code-unit-order.d.ts +4 -0
  121. package/dist/internal/code-unit-order.d.ts.map +1 -0
  122. package/dist/internal/code-unit-order.js +6 -0
  123. package/dist/internal/code-unit-order.js.map +1 -0
  124. package/dist/internal/codegen.d.ts +3 -0
  125. package/dist/internal/codegen.d.ts.map +1 -1
  126. package/dist/internal/codegen.js +151 -30
  127. package/dist/internal/codegen.js.map +1 -1
  128. package/dist/internal/dev-csp.d.ts +24 -0
  129. package/dist/internal/dev-csp.d.ts.map +1 -0
  130. package/dist/internal/dev-csp.js +160 -0
  131. package/dist/internal/dev-csp.js.map +1 -0
  132. package/dist/internal/dev-reserved.d.ts +25 -0
  133. package/dist/internal/dev-reserved.d.ts.map +1 -0
  134. package/dist/internal/dev-reserved.js +39 -0
  135. package/dist/internal/dev-reserved.js.map +1 -0
  136. package/dist/internal/html-spans.d.ts +10 -0
  137. package/dist/internal/html-spans.d.ts.map +1 -0
  138. package/dist/internal/html-spans.js +46 -0
  139. package/dist/internal/html-spans.js.map +1 -0
  140. package/dist/internal/loading-runtime.d.ts +44 -0
  141. package/dist/internal/loading-runtime.d.ts.map +1 -0
  142. package/dist/internal/loading-runtime.js +206 -0
  143. package/dist/internal/loading-runtime.js.map +1 -0
  144. package/dist/internal/matches-runtime.d.ts +8 -0
  145. package/dist/internal/matches-runtime.d.ts.map +1 -0
  146. package/dist/internal/matches-runtime.js +61 -0
  147. package/dist/internal/matches-runtime.js.map +1 -0
  148. package/dist/internal/mount-shadow.d.ts +36 -0
  149. package/dist/internal/mount-shadow.d.ts.map +1 -0
  150. package/dist/internal/mount-shadow.js +65 -0
  151. package/dist/internal/mount-shadow.js.map +1 -0
  152. package/dist/internal/node-builtins.d.ts +3 -0
  153. package/dist/internal/node-builtins.d.ts.map +1 -0
  154. package/dist/internal/node-builtins.js +79 -0
  155. package/dist/internal/node-builtins.js.map +1 -0
  156. package/dist/internal/output-guard.d.ts +65 -0
  157. package/dist/internal/output-guard.d.ts.map +1 -0
  158. package/dist/internal/output-guard.js +630 -0
  159. package/dist/internal/output-guard.js.map +1 -0
  160. package/dist/internal/page-execution.d.ts +69 -0
  161. package/dist/internal/page-execution.d.ts.map +1 -0
  162. package/dist/internal/page-execution.js +1021 -0
  163. package/dist/internal/page-execution.js.map +1 -0
  164. package/dist/internal/parity.d.ts +21 -0
  165. package/dist/internal/parity.d.ts.map +1 -1
  166. package/dist/internal/parity.js +104 -22
  167. package/dist/internal/parity.js.map +1 -1
  168. package/dist/internal/private-env.d.ts +12 -0
  169. package/dist/internal/private-env.d.ts.map +1 -0
  170. package/dist/internal/private-env.js +335 -0
  171. package/dist/internal/private-env.js.map +1 -0
  172. package/dist/internal/public-url.d.ts +3 -0
  173. package/dist/internal/public-url.d.ts.map +1 -0
  174. package/dist/internal/public-url.js +18 -0
  175. package/dist/internal/public-url.js.map +1 -0
  176. package/dist/internal/render-document.d.ts +37 -21
  177. package/dist/internal/render-document.d.ts.map +1 -1
  178. package/dist/internal/render-document.js +104 -65
  179. package/dist/internal/render-document.js.map +1 -1
  180. package/dist/internal/request-url.d.ts +3 -0
  181. package/dist/internal/request-url.d.ts.map +1 -0
  182. package/dist/internal/request-url.js +14 -0
  183. package/dist/internal/request-url.js.map +1 -0
  184. package/dist/internal/response-controls.d.ts +75 -0
  185. package/dist/internal/response-controls.d.ts.map +1 -0
  186. package/dist/internal/response-controls.js +256 -0
  187. package/dist/internal/response-controls.js.map +1 -0
  188. package/dist/internal/runtime-contract.d.ts +11 -16
  189. package/dist/internal/runtime-contract.d.ts.map +1 -1
  190. package/dist/internal/runtime-contract.js.map +1 -1
  191. package/dist/internal/secret-scan.d.ts +104 -0
  192. package/dist/internal/secret-scan.d.ts.map +1 -0
  193. package/dist/internal/secret-scan.js +485 -0
  194. package/dist/internal/secret-scan.js.map +1 -0
  195. package/dist/internal/server-boundary.d.ts +6 -29
  196. package/dist/internal/server-boundary.d.ts.map +1 -1
  197. package/dist/internal/server-boundary.js +6 -66
  198. package/dist/internal/server-boundary.js.map +1 -1
  199. package/dist/internal/source-map.d.ts +47 -0
  200. package/dist/internal/source-map.d.ts.map +1 -0
  201. package/dist/internal/source-map.js +225 -0
  202. package/dist/internal/source-map.js.map +1 -0
  203. package/dist/internal/target-compat.d.ts +27 -0
  204. package/dist/internal/target-compat.d.ts.map +1 -0
  205. package/dist/internal/target-compat.js +77 -0
  206. package/dist/internal/target-compat.js.map +1 -0
  207. package/dist/internal/timing-safe-equal.d.ts +6 -0
  208. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  209. package/dist/internal/timing-safe-equal.js +10 -0
  210. package/dist/internal/timing-safe-equal.js.map +1 -1
  211. package/dist/internal/vitals-runtime.d.ts +18 -0
  212. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  213. package/dist/internal/vitals-runtime.js +38 -0
  214. package/dist/internal/vitals-runtime.js.map +1 -0
  215. package/dist/internal/web-app.d.ts +56 -8
  216. package/dist/internal/web-app.d.ts.map +1 -1
  217. package/dist/internal/web-app.js +215 -772
  218. package/dist/internal/web-app.js.map +1 -1
  219. package/dist/internal/zone-graph.d.ts +89 -0
  220. package/dist/internal/zone-graph.d.ts.map +1 -0
  221. package/dist/internal/zone-graph.js +492 -0
  222. package/dist/internal/zone-graph.js.map +1 -0
  223. package/dist/isr.d.ts +83 -6
  224. package/dist/isr.d.ts.map +1 -1
  225. package/dist/isr.js +301 -39
  226. package/dist/isr.js.map +1 -1
  227. package/dist/llms-txt.d.ts +3 -0
  228. package/dist/llms-txt.d.ts.map +1 -1
  229. package/dist/llms-txt.js +37 -9
  230. package/dist/llms-txt.js.map +1 -1
  231. package/dist/manifest.d.ts +221 -14
  232. package/dist/manifest.d.ts.map +1 -1
  233. package/dist/manifest.js +386 -9
  234. package/dist/manifest.js.map +1 -1
  235. package/dist/module-graph.d.ts +26 -5
  236. package/dist/module-graph.d.ts.map +1 -1
  237. package/dist/module-graph.js +17 -7
  238. package/dist/module-graph.js.map +1 -1
  239. package/dist/navigation.d.ts +17 -3
  240. package/dist/navigation.d.ts.map +1 -1
  241. package/dist/navigation.js +11 -5
  242. package/dist/navigation.js.map +1 -1
  243. package/dist/nonce.d.ts +47 -0
  244. package/dist/nonce.d.ts.map +1 -0
  245. package/dist/nonce.js +63 -0
  246. package/dist/nonce.js.map +1 -0
  247. package/dist/plugins/css-modules.d.ts.map +1 -1
  248. package/dist/plugins/css-modules.js +4 -3
  249. package/dist/plugins/css-modules.js.map +1 -1
  250. package/dist/plugins/kit.d.ts +9 -0
  251. package/dist/plugins/kit.d.ts.map +1 -1
  252. package/dist/plugins/kit.js +18 -0
  253. package/dist/plugins/kit.js.map +1 -1
  254. package/dist/plugins/svg.d.ts +22 -3
  255. package/dist/plugins/svg.d.ts.map +1 -1
  256. package/dist/plugins/svg.js +112 -1
  257. package/dist/plugins/svg.js.map +1 -1
  258. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  259. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  260. package/dist/plugins/vite-leak-guard.js +378 -17
  261. package/dist/plugins/vite-leak-guard.js.map +1 -1
  262. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  263. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  264. package/dist/plugins/vite-zone-guard.js +208 -0
  265. package/dist/plugins/vite-zone-guard.js.map +1 -0
  266. package/dist/plugins/zone-guard.d.ts +27 -0
  267. package/dist/plugins/zone-guard.d.ts.map +1 -0
  268. package/dist/plugins/zone-guard.js +72 -0
  269. package/dist/plugins/zone-guard.js.map +1 -0
  270. package/dist/prerender.d.ts +10 -0
  271. package/dist/prerender.d.ts.map +1 -1
  272. package/dist/prerender.js +35 -3
  273. package/dist/prerender.js.map +1 -1
  274. package/dist/public-dir.d.ts +9 -0
  275. package/dist/public-dir.d.ts.map +1 -1
  276. package/dist/public-dir.js +131 -48
  277. package/dist/public-dir.js.map +1 -1
  278. package/dist/pwa-manifest.d.ts +68 -0
  279. package/dist/pwa-manifest.d.ts.map +1 -0
  280. package/dist/pwa-manifest.js +178 -0
  281. package/dist/pwa-manifest.js.map +1 -0
  282. package/dist/query.js +4 -4
  283. package/dist/query.js.map +1 -1
  284. package/dist/render-seam.d.ts +42 -2
  285. package/dist/render-seam.d.ts.map +1 -1
  286. package/dist/render-seam.js +6 -0
  287. package/dist/render-seam.js.map +1 -1
  288. package/dist/route-manifest.d.ts +3 -0
  289. package/dist/route-manifest.d.ts.map +1 -1
  290. package/dist/route-manifest.js +26 -3
  291. package/dist/route-manifest.js.map +1 -1
  292. package/dist/route-types.d.ts +24 -0
  293. package/dist/route-types.d.ts.map +1 -0
  294. package/dist/route-types.js +195 -0
  295. package/dist/route-types.js.map +1 -0
  296. package/dist/router.d.ts +28 -6
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +129 -40
  299. package/dist/router.js.map +1 -1
  300. package/dist/trusted-html.d.ts +34 -0
  301. package/dist/trusted-html.d.ts.map +1 -0
  302. package/dist/trusted-html.js +35 -0
  303. package/dist/trusted-html.js.map +1 -0
  304. package/dist/vitals.d.ts +33 -0
  305. package/dist/vitals.d.ts.map +1 -0
  306. package/dist/vitals.js +16 -0
  307. package/dist/vitals.js.map +1 -0
  308. package/dist/vite.d.ts +16 -1
  309. package/dist/vite.d.ts.map +1 -1
  310. package/dist/vite.js +187 -36
  311. package/dist/vite.js.map +1 -1
  312. package/dist/zones.d.ts +70 -0
  313. package/dist/zones.d.ts.map +1 -0
  314. package/dist/zones.js +365 -0
  315. package/dist/zones.js.map +1 -0
  316. package/package.json +67 -14
  317. package/src/backend-only.ts +16 -0
  318. package/src/boundary.ts +18 -5
  319. package/src/build-plan.ts +113 -36
  320. package/src/build-vite.ts +124 -15
  321. package/src/build.ts +535 -102
  322. package/src/bun-dev-entry.ts +1 -1
  323. package/src/cdn.ts +216 -0
  324. package/src/client.ts +251 -18
  325. package/src/conformance.ts +16 -3
  326. package/src/csp.ts +194 -0
  327. package/src/css-contract.ts +44 -0
  328. package/src/dev-client.ts +349 -0
  329. package/src/dev-diagnostics.ts +60 -7
  330. package/src/dev-error.ts +49 -5
  331. package/src/dev-feed.ts +1140 -0
  332. package/src/dev-indicator.ts +113 -0
  333. package/src/dev-session.ts +729 -0
  334. package/src/dev-sourcemap.ts +281 -0
  335. package/src/dev-ssr-graph.ts +16 -5
  336. package/src/dev.ts +86 -43
  337. package/src/diagnostic-catalog.ts +243 -0
  338. package/src/diagnostic-prompt.ts +355 -0
  339. package/src/diagnostic.ts +63 -66
  340. package/src/draft.ts +4 -1
  341. package/src/fn.ts +19 -3
  342. package/src/fs.ts +13 -2
  343. package/src/index.ts +57 -10
  344. package/src/internal/cdn/cloudflare.ts +146 -0
  345. package/src/internal/cdn/core.ts +316 -0
  346. package/src/internal/cdn/fastly.ts +62 -0
  347. package/src/internal/cdn/vercel.ts +95 -0
  348. package/src/internal/code-unit-order.ts +5 -0
  349. package/src/internal/codegen.ts +180 -29
  350. package/src/internal/dev-csp.ts +176 -0
  351. package/src/internal/dev-reserved.ts +52 -0
  352. package/src/internal/html-spans.ts +50 -0
  353. package/src/internal/loading-runtime.ts +244 -0
  354. package/src/internal/matches-runtime.ts +62 -0
  355. package/src/internal/mount-shadow.ts +88 -0
  356. package/src/internal/node-builtins.ts +79 -0
  357. package/src/internal/output-guard.ts +701 -0
  358. package/src/internal/page-execution.ts +1371 -0
  359. package/src/internal/parity.ts +150 -36
  360. package/src/internal/private-env.ts +334 -0
  361. package/src/internal/public-url.ts +22 -0
  362. package/src/internal/render-document.ts +153 -73
  363. package/src/internal/request-url.ts +14 -0
  364. package/src/internal/response-controls.ts +284 -0
  365. package/src/internal/runtime-contract.ts +10 -15
  366. package/src/internal/secret-scan.ts +623 -0
  367. package/src/internal/server-boundary.ts +6 -69
  368. package/src/internal/source-map.ts +256 -0
  369. package/src/internal/target-compat.ts +92 -0
  370. package/src/internal/timing-safe-equal.ts +13 -0
  371. package/src/internal/vitals-runtime.ts +53 -0
  372. package/src/internal/web-app.ts +290 -879
  373. package/src/internal/zone-graph.ts +593 -0
  374. package/src/isr.ts +366 -39
  375. package/src/llms-txt.ts +44 -8
  376. package/src/manifest.ts +671 -17
  377. package/src/module-graph.ts +53 -9
  378. package/src/navigation.ts +29 -6
  379. package/src/nonce.ts +104 -0
  380. package/src/plugins/css-modules.ts +5 -3
  381. package/src/plugins/kit.ts +24 -0
  382. package/src/plugins/svg.ts +135 -4
  383. package/src/plugins/vite-leak-guard.ts +529 -16
  384. package/src/plugins/vite-zone-guard.ts +263 -0
  385. package/src/plugins/zone-guard.ts +110 -0
  386. package/src/prerender.ts +49 -3
  387. package/src/public-dir.ts +135 -49
  388. package/src/pwa-manifest.ts +226 -0
  389. package/src/query.ts +4 -4
  390. package/src/render-seam.ts +46 -1
  391. package/src/route-manifest.ts +15 -3
  392. package/src/route-types.ts +264 -0
  393. package/src/router.ts +163 -63
  394. package/src/trusted-html.ts +44 -0
  395. package/src/vitals.ts +44 -0
  396. package/src/vite.ts +207 -36
  397. package/src/zones.ts +430 -0
  398. package/dist/plugins/vite-server-only.d.ts +0 -49
  399. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  400. package/dist/plugins/vite-server-only.js +0 -57
  401. package/dist/plugins/vite-server-only.js.map +0 -1
  402. package/dist/server-only.d.ts +0 -21
  403. package/dist/server-only.d.ts.map +0 -1
  404. package/dist/server-only.js +0 -25
  405. package/dist/server-only.js.map +0 -1
  406. package/src/plugins/vite-server-only.ts +0 -75
  407. package/src/server-only.ts +0 -25
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Maps a browser stack trace back to source files, using the source maps the dev server itself
3
+ * serves (Bun links `/_bun/client/<gen>.js.map`; Vite inlines a data URL per module).
4
+ *
5
+ * Every fetch goes to the dev server's own origin, whatever host a frame names, so a crafted frame
6
+ * cannot make the server request anything else. Browser stacks in any engine's format come out as
7
+ * V8 `at` frames, which is what {@link parseFrames} reads.
8
+ */
9
+
10
+ import { isAbsolute, resolve } from "node:path"
11
+ import { fileURLToPath } from "node:url"
12
+ import {
13
+ decodeMappings,
14
+ inlineSourceMap,
15
+ lookupPosition,
16
+ type MappingLine,
17
+ originalPosition,
18
+ type RawSourceMap,
19
+ } from "./internal/source-map.ts"
20
+
21
+ export interface SourceMapperOptions {
22
+ readonly root: string
23
+ /** The dev server's own origin (`http://127.0.0.1:<port>`), or undefined before it listens. */
24
+ readonly origin: () => string | undefined
25
+ readonly fetch?: (url: string, init: { signal: AbortSignal }) => Promise<Response>
26
+ /** Decoded maps kept, most recently used first. */
27
+ readonly maxMaps?: number
28
+ }
29
+
30
+ export interface SourceMapper {
31
+ /** `stack` with each frame of a script this server served pointed at its source file. */
32
+ mapStack(stack: string): Promise<string>
33
+ /** Forget every map: the code they describe changed. */
34
+ clear(): void
35
+ }
36
+
37
+ interface Frame {
38
+ readonly name: string | undefined
39
+ readonly url: string
40
+ readonly line: number
41
+ readonly column: number
42
+ }
43
+
44
+ // V8: ` at fn (url:1:2)` / ` at url:1:2` / ` at async fn (url:1:2)`.
45
+ const V8_FRAME = /^\s*at\s+(?:(.*?)\s+\()?(.+?):(\d+):(\d+)\)?\s*$/
46
+ // SpiderMonkey / JavaScriptCore: `fn@url:1:2` / `@url:1:2`.
47
+ const AT_FRAME = /^\s*(.*?)@(.+?):(\d+):(\d+)\s*$/
48
+ const MAX_FRAMES = 50
49
+ const SCRIPT_PATH = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|mdx)$/i
50
+ const MAX_SCRIPT_BYTES = 64 * 1024 * 1024
51
+ const FETCH_TIMEOUT_MS = 5_000
52
+
53
+ function parseFrame(line: string): Frame | undefined {
54
+ const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line)
55
+ if (match === null) return undefined
56
+ const [, name, url = "", lineText = "", columnText = ""] = match
57
+ return {
58
+ name: name === undefined || name === "" ? undefined : name.replace(/^async\s+/, ""),
59
+ url,
60
+ line: Number(lineText),
61
+ column: Number(columnText),
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------------------------------
66
+ // Lookup
67
+ // ---------------------------------------------------------------------------------------------------
68
+
69
+ export { decodeMappings } from "./internal/source-map.ts"
70
+
71
+ interface DecodedMap {
72
+ readonly sources: readonly string[]
73
+ readonly lines: readonly (MappingLine | undefined)[]
74
+ readonly sourcesContent: readonly unknown[]
75
+ /** Per source, the map its content carries inline (a plugin's compile map), read on first use. */
76
+ readonly inner: Map<number, RawSourceMap | undefined>
77
+ }
78
+
79
+ /**
80
+ * The source position for a 0-based generated line/column. When the source is a plugin's output that
81
+ * carries its own map inline (a compiled `.svelte` or `.vue` file), that map is followed too: Bun's
82
+ * bundler keeps the plugin's output as the source, not the file it compiled.
83
+ */
84
+ function lookup(
85
+ map: DecodedMap,
86
+ line: number,
87
+ column: number,
88
+ ): { source: string; line: number; column: number } | undefined {
89
+ const found = lookupPosition(map.lines, line, column)
90
+ if (found === undefined) return undefined
91
+ const source = map.sources[found.source]
92
+ if (source === undefined) return undefined
93
+ if (!map.inner.has(found.source)) {
94
+ const content = map.sourcesContent[found.source]
95
+ map.inner.set(found.source, typeof content === "string" ? inlineSourceMap(content) : undefined)
96
+ }
97
+ const inner = map.inner.get(found.source)
98
+ const original =
99
+ inner === undefined ? undefined : originalPosition(inner, found.line, found.column)
100
+ return original === undefined
101
+ ? { source, line: found.line, column: found.column }
102
+ : { source, line: original.line, column: original.column }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------------------------------
106
+ // Fetching
107
+ // ---------------------------------------------------------------------------------------------------
108
+
109
+ function isRecord(value: unknown): value is Record<string, unknown> {
110
+ return typeof value === "object" && value !== null && !Array.isArray(value)
111
+ }
112
+
113
+ const SOURCE_MAPPING_URL = /[#@]\s*sourceMappingURL=([^\s'"]+)\s*$/
114
+
115
+ function sourceMappingUrl(script: string): string | undefined {
116
+ // The comment is the last one in the file; a data URL can be megabytes, so look from the end.
117
+ const at = script.lastIndexOf("sourceMappingURL=")
118
+ if (at === -1) return undefined
119
+ const lineStart = script.lastIndexOf("\n", at) + 1
120
+ return SOURCE_MAPPING_URL.exec(script.slice(lineStart))?.[1]
121
+ }
122
+
123
+ function decodeDataUrl(url: string): string | undefined {
124
+ const comma = url.indexOf(",")
125
+ if (comma === -1) return undefined
126
+ const meta = url.slice(5, comma)
127
+ const payload = url.slice(comma + 1)
128
+ return meta.endsWith(";base64")
129
+ ? Buffer.from(payload, "base64").toString("utf8")
130
+ : decodeURIComponent(payload)
131
+ }
132
+
133
+ /**
134
+ * The directory a served script lives in on disk. Vite serves files outside the root as
135
+ * `/@fs/<absolute path>`; everything else maps onto the project root.
136
+ */
137
+ function scriptDirectory(scriptPath: string, root: string): string {
138
+ const dir = decodeURIComponent(scriptPath.replace(/[^/]*$/, ""))
139
+ // `/@fs/C:/x/` names `C:/x/` on Windows: the slash before a drive letter is URL syntax only.
140
+ return dir.startsWith("/@fs/")
141
+ ? dir.slice("/@fs".length).replace(/^\/([a-zA-Z]:)/, "$1")
142
+ : resolve(root, `.${dir}`)
143
+ }
144
+
145
+ /** The filesystem path a map's source names, when it is a file. */
146
+ function sourcePath(source: string, sourceRoot: string, scriptPath: string, root: string): string {
147
+ const joined = sourceRoot === "" ? source : `${sourceRoot.replace(/\/?$/, "/")}${source}`
148
+ if (joined.startsWith("file://")) {
149
+ try {
150
+ return fileURLToPath(joined)
151
+ } catch {
152
+ return joined
153
+ }
154
+ }
155
+ if (/^[a-z][a-z\d+.-]*:/i.test(joined)) return joined
156
+ if (isAbsolute(joined)) return joined
157
+ // Relative to the script's own location, as a map's sources always are.
158
+ return resolve(scriptDirectory(scriptPath, root), joined)
159
+ }
160
+
161
+ export function createSourceMapper(options: SourceMapperOptions): SourceMapper {
162
+ const root = resolve(options.root)
163
+ const fetchImpl = options.fetch ?? ((url, init) => fetch(url, init))
164
+ const maxMaps = options.maxMaps ?? 8
165
+ const maps = new Map<string, Promise<DecodedMap | undefined>>()
166
+
167
+ const fetchText = async (url: string): Promise<string | undefined> => {
168
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) })
169
+ if (!response.ok) return undefined
170
+ const length = Number(response.headers.get("content-length") ?? "0")
171
+ if (length > MAX_SCRIPT_BYTES) return undefined
172
+ const text = await response.text()
173
+ return text.length > MAX_SCRIPT_BYTES ? undefined : text
174
+ }
175
+
176
+ const load = async (origin: string, pathAndQuery: string): Promise<DecodedMap | undefined> => {
177
+ const scriptUrl = new URL(pathAndQuery, origin)
178
+ const script = await fetchText(scriptUrl.href)
179
+ if (script === undefined) return undefined
180
+ const reference = sourceMappingUrl(script)
181
+ if (reference === undefined) return undefined
182
+ let text: string | undefined
183
+ if (reference.startsWith("data:")) text = decodeDataUrl(reference)
184
+ else {
185
+ const mapUrl = new URL(reference, scriptUrl)
186
+ // Same server only: the map's URL comes from a script body, and a body is data.
187
+ if (mapUrl.origin !== scriptUrl.origin) return undefined
188
+ text = await fetchText(mapUrl.href)
189
+ }
190
+ if (text === undefined) return undefined
191
+ const raw: unknown = JSON.parse(text)
192
+ if (!isRecord(raw) || typeof raw.mappings !== "string" || !Array.isArray(raw.sources))
193
+ return undefined
194
+ const sourceRoot = typeof raw.sourceRoot === "string" ? raw.sourceRoot : ""
195
+ return {
196
+ sources: raw.sources.map((source) =>
197
+ typeof source === "string" ? sourcePath(source, sourceRoot, scriptUrl.pathname, root) : "",
198
+ ),
199
+ lines: decodeMappings(raw.mappings),
200
+ sourcesContent: Array.isArray(raw.sourcesContent) ? raw.sourcesContent : [],
201
+ inner: new Map(),
202
+ }
203
+ }
204
+
205
+ const mapFor = (origin: string, pathAndQuery: string): Promise<DecodedMap | undefined> => {
206
+ const cached = maps.get(pathAndQuery)
207
+ if (cached !== undefined) {
208
+ maps.delete(pathAndQuery)
209
+ maps.set(pathAndQuery, cached)
210
+ return cached
211
+ }
212
+ const loading = load(origin, pathAndQuery).catch(() => undefined)
213
+ maps.set(pathAndQuery, loading)
214
+ while (maps.size > maxMaps) {
215
+ const oldest = maps.keys().next().value
216
+ if (oldest === undefined) break
217
+ maps.delete(oldest)
218
+ }
219
+ return loading
220
+ }
221
+
222
+ /** The script's path on this server, when a frame names this server at all. */
223
+ const ownPath = (url: string, origin: string): string | undefined => {
224
+ let parsed: URL
225
+ try {
226
+ parsed = new URL(url)
227
+ } catch {
228
+ return undefined
229
+ }
230
+ const own = new URL(origin)
231
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined
232
+ // `localhost:<port>` and `127.0.0.1:<port>` both reach this server; another port is another server.
233
+ if (parsed.port !== own.port) return undefined
234
+ const host = parsed.hostname
235
+ if (
236
+ host !== "localhost" &&
237
+ host !== "127.0.0.1" &&
238
+ host !== "[::1]" &&
239
+ !host.endsWith(".localhost")
240
+ )
241
+ return undefined
242
+ // Scripts only: fetching a page URL (an inline script's frame) would render it, loaders and all.
243
+ if (!SCRIPT_PATH.test(parsed.pathname)) return undefined
244
+ return `${parsed.pathname}${parsed.search}`
245
+ }
246
+
247
+ return {
248
+ async mapStack(stack) {
249
+ const origin = options.origin()
250
+ const lines = stack.split("\n")
251
+ const out: string[] = []
252
+ let frames = 0
253
+ for (const line of lines) {
254
+ const frame = parseFrame(line)
255
+ if (frame === undefined) {
256
+ // The message block (and engine noise between frames) stays as it was.
257
+ if (frames === 0) out.push(line)
258
+ continue
259
+ }
260
+ if (frames >= MAX_FRAMES) break
261
+ frames += 1
262
+ let location = `${frame.url}:${frame.line}:${frame.column}`
263
+ const path = origin === undefined ? undefined : ownPath(frame.url, origin)
264
+ if (origin !== undefined && path !== undefined) {
265
+ const map = await mapFor(origin, path)
266
+ const position =
267
+ map === undefined ? undefined : lookup(map, frame.line - 1, frame.column - 1)
268
+ if (position !== undefined && position.source !== "")
269
+ location = `${position.source}:${position.line}:${position.column}`
270
+ }
271
+ out.push(
272
+ frame.name === undefined ? ` at ${location}` : ` at ${frame.name} (${location})`,
273
+ )
274
+ }
275
+ return out.join("\n")
276
+ },
277
+ clear() {
278
+ maps.clear()
279
+ },
280
+ }
281
+ }
@@ -4,7 +4,7 @@
4
4
  * Bun's runtime caches every module it has evaluated, and nothing invalidates that cache on a file
5
5
  * change. The dev server's answer used to stop at the route module: `discoverRoutes({ importQuery })`
6
6
  * appends a changing query to each route's own dynamic import, so the route re-evaluates. Its imports
7
- * do not - a component, a helper, a `*.server` module all keep resolving to the same cached specifier,
7
+ * do not - a component, a helper, a backend module all keep resolving to the same cached specifier,
8
8
  * so SSR kept rendering the code that was on disk when the server started while the client, which Bun
9
9
  * rebuilds properly, rendered the edit. Every framework on this pipeline had it; the Vite pipeline does
10
10
  * not, because Vite owns its own SSR module graph.
@@ -46,8 +46,9 @@
46
46
  *
47
47
  * So the version is applied one level earlier, in the importer's own source: on load, each app-owned
48
48
  * module has its relative specifiers rewritten to absolute paths carrying the current version of the
49
- * file they point at. `onResolve` stays as well, because it is the only hook that sees modules loaded by
50
- * a third-party plugin this one does not wrap.
49
+ * file they point at - an alias such as `@/components/Button` too, resolved the way Bun resolves it.
50
+ * `onResolve` stays as well, because it is the only hook that sees modules loaded by a third-party plugin
51
+ * this one does not wrap.
51
52
  *
52
53
  * Old versions stay in Bun's registry - a bounded dev-only leak, one entry per edit, which is what the
53
54
  * route-level query already did.
@@ -104,6 +105,14 @@ const appSourceFilter = (root: string): RegExp => {
104
105
  */
105
106
  const SPECIFIER = /\b(from|import|require)((?:\s*\()?\s*)(["'])([^"'\n]+)\3/g
106
107
 
108
+ /**
109
+ * A specifier that can name an app file: relative, absolute, or an alias an npm package name cannot be
110
+ * (`@/x`, `~/x`, `#x`, `$lib/x`). A bare package name is left alone - resolving one here would pin it
111
+ * for the life of the dev server, and a package is never app source anyway. An alias shaped like a
112
+ * package (`@app/x`, `src/x`) is not recognized.
113
+ */
114
+ const APP_SPECIFIER = /^(?:[^a-z0-9@]|@[^a-z0-9])/i
115
+
107
116
  export interface SsrGraphOptions {
108
117
  /** The app root. Only files under it (and outside `node_modules`) are tracked. */
109
118
  readonly root: string
@@ -262,7 +271,9 @@ export function createSsrGraph(options: SsrGraphOptions): SsrGraph {
262
271
  // through untouched rather than adding a second, worse one.
263
272
  return contents
264
273
  }
265
- const candidates = new Set(scanned.map((entry) => entry.path).filter((p) => p.startsWith(".")))
274
+ const candidates = new Set(
275
+ scanned.map((entry) => entry.path).filter((path) => APP_SPECIFIER.test(path)),
276
+ )
266
277
  if (candidates.size === 0) return contents
267
278
  const keys = new Map<string, string>()
268
279
  for (const specifier of candidates) {
@@ -289,7 +300,7 @@ export function createSsrGraph(options: SsrGraphOptions): SsrGraph {
289
300
  })
290
301
  // The resolver half covers what the loader half cannot see: a module compiled by a plugin that
291
302
  // does not call `rewriteSsrImports`. Extension-bearing specifiers only, which is all Bun offers.
292
- build.onResolve({ filter: /^[./]/ }, (args) => {
303
+ build.onResolve({ filter: APP_SPECIFIER }, (args) => {
293
304
  if (resolving) return undefined
294
305
  // A specifier that already carries a query is either this module's own rewrite or a plugin's
295
306
  // virtual sub-request (`?vue-css` and friends) - both are already keyed.
package/src/dev.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * not, and saving it does a clean full reload. Plus no Vite dependency and ONE bundler across dev and
15
15
  * production, which is the real prize - the dev/prod seam disappears.
16
16
  *
17
- * CSS Modules, server functions, and `*.server` modules use the same production transforms through the
17
+ * CSS Modules, server functions, and the zone guard use the same production transforms through the
18
18
  * generated Bun config. The CLI owns that config because Bun's HTML dev server accepts plugins only from
19
19
  * `[serve.static] plugins`; direct callers should pass the equivalent plugin through their Bun config.
20
20
  *
@@ -39,7 +39,7 @@
39
39
  *
40
40
  * SSR invalidation is Bun's import cache rather than Vite's module graph, so route modules are re-imported
41
41
  * under a changing query on each change - which is what `discoverRoutes({ importQuery })` exists for. That
42
- * query stops at the route file, so everything BELOW it - components, helpers, `*.server` modules - is
42
+ * query stops at the route file, so everything BELOW it - components, helpers, backend modules - is
43
43
  * tracked and re-keyed per module by `./dev-ssr-graph.ts`, or SSR would render the code that was on disk
44
44
  * when the server started.
45
45
  *
@@ -57,14 +57,16 @@ import {
57
57
  import { dirname, relative, resolve } from "node:path"
58
58
  import { type BuildClientOptions, buildClient } from "./build.ts"
59
59
  import { type DevEntryMatch, resolveDevEntry } from "./bun-dev-entry.ts"
60
- import { createDevDiagnostics } from "./dev-diagnostics.ts"
61
60
  import { explainBindFailure } from "./dev-port.ts"
61
+ import { createDevSession, type DevAppHooks, type DevSession } from "./dev-session.ts"
62
62
  import { createSsrGraph, type SsrGraph } from "./dev-ssr-graph.ts"
63
63
  import { discoverRoutes } from "./fs.ts"
64
64
  import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
65
+ import { DEV_ENTRY_FILE } from "./internal/dev-reserved.ts"
65
66
  import { DEV_HMR_ENV, DEV_ROOT_ENV, DEV_ROUTES_ENV } from "./plugins/kit.ts"
66
67
  import { servePublicDir } from "./public-dir.ts"
67
68
 
69
+ export type { DevAppHooks } from "./dev-session.ts"
68
70
  export { LAST_ERROR_PATH } from "./diagnostic.ts"
69
71
 
70
72
  /** Minimal app surface the dev server needs - `createWebApp(...)` satisfies it. */
@@ -76,9 +78,14 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
76
78
  /**
77
79
  * Build the nifra app for the current client entry. `importQuery` changes on every reload - pass it to
78
80
  * `discoverRoutes(routesDir, { importQuery })` so SSR re-imports edited route modules instead of Bun's
79
- * cached copies.
81
+ * cached copies. Pass `dev.onLoaderError` to `createWebApp` so failures an `_error` boundary renders
82
+ * still reach the dev feed (`nifra errors`).
80
83
  */
81
- readonly createApp: (clientEntry: string, importQuery: string) => FetchApp | Promise<FetchApp>
84
+ readonly createApp: (
85
+ clientEntry: string,
86
+ importQuery: string,
87
+ dev: DevAppHooks,
88
+ ) => FetchApp | Promise<FetchApp>
82
89
  /** Directories to watch (default: `[routesDir]`). */
83
90
  readonly watch?: readonly string[]
84
91
  /** Port to listen on (default {@link DEFAULT_DEV_PORT}). */
@@ -95,6 +102,16 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
95
102
  * pass runs in the background off the hot path, so HMR is never waiting on it, and only reports.
96
103
  */
97
104
  readonly guardLeaks?: boolean
105
+ /**
106
+ * Write `.nifra/dev-server.json` (how `nifra errors`/`nifra logs` and the MCP tools find this server)
107
+ * and the persisted dev log beside it (default `true`).
108
+ */
109
+ readonly record?: boolean
110
+ /**
111
+ * Show the browser errors a dev page reports in a badge on that page, with a Copy prompt button per
112
+ * fix (default `true`). Off, the errors still reach the feed.
113
+ */
114
+ readonly indicator?: boolean
98
115
  }
99
116
 
100
117
  export interface DevServer {
@@ -293,10 +310,17 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
293
310
  // (`devHotComponent`). Announced here rather than passed, for the same reason the flag above is.
294
311
  process.env[DEV_ROOT_ENV] = root
295
312
  process.env[DEV_ROUTES_ENV] = resolve(routesDir)
296
- // The most recent SSR failure as a structured Diagnostic, scoped to THIS server. Served at
297
- // LAST_ERROR_PATH so an agent driving the dev server reads the exact failure (code, codeframe, fix) as
298
- // JSON instead of scraping the overlay. Shared with the Vite adapter so the endpoint can't drift.
299
- const devDiagnostics = createDevDiagnostics(root)
313
+ // Everything this server sees, for the agent driving it: failures (with the overlay's Diagnostic and
314
+ // LAST_ERROR_PATH riding on the same capture), console output, request traces and the discovery
315
+ // record. Shared with the Vite adapter so the two servers report identically.
316
+ const session = createDevSession({
317
+ root,
318
+ pipeline: "bun",
319
+ publicEnvPrefix: options.publicEnvPrefix,
320
+ record: options.record,
321
+ indicator: options.indicator,
322
+ })
323
+ const devHooks: DevAppHooks = { onLoaderError: session.onLoaderError }
300
324
  // SSR freshness BELOW the route module. The route-level `importQuery` only ever reloaded the route
301
325
  // itself; everything it imports is tracked here and re-keyed when it changes. Registered now because a
302
326
  // Bun runtime plugin only affects modules loaded after it, and the first route import is `appFor`
@@ -307,7 +331,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
307
331
  const ssrGraph = createSsrGraph({ root })
308
332
  ;(await import("bun")).plugin(ssrGraph.plugin)
309
333
  const devDir = resolve(root, DEV_DIR)
310
- const entryPath = resolve(devDir, "entry.tsx")
334
+ const entryPath = resolve(devDir, DEV_ENTRY_FILE)
311
335
  const htmlPath = resolve(devDir, "entry.html")
312
336
  const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
313
337
  // Route dev's `public/` through the SAME handler production uses. Dev previously inherited this
@@ -378,15 +402,17 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
378
402
  * client leaks. It is no longer what keeps SSR correct.
379
403
  *
380
404
  * The entry hash is not the whole marker, though, because it only covers the CLIENT graph. A module
381
- * the browser never receives - a `*.server` file, a loader's helper - can change without moving it, so
405
+ * the browser never receives - a backend module, a loader's helper - can change without moving it, so
382
406
  * the key also carries {@link SsrGraph} generation, which counts changes on the SERVER side. Either
383
407
  * one moving rebuilds the app, which is what re-imports the route modules under a fresh query.
384
408
  */
385
409
  const appFor = (key: string): Promise<FetchApp> => {
386
410
  if (built?.key === key) return Promise.resolve(built.app)
387
411
  if (building?.key === key) return building.promise
412
+ // A new key means the code moved since the last build: what the feed recorded before is stale.
413
+ if (version > 0) session.markChange()
388
414
  version += 1
389
- const promise = Promise.resolve(createApp(CLIENT_ENTRY_PATH, `v=${version}`))
415
+ const promise = Promise.resolve(createApp(CLIENT_ENTRY_PATH, `v=${version}`, devHooks))
390
416
  building = { key, promise }
391
417
  void promise
392
418
  .then((next) => {
@@ -408,6 +434,37 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
408
434
  return appFor(`${entrySrc}#${ssrGraph.generation()}`)
409
435
  }
410
436
 
437
+ const renderApp = async (req: Request, url: URL): Promise<Response> => {
438
+ try {
439
+ // One fresh probe per request: it is both the freshness check for SSR and the stylesheet list,
440
+ // so the page cannot be rendered against a build the browser is not about to load.
441
+ const entry = await currentEntry(true)
442
+ const res = await (await appForRequest(entry.src)).fetch(req)
443
+ if (!(res.headers.get("content-type") ?? "").includes("text/html")) return res
444
+ if (entry.styles.length === 0) return session.decoratePage(req, res)
445
+ const headers = new Headers(res.headers)
446
+ headers.delete("content-length") // the body grows with the injected stylesheet links
447
+ return session.decoratePage(
448
+ req,
449
+ new Response(injectStyles(await res.text(), entry.styles), {
450
+ status: res.status,
451
+ headers,
452
+ }),
453
+ )
454
+ } catch (err) {
455
+ // One Diagnostic drives every surface: the overlay returned here, the JSON at LAST_ERROR_PATH and
456
+ // the feed's `ssr` entry.
457
+ const html = session.failure(err, {
458
+ method: req.method,
459
+ url: `${url.pathname}${url.search}`,
460
+ })
461
+ return new Response(html, {
462
+ status: 500,
463
+ headers: { "content-type": "text/html; charset=utf-8" },
464
+ })
465
+ }
466
+ }
467
+
411
468
  try {
412
469
  server = serve({
413
470
  port,
@@ -427,11 +484,9 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
427
484
  const entry = await currentEntry()
428
485
  return new Response(null, { status: 307, headers: { location: entry.src } })
429
486
  }
430
- if (devDiagnostics.isLastErrorPath(url.pathname)) {
431
- // The structured form of the overlay, for an agent driving the dev server (shared surface).
432
- const { body, headers } = devDiagnostics.lastError()
433
- return new Response(body, { headers })
434
- }
487
+ // The agent endpoints (errors, logs, requests, identity, last-error): shared surface.
488
+ const agent = await session.handle(req)
489
+ if (agent !== undefined) return agent
435
490
  // Static probe before routing; a miss returns undefined and falls through, so no route is
436
491
  // shadowed. Every GET/HEAD pays the probe, including page renders and API routes: the handler
437
492
  // serves extension-less files too (an ACME challenge token is the reason), so there is no
@@ -439,33 +494,11 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
439
494
  // mounting this handler itself can pass `files` to answer a miss without touching the disk.
440
495
  const staticFile = await servePublic(req)
441
496
  if (staticFile !== undefined) return staticFile
442
- try {
443
- // One fresh probe per request: it is both the freshness check for SSR and the stylesheet list,
444
- // so the page cannot be rendered against a build the browser is not about to load.
445
- const entry = await currentEntry(true)
446
- const res = await (await appForRequest(entry.src)).fetch(req)
447
- if (!(res.headers.get("content-type") ?? "").includes("text/html")) return res
448
- if (entry.styles.length === 0) return res
449
- const headers = new Headers(res.headers)
450
- headers.delete("content-length") // the body grows with the injected stylesheet links
451
- return new Response(injectStyles(await res.text(), entry.styles), {
452
- status: res.status,
453
- headers,
454
- })
455
- } catch (err) {
456
- // One Diagnostic drives both surfaces: the overlay returned here and the JSON at LAST_ERROR_PATH.
457
- const html = devDiagnostics.capture(err, {
458
- method: req.method,
459
- url: `${url.pathname}${url.search}`,
460
- })
461
- return new Response(html, {
462
- status: 500,
463
- headers: { "content-type": "text/html; charset=utf-8" },
464
- })
465
- }
497
+ return session.track(req, () => renderApp(req, url))
466
498
  },
467
499
  })
468
500
  } catch (err) {
501
+ session.stop()
469
502
  // `Bun.serve` throws synchronously on a bind failure; the Vite path's equivalent arrives as an async
470
503
  // `error` event. Same explanation either way - see ./dev-port.ts for why it is worth spelling out.
471
504
  throw explainBindFailure(err, port)
@@ -479,10 +512,12 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
479
512
  // Leaving the server up would answer 500s forever, which presents as a running server rather than as
480
513
  // the startup failure it is.
481
514
  server.stop(true)
515
+ session.stop()
482
516
  throw err
483
517
  }
518
+ session.listening(server.port ?? port)
484
519
 
485
- const guard = options.guardLeaks !== false ? leakGuard(options) : undefined
520
+ const guard = options.guardLeaks !== false ? leakGuard(options, session) : undefined
486
521
  guard?.()
487
522
 
488
523
  // What is left for the watcher, now that request ordering keeps SSR fresh: regenerate the client entry
@@ -494,11 +529,16 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
494
529
  const onChange = (): void => {
495
530
  if (timer) clearTimeout(timer)
496
531
  timer = setTimeout(() => {
532
+ // A deletion its own watcher reported is unwatched now, or the next topology poll reports it
533
+ // again as a second change: another regeneration and another leak-guard build.
534
+ syncWatchedFiles(false)
535
+ session.markChange()
497
536
  try {
498
537
  writeDevFiles({ routesDir, clientModule, entryPath, htmlPath })
499
538
  } catch (err) {
500
539
  // A half-saved file can fail the scan mid-edit; the next change re-runs this.
501
540
  console.error("[nifra/web/dev] client entry regeneration failed:", err)
541
+ session.buildFailed(err, "client entry regeneration failed")
502
542
  }
503
543
  guard?.()
504
544
  }, 60)
@@ -552,6 +592,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
552
592
  for (const file of watched) unwatchFile(file)
553
593
  ssrGraph.dispose()
554
594
  server.stop(true)
595
+ session.stop()
555
596
  rmSync(devDir, { recursive: true, force: true })
556
597
  // Put the dev-phase env flags back the way this server found them (see `priorDevEnv`).
557
598
  for (const [key, prior] of Object.entries(priorDevEnv)) {
@@ -585,7 +626,7 @@ export function buildFailureDetail(err: unknown): string {
585
626
  : ` ${err instanceof Error ? err.message : String(err)}`
586
627
  }
587
628
 
588
- function leakGuard(options: DevServerOptions): () => void {
629
+ function leakGuard(options: DevServerOptions, session: DevSession): () => void {
589
630
  let running = false
590
631
  let queued = false
591
632
  const run = (): void => {
@@ -595,11 +636,13 @@ function leakGuard(options: DevServerOptions): () => void {
595
636
  }
596
637
  running = true
597
638
  buildClient({ ...options, minify: false })
639
+ .then(() => session.buildPassed())
598
640
  .catch((err: unknown) => {
599
641
  console.error(
600
642
  `\n[nifra/web/dev] client-leak guard failed:\n${buildFailureDetail(err)}\n` +
601
643
  " The dev server is still running. This will fail `nifra build`.\n",
602
644
  )
645
+ session.buildFailed(err, "client build failed (this will fail `nifra build`)")
603
646
  })
604
647
  .finally(() => {
605
648
  running = false