@nifrajs/web 3.5.0 → 4.0.2

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 (390) 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 +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  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 +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +136 -35
  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/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +503 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +394 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +80 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +248 -43
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +135 -36
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +638 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +682 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +119 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +257 -42
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
package/src/vite.ts CHANGED
@@ -10,17 +10,20 @@
10
10
  * injects its HMR client + the framework's refresh preamble. Node `http` (not `Bun.serve`) because
11
11
  * Vite's `middlewares` are Connect-style - it runs fine under Bun.
12
12
  */
13
- import { writeFileSync } from "node:fs"
13
+ import { type Dirent, readdirSync, writeFileSync } from "node:fs"
14
14
  import {
15
15
  createServer as createHttpServer,
16
16
  type IncomingMessage,
17
17
  type ServerResponse,
18
18
  } from "node:http"
19
- import { isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path"
20
- import { createDevDiagnostics } from "./dev-diagnostics.ts"
19
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path"
20
+ import { CLIENT_BATCH_MAX_BYTES } from "./dev-client.ts"
21
+ import { DEV_REQUEST_ID_HEADER, DEV_SERVER_FILES } from "./dev-feed.ts"
21
22
  import { listenOrExplain } from "./dev-port.ts"
23
+ import { createDevSession, type DevAppHooks, isDevAgentPath } from "./dev-session.ts"
22
24
  import { discoverRoutes } from "./fs.ts"
23
25
  import { DEFAULT_DEV_PORT, generateClientEntry, setSsrModuleLoader } from "./index.ts"
26
+ import { admitViteTags } from "./internal/dev-csp.ts"
24
27
  import { viteDedupePackages } from "./internal/identity-policy.ts"
25
28
  import {
26
29
  assertIdentityParity,
@@ -39,7 +42,7 @@ import {
39
42
  reproduciblePath,
40
43
  } from "./plugins/kit.ts"
41
44
  import { viteServerFnStub } from "./plugins/vite-server-fn.ts"
42
- import { viteServerOnlyEmpty } from "./plugins/vite-server-only.ts"
45
+ import { viteZoneGuard } from "./plugins/vite-zone-guard.ts"
43
46
 
44
47
  /** Minimal app surface - `createWebApp(...)` satisfies it. */
45
48
  interface FetchApp {
@@ -62,13 +65,20 @@ interface ViteModuleGraph {
62
65
  interface ViteLike {
63
66
  /** Load a module through VITE's graph - the seam that makes the Vite pipeline own SSR too. */
64
67
  ssrLoadModule(url: string): Promise<Record<string, unknown>>
65
- readonly middlewares: (req: IncomingMessage, res: ServerResponse, next: () => void) => void
68
+ readonly middlewares: (
69
+ req: IncomingMessage,
70
+ res: ServerResponse,
71
+ next: (error?: unknown) => void,
72
+ ) => void
66
73
  transformIndexHtml(url: string, html: string): Promise<string>
67
74
  ssrFixStacktrace(err: Error): void
68
75
  /** The module graph, used to invalidate a changed file AND its transitive importers before reload. */
69
76
  readonly moduleGraph?: ViteModuleGraph
70
77
  readonly watcher: {
71
78
  on(event: "change" | "add" | "unlink", cb: (path: string) => void): void
79
+ on(event: "ready", cb: () => void): void
80
+ /** Chokidar's ready state, used to handle a ready event emitted before this adapter attaches. */
81
+ readonly _readyEmitted?: boolean
72
82
  }
73
83
  close(): Promise<void>
74
84
  }
@@ -94,10 +104,14 @@ export interface ViteDevServerOptions {
94
104
  * Without it SSR resolves through Bun while the client resolves through Vite - two resolvers, one
95
105
  * process - which is what makes `resolve.dedupe` fail to reach SSR and produces the dual-React
96
106
  * crash. Vite re-evaluates on change, so no `importQuery` cache-buster is needed alongside it.
107
+ *
108
+ * Pass `dev.onLoaderError` to `createWebApp` so failures an `_error` boundary renders still reach the
109
+ * dev feed (`nifra errors`).
97
110
  */
98
111
  readonly createApp: (
99
112
  clientEntry: string,
100
113
  load: (absolutePath: string) => Promise<unknown>,
114
+ dev: DevAppHooks,
101
115
  ) => FetchApp | Promise<FetchApp>
102
116
  /** Vite plugins - inject your framework's official plugin, e.g. `[react()]`. */
103
117
  readonly plugins?: readonly unknown[]
@@ -133,6 +147,16 @@ export interface ViteDevServerOptions {
133
147
  * resolve it; `nifra build` never honors it. Wired to `nifra dev --allow-duplicate-identity`.
134
148
  */
135
149
  readonly allowDuplicateIdentity?: boolean
150
+ /**
151
+ * Write `.nifra/dev-server.json` (how `nifra errors`/`nifra logs` and the MCP tools find this server)
152
+ * and the persisted dev log beside it (default `true`).
153
+ */
154
+ readonly record?: boolean
155
+ /**
156
+ * Show the browser errors a dev page reports in a badge on that page, with a Copy prompt button per
157
+ * fix (default `true`). Off, the errors still reach the feed.
158
+ */
159
+ readonly indicator?: boolean
136
160
  }
137
161
 
138
162
  export interface ViteDevServer {
@@ -140,10 +164,13 @@ export interface ViteDevServer {
140
164
  stop(): Promise<void>
141
165
  }
142
166
 
167
+ export type { DevAppHooks } from "./dev-session.ts"
143
168
  export { LAST_ERROR_PATH } from "./diagnostic.ts"
144
169
 
145
170
  // The codegen'd client entry is written here (at the Vite root) so Vite serves + HMRs it.
146
171
  const DEV_ENTRY = ".nifra-vite-entry.tsx"
172
+ const VITE_CLOSE_BOUND_MS = 2000
173
+ const POLL_INTERVAL_MS = 80
147
174
 
148
175
  /** Candidate spellings for Vite's module-graph map. Vite has used native and slash-normalized
149
176
  * absolute Windows paths across versions, while watcher events can additionally arrive as a file URL. */
@@ -158,6 +185,22 @@ const pathInside = (root: string, path: string): boolean => {
158
185
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
159
186
  }
160
187
 
188
+ /** Every file below `dir` as an absolute path. A directory removed mid-walk contributes nothing. */
189
+ const filesUnder = (dir: string, into = new Set<string>()): Set<string> => {
190
+ let entries: Dirent[]
191
+ try {
192
+ entries = readdirSync(dir, { withFileTypes: true })
193
+ } catch {
194
+ return into
195
+ }
196
+ for (const entry of entries) {
197
+ const path = join(dir, entry.name)
198
+ if (entry.isDirectory()) filesUnder(path, into)
199
+ else into.add(path)
200
+ }
201
+ return into
202
+ }
203
+
161
204
  const readNodeBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {
162
205
  if (req.method === "GET" || req.method === "HEAD") return undefined
163
206
  const chunks: Buffer[] = []
@@ -165,6 +208,23 @@ const readNodeBody = async (req: IncomingMessage): Promise<Buffer | undefined> =
165
208
  return Buffer.concat(chunks)
166
209
  }
167
210
 
211
+ /**
212
+ * An agent request's body, never buffered past `max` bytes. A larger body is drained, not kept, so the
213
+ * client still gets an answer: the session refuses the truncated batch.
214
+ */
215
+ const readAgentBody = async (req: IncomingMessage, max: number): Promise<Buffer | undefined> => {
216
+ if (req.method === "GET" || req.method === "HEAD") return undefined
217
+ const chunks: Buffer[] = []
218
+ let total = 0
219
+ for await (const chunk of req) {
220
+ if (!Buffer.isBuffer(chunk)) continue
221
+ total += chunk.length
222
+ if (total <= max) chunks.push(chunk)
223
+ }
224
+ // One byte past the cap is enough for the session's own size check to refuse it.
225
+ return total > max ? Buffer.alloc(max + 1) : Buffer.concat(chunks)
226
+ }
227
+
168
228
  /** Build a Web `Request` from a Node `IncomingMessage` (+ already-read body) for nifra's `app.fetch`. */
169
229
  function toWebRequest(req: IncomingMessage, body: Buffer | undefined): Request {
170
230
  const headers = new Headers()
@@ -372,6 +432,9 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
372
432
  generateClientEntry(manifest, { clientModule: options.clientModule, resolve: toUrl }),
373
433
  )
374
434
  }
435
+ const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
436
+ // Listed before the entry is generated, so a file added in between is reconciled again, not lost.
437
+ const routeFiles = usePolling ? filesUnder(routesDir) : new Set<string>()
375
438
  writeClientEntry()
376
439
  const entryUrl = `/${DEV_ENTRY}`
377
440
 
@@ -382,27 +445,101 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
382
445
  // `vite`, which is assigned just below before the server starts listening.
383
446
  let vite!: ViteLike
384
447
  let app: FetchApp
385
- // The most recent SSR failure, served as JSON at LAST_ERROR_PATH. Shared with the Bun adapter so the
386
- // endpoint and its headers can't drift between the two dev servers.
387
- const devDiagnostics = createDevDiagnostics(root)
448
+ // Failures, console output, request traces and the discovery record for the agent driving this
449
+ // server, plus the overlay and LAST_ERROR_PATH. Shared with the Bun adapter so they cannot drift.
450
+ const session = createDevSession({
451
+ root,
452
+ pipeline: "vite",
453
+ publicEnvPrefix: options.publicEnvPrefix,
454
+ record: options.record,
455
+ indicator: options.indicator,
456
+ })
457
+ const devHooks: DevAppHooks = { onLoaderError: session.onLoaderError }
458
+
459
+ // Refuses backend code before Vite serves or transforms it; held here so a refused module's request
460
+ // is answered with the refusal rather than falling through to the app.
461
+ const zoneGuard = viteZoneGuard({
462
+ appRoot: dirname(routesDir),
463
+ routesDir,
464
+ generatedFiles: [resolvePath(root, DEV_ENTRY)],
465
+ ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
466
+ })
388
467
 
389
468
  const server: NodeHttpServer = createHttpServer((req, res) => {
390
- // Keep this before Vite's middleware: it is an agent endpoint owned by nifra, not a file or app route.
391
- if (devDiagnostics.isLastErrorPath((req.url ?? "/").split("?", 1)[0] ?? "/")) {
392
- const { body, headers } = devDiagnostics.lastError()
393
- res.statusCode = 200
394
- for (const [key, value] of Object.entries(headers)) res.setHeader(key, value)
395
- res.end(body)
469
+ // Keep this before Vite's middleware: the agent endpoints are owned by nifra, not files or routes.
470
+ if (isDevAgentPath((req.url ?? "/").split("?", 1)[0] ?? "/")) {
471
+ void (async () => {
472
+ try {
473
+ const body = await readAgentBody(req, CLIENT_BATCH_MAX_BYTES)
474
+ const agent = await session.handle(toWebRequest(req, body))
475
+ if (agent === undefined) {
476
+ handleWithVite(req, res)
477
+ return
478
+ }
479
+ res.statusCode = agent.status
480
+ applyResponseHeaders(agent.headers, res)
481
+ res.end(await agent.text())
482
+ } catch (err) {
483
+ // A client that aborts mid-body rejects the read; unhandled, that rejection ends the process.
484
+ if (!req.destroyed) console.error("[nifra/web/vite] dev endpoint failed:", err)
485
+ if (!res.headersSent) res.statusCode = 500
486
+ res.end()
487
+ }
488
+ })()
396
489
  return
397
490
  }
398
- vite.middlewares(req, res, () => {
491
+ handleWithVite(req, res)
492
+ })
493
+
494
+ function handleWithVite(req: IncomingMessage, res: ServerResponse): void {
495
+ vite.middlewares(req, res, (error) => {
496
+ // In middleware mode Vite reports a module's transform error to the overlay and passes the
497
+ // request on. A request naming a source file was for a module, never a page: it gets the error,
498
+ // not the app's HTML.
499
+ const url = req.url ?? "/"
500
+ const file = /\.[\w]+(?:[?#]|$)/.test(url) ? zoneGuard.fileForUrl(url) : undefined
501
+ if (error !== undefined || file !== undefined) {
502
+ res.statusCode = 500
503
+ res.setHeader("content-type", "text/plain; charset=utf-8")
504
+ res.setHeader("cache-control", "no-store")
505
+ const reason =
506
+ zoneGuard.refusalFor(url) ??
507
+ (error instanceof Error
508
+ ? error.message
509
+ : `[nifra] ${url} failed to transform; the dev server log has the error`)
510
+ session.buildFailed(error ?? new Error(reason), `${url} failed to transform`)
511
+ res.end(`${reason}\n`)
512
+ return
513
+ }
399
514
  // Not a Vite asset → nifra SSR. (`next` runs after Vite declines, so the body is still readable.)
400
515
  void (async () => {
401
516
  try {
402
517
  const body = await readNodeBody(req)
403
- const nifraRes = await app.fetch(toWebRequest(req, body))
518
+ const webRequest = toWebRequest(req, body)
519
+ let overlay = false
520
+ const nifraRes = await session.track(webRequest, async () => {
521
+ try {
522
+ return await app.fetch(webRequest)
523
+ } catch (err) {
524
+ // Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
525
+ // the readable dev overlay instead of a bare text dump. Dev-only - production maps to `_error`.
526
+ if (err instanceof Error) vite.ssrFixStacktrace(err)
527
+ overlay = true
528
+ return new Response(
529
+ session.failure(err, { method: req.method ?? "GET", url: req.url ?? "/" }),
530
+ { status: 500, headers: { "content-type": "text/html; charset=utf-8" } },
531
+ )
532
+ }
533
+ })
404
534
  const contentType = nifraRes.headers.get("content-type") ?? ""
405
535
  res.statusCode = nifraRes.status
536
+ const requestId = nifraRes.headers.get(DEV_REQUEST_ID_HEADER)
537
+ if (requestId !== null) res.setHeader(DEV_REQUEST_ID_HEADER, requestId)
538
+ if (overlay || session.isOverlay(nifraRes)) {
539
+ res.setHeader("content-type", "text/html; charset=utf-8")
540
+ res.end(await nifraRes.text())
541
+ return
542
+ }
406
543
  if (!contentType.includes("text/html")) {
407
544
  // Data / redirect / asset response - pass through untouched, streamed (SSE-safe). Set-Cookie is
408
545
  // emitted per-header so multi-cookie responses (e.g. better-auth sessions) aren't collapsed.
@@ -410,25 +547,26 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
410
547
  await pipeWebBodyToNode(nifraRes.body, res)
411
548
  return
412
549
  }
413
- // Inject Vite's HMR client + the framework's refresh preamble into the SSR'd HTML.
414
- const html = await vite.transformIndexHtml(req.url ?? "/", await nifraRes.text())
550
+ // Inject Vite's HMR client + the framework's refresh preamble into the SSR'd HTML, keeping the
551
+ // app's headers (cookies, CSP, cache-control) as the Bun pipeline does.
552
+ const headers = new Headers(nifraRes.headers)
553
+ headers.delete("content-length") // the body grows with Vite's tags
554
+ const page = session.decorateHtml(webRequest, await nifraRes.text(), headers, requestId)
555
+ const html = admitViteTags(await vite.transformIndexHtml(req.url ?? "/", page), headers)
556
+ applyResponseHeaders(headers, res)
415
557
  res.setHeader("content-type", "text/html; charset=utf-8")
416
558
  res.end(html)
417
559
  } catch (err) {
418
- // Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
419
- // the readable dev overlay instead of a bare text dump. Dev-only - production maps to `_error`.
560
+ // A failure past the app (reading the body, Vite's HTML transform): same overlay, same feed.
420
561
  if (err instanceof Error) vite.ssrFixStacktrace(err)
421
- const html = devDiagnostics.capture(err, {
422
- method: req.method ?? "GET",
423
- url: req.url ?? "/",
424
- })
562
+ const html = session.failure(err, { method: req.method ?? "GET", url: req.url ?? "/" })
425
563
  res.statusCode = 500
426
564
  res.setHeader("content-type", "text/html; charset=utf-8")
427
565
  res.end(html)
428
566
  }
429
567
  })()
430
568
  })
431
- })
569
+ }
432
570
 
433
571
  // `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
434
572
  // their TS **source** - so the dev server needs no prior `dist` build of the adapter packages.
@@ -460,7 +598,16 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
460
598
  // path-shaped `clientModule` (tests) names no package; there is then nothing to externalize.
461
599
  const adapterPackage = packageNameOf(options.clientModule)
462
600
  const ssrExternal = [...(adapterPackage !== undefined ? [adapterPackage] : []), "@nifrajs/web"]
463
- const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
601
+ // The feed writes its record and log under `.nifra/`: watching them would turn every log flush into
602
+ // a "file change" that re-creates the app and marks every entry stale.
603
+ const watch: { ignored: RegExp[]; usePolling?: boolean; interval?: number } = {
604
+ ignored: [DEV_SERVER_FILES],
605
+ }
606
+ // Poll when native fs events aren't delivered (containers/sandboxes).
607
+ if (usePolling) {
608
+ watch.usePolling = true
609
+ watch.interval = POLL_INTERVAL_MS
610
+ }
464
611
  // Never `import("vite")` directly here: the guard in `importVite` has to run first, and the dev
465
612
  // server importing vite unguarded is precisely what poisoned the module for the whole process.
466
613
  const viteModule = await importVite<ViteModule>()
@@ -480,12 +627,11 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
480
627
  server: {
481
628
  middlewareMode: true,
482
629
  hmr: { server },
483
- // Explicit watch config; poll when native fs events aren't delivered (containers/sandboxes).
484
- watch: usePolling ? { usePolling: true, interval: 80 } : {},
630
+ watch,
485
631
  },
486
- // Ahead of the user's plugins: a `*.fn` module must be replaced before anything else
487
- // reads it, and the dev server is a client bundler like any other.
488
- plugins: [viteServerFnStub(), viteServerOnlyEmpty(), ...plugins],
632
+ // Ahead of the user's plugins: the zone guard refuses backend code before anything serves or
633
+ // transforms it, and a `*.fn` module must be replaced before anything else reads it.
634
+ plugins: [zoneGuard, viteServerFnStub(), ...plugins],
489
635
  resolve: {
490
636
  conditions: resolveConditions,
491
637
  // Dedupe each framework runtime to ONE copy. In a multi-root workspace a shared package can pull
@@ -523,6 +669,22 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
523
669
  },
524
670
  ...(options.define ? { define: options.define } : {}),
525
671
  })
672
+ // Vite resolves before chokidar necessarily finishes its initial scan. Do not expose the
673
+ // dev server until that scan is complete: a route created immediately after startup must be
674
+ // observed as a change, not folded into the ignored initial snapshot on slower filesystems.
675
+ const watcherReady = new Promise<void>((resolve) => {
676
+ let settled = false
677
+ const markReady = (): void => {
678
+ if (settled) return
679
+ settled = true
680
+ resolve()
681
+ }
682
+ vite.watcher.on("ready", markReady)
683
+ // createServer() can return after chokidar has emitted `ready`, especially on fast local
684
+ // filesystems. Chokidar exposes this state in its structural type so we can cover that case
685
+ // without a startup timeout or a false-positive partial directory snapshot.
686
+ if (vite.watcher._readyEmitted === true) markReady()
687
+ })
526
688
 
527
689
  const ssrLoad = (absolutePath: string): Promise<unknown> => vite.ssrLoadModule(absolutePath)
528
690
  // Published so an ADAPTER can reach this graph too. Route modules already load through it; an
@@ -530,7 +692,7 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
530
692
  // `Chain.svelte`) would otherwise take it from the runtime, which has no compiler for it and no
531
693
  // reason to agree with Vite about which copy of the framework runtime it renders through.
532
694
  setSsrModuleLoader(ssrLoad)
533
- app = await options.createApp(entryUrl, ssrLoad)
695
+ app = await options.createApp(entryUrl, ssrLoad, devHooks)
534
696
 
535
697
  // Evict a changed file from the SSR graph together with every module that (transitively) imports it,
536
698
  // BEFORE re-creating the app. Vite re-evaluates a directly-changed module on its own, but a parent
@@ -566,20 +728,58 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
566
728
  const refreshApp = (path?: string): void => {
567
729
  if (path !== undefined) invalidateImporterClosure(path)
568
730
  const version = ++refreshVersion
569
- Promise.resolve(options.createApp(entryUrl, ssrLoad))
731
+ Promise.resolve(options.createApp(entryUrl, ssrLoad, devHooks))
570
732
  .then((next) => {
571
733
  if (version === refreshVersion) app = next
572
734
  })
573
- .catch((err) => console.error("[nifra/web/vite] app re-create failed:", err))
735
+ .catch((err) => {
736
+ console.error("[nifra/web/vite] app re-create failed:", err)
737
+ session.buildFailed(err, "app re-create failed")
738
+ })
739
+ }
740
+ vite.watcher.on("change", (path) => {
741
+ session.markChange()
742
+ refreshApp(normalizeFilePath(path))
743
+ })
744
+ const onAddUnlink = (event: "add" | "unlink", path: string): void => {
745
+ const filePath = normalizeFilePath(path)
746
+ const inRoutes = pathInside(routesDir, filePath)
747
+ if (inRoutes && usePolling) {
748
+ const file = resolvePath(filePath)
749
+ // Whichever of the watcher and the rescan below reports a route file first handles it.
750
+ if (routeFiles.has(file) === (event === "add")) return
751
+ if (event === "add") routeFiles.add(file)
752
+ else routeFiles.delete(file)
753
+ }
754
+ session.markChange()
755
+ if (inRoutes) {
756
+ writeClientEntry()
757
+ // A newly added route is not in Vite's module graph yet, so invalidating only the
758
+ // route path cannot evict the cached generated entry that imports the route. Evict the
759
+ // generated entry after rewriting it before recreating the SSR app; this also prevents
760
+ // an unlink refresh from briefly loading an entry that still references the deleted file.
761
+ invalidateImporterClosure(resolvePath(root, DEV_ENTRY))
762
+ }
763
+ refreshApp(filePath)
574
764
  }
575
- vite.watcher.on("change", (path) => refreshApp(normalizeFilePath(path)))
576
765
  for (const event of ["add", "unlink"] as const) {
577
- vite.watcher.on(event, (path) => {
578
- const filePath = normalizeFilePath(path)
579
- if (pathInside(routesDir, filePath)) writeClientEntry()
580
- refreshApp(filePath)
581
- })
766
+ vite.watcher.on(event, (path) => onAddUnlink(event, path))
767
+ }
768
+ // Chokidar reports ready before the poller has taken its baseline stat of a directory (Bun's
769
+ // fs.watchFile takes it asynchronously), and a route added or removed in that gap is folded into the
770
+ // baseline and never reported. The manifest comes from a directory scan, so a scan diffed against the
771
+ // route files already handled recovers those events.
772
+ const reconcileRouteFiles = (): void => {
773
+ const onDisk = filesUnder(routesDir)
774
+ try {
775
+ for (const file of onDisk) if (!routeFiles.has(file)) onAddUnlink("add", file)
776
+ for (const file of [...routeFiles]) if (!onDisk.has(file)) onAddUnlink("unlink", file)
777
+ } catch (err) {
778
+ console.error("[nifra/web/vite] route rescan failed:", err)
779
+ session.buildFailed(err, "route rescan failed")
780
+ }
582
781
  }
782
+ await watcherReady
583
783
 
584
784
  try {
585
785
  await listenOrExplain(server, port, "127.0.0.1")
@@ -589,21 +789,36 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
589
789
  // then HANGS on it, which is worse than the raw crash this guard replaced: the user sees a dev server
590
790
  // that appears to be starting. Tear Vite down so the failure is terminal.
591
791
  await vite.close().catch(() => {})
792
+ session.stop()
592
793
  throw err
593
794
  }
594
795
  // Report the port actually BOUND, not the one requested. They differ for `port: 0`, which is how you
595
796
  // ask the OS for a free one - the correct thing to do in a test or when running several apps at once.
596
797
  // Echoing the request back would return a literal 0, which connects to nothing.
597
798
  const address = server.address()
799
+ const boundPort = typeof address === "object" && address !== null ? address.port : port
800
+ session.listening(boundPort)
801
+ const routeRescan = usePolling ? setInterval(reconcileRouteFiles, POLL_INTERVAL_MS) : undefined
598
802
  return {
599
- port: typeof address === "object" && address !== null ? address.port : port,
803
+ port: boundPort,
600
804
  stop: async () => {
805
+ clearInterval(routeRescan)
806
+ session.stop()
601
807
  // Cleared with the server that owns it: the slot is process-global, so a stopped server leaving
602
808
  // its loader behind would hand the next one's adapter a graph that is closed (tests start and
603
809
  // stop several dev servers in one process).
604
810
  setSsrModuleLoader(undefined)
605
811
  server.close()
606
- await vite.close()
812
+ // Vite (8.2) never settles a first dependency optimization that close() cuts short, and close()
813
+ // waits on it for good. Everything else closes in parallel, so only that wait is bounded.
814
+ let bound: ReturnType<typeof setTimeout> | undefined
815
+ await Promise.race([
816
+ vite.close(),
817
+ new Promise<void>((resolve) => {
818
+ bound = setTimeout(resolve, VITE_CLOSE_BOUND_MS)
819
+ }),
820
+ ])
821
+ clearTimeout(bound)
607
822
  },
608
823
  }
609
824
  }