@nifrajs/web 4.0.0 → 5.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 (388) hide show
  1. package/README.md +11 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +49 -26
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-session.d.ts.map +1 -1
  26. package/dist/dev-session.js +4 -14
  27. package/dist/dev-session.js.map +1 -1
  28. package/dist/dev.d.ts +7 -17
  29. package/dist/dev.d.ts.map +1 -1
  30. package/dist/dev.js +13 -1
  31. package/dist/dev.js.map +1 -1
  32. package/dist/draft.d.ts +8 -5
  33. package/dist/draft.d.ts.map +1 -1
  34. package/dist/draft.js +30 -13
  35. package/dist/draft.js.map +1 -1
  36. package/dist/fonts-google.d.ts.map +1 -1
  37. package/dist/fonts-google.js +7 -0
  38. package/dist/fonts-google.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codegen.d.ts +1 -0
  44. package/dist/internal/codegen.d.ts.map +1 -1
  45. package/dist/internal/codegen.js +56 -29
  46. package/dist/internal/codegen.js.map +1 -1
  47. package/dist/internal/disposable-dir.d.ts +8 -0
  48. package/dist/internal/disposable-dir.d.ts.map +1 -0
  49. package/dist/internal/disposable-dir.js +25 -0
  50. package/dist/internal/disposable-dir.js.map +1 -0
  51. package/dist/internal/entry-chunks.d.ts +9 -0
  52. package/dist/internal/entry-chunks.d.ts.map +1 -0
  53. package/dist/internal/entry-chunks.js +15 -0
  54. package/dist/internal/entry-chunks.js.map +1 -0
  55. package/dist/internal/evidence-traversal.d.ts +12 -0
  56. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  57. package/dist/internal/evidence-traversal.js +48 -0
  58. package/dist/internal/evidence-traversal.js.map +1 -0
  59. package/dist/internal/head-merge.d.ts +6 -0
  60. package/dist/internal/head-merge.d.ts.map +1 -1
  61. package/dist/internal/head-merge.js +9 -0
  62. package/dist/internal/head-merge.js.map +1 -1
  63. package/dist/internal/js-string.d.ts +2 -0
  64. package/dist/internal/js-string.d.ts.map +1 -1
  65. package/dist/internal/js-string.js +5 -1
  66. package/dist/internal/js-string.js.map +1 -1
  67. package/dist/internal/mount-shadow.js +1 -1
  68. package/dist/internal/mount-shadow.js.map +1 -1
  69. package/dist/internal/output-guard.d.ts +7 -0
  70. package/dist/internal/output-guard.d.ts.map +1 -1
  71. package/dist/internal/output-guard.js +80 -8
  72. package/dist/internal/output-guard.js.map +1 -1
  73. package/dist/internal/page-execution.d.ts +6 -0
  74. package/dist/internal/page-execution.d.ts.map +1 -1
  75. package/dist/internal/page-execution.js +377 -108
  76. package/dist/internal/page-execution.js.map +1 -1
  77. package/dist/internal/progressive-cookies.d.ts +14 -0
  78. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  79. package/dist/internal/progressive-cookies.js +43 -0
  80. package/dist/internal/progressive-cookies.js.map +1 -0
  81. package/dist/internal/public-env.d.ts +2 -0
  82. package/dist/internal/public-env.d.ts.map +1 -0
  83. package/dist/internal/public-env.js +13 -0
  84. package/dist/internal/public-env.js.map +1 -0
  85. package/dist/internal/render-document.d.ts +1 -0
  86. package/dist/internal/render-document.d.ts.map +1 -1
  87. package/dist/internal/render-document.js +28 -5
  88. package/dist/internal/render-document.js.map +1 -1
  89. package/dist/internal/response-controls.d.ts +2 -0
  90. package/dist/internal/response-controls.d.ts.map +1 -1
  91. package/dist/internal/response-controls.js +26 -16
  92. package/dist/internal/response-controls.js.map +1 -1
  93. package/dist/internal/revive-dates.d.ts +16 -0
  94. package/dist/internal/revive-dates.d.ts.map +1 -0
  95. package/dist/internal/revive-dates.js +32 -0
  96. package/dist/internal/revive-dates.js.map +1 -0
  97. package/dist/internal/route-path.d.ts +8 -0
  98. package/dist/internal/route-path.d.ts.map +1 -0
  99. package/dist/internal/route-path.js +8 -0
  100. package/dist/internal/route-path.js.map +1 -0
  101. package/dist/internal/secret-scan.d.ts.map +1 -1
  102. package/dist/internal/secret-scan.js +105 -19
  103. package/dist/internal/secret-scan.js.map +1 -1
  104. package/dist/internal/server-boundary.d.ts.map +1 -1
  105. package/dist/internal/server-boundary.js +33 -5
  106. package/dist/internal/server-boundary.js.map +1 -1
  107. package/dist/internal/web-app.d.ts +21 -16
  108. package/dist/internal/web-app.d.ts.map +1 -1
  109. package/dist/internal/web-app.js +73 -21
  110. package/dist/internal/web-app.js.map +1 -1
  111. package/dist/internal/wire-date.d.ts +3 -0
  112. package/dist/internal/wire-date.d.ts.map +1 -0
  113. package/dist/internal/wire-date.js +14 -0
  114. package/dist/internal/wire-date.js.map +1 -0
  115. package/dist/internal/zone-graph.d.ts +4 -0
  116. package/dist/internal/zone-graph.d.ts.map +1 -1
  117. package/dist/internal/zone-graph.js +54 -14
  118. package/dist/internal/zone-graph.js.map +1 -1
  119. package/dist/isr.d.ts +58 -2
  120. package/dist/isr.d.ts.map +1 -1
  121. package/dist/isr.js +296 -39
  122. package/dist/isr.js.map +1 -1
  123. package/dist/llms-txt.d.ts +2 -0
  124. package/dist/llms-txt.d.ts.map +1 -1
  125. package/dist/llms-txt.js +29 -3
  126. package/dist/llms-txt.js.map +1 -1
  127. package/dist/manifest.d.ts +28 -4
  128. package/dist/manifest.d.ts.map +1 -1
  129. package/dist/manifest.js +14 -3
  130. package/dist/manifest.js.map +1 -1
  131. package/dist/open-resolved.d.ts +18 -0
  132. package/dist/open-resolved.d.ts.map +1 -0
  133. package/dist/open-resolved.js +62 -0
  134. package/dist/open-resolved.js.map +1 -0
  135. package/dist/plugins/kit.d.ts +1 -0
  136. package/dist/plugins/kit.d.ts.map +1 -1
  137. package/dist/plugins/kit.js +26 -11
  138. package/dist/plugins/kit.js.map +1 -1
  139. package/dist/plugins/vite-leak-guard.d.ts +3 -1
  140. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  141. package/dist/plugins/vite-leak-guard.js +4 -0
  142. package/dist/plugins/vite-leak-guard.js.map +1 -1
  143. package/dist/plugins/zone-guard.d.ts.map +1 -1
  144. package/dist/plugins/zone-guard.js +10 -2
  145. package/dist/plugins/zone-guard.js.map +1 -1
  146. package/dist/public-dir.d.ts +8 -1
  147. package/dist/public-dir.d.ts.map +1 -1
  148. package/dist/public-dir.js +17 -6
  149. package/dist/public-dir.js.map +1 -1
  150. package/dist/query.d.ts +13 -4
  151. package/dist/query.d.ts.map +1 -1
  152. package/dist/query.js +89 -26
  153. package/dist/query.js.map +1 -1
  154. package/dist/render-seam.d.ts +2 -0
  155. package/dist/render-seam.d.ts.map +1 -1
  156. package/dist/render-seam.js +1 -0
  157. package/dist/render-seam.js.map +1 -1
  158. package/dist/route-types.d.ts.map +1 -1
  159. package/dist/route-types.js +20 -9
  160. package/dist/route-types.js.map +1 -1
  161. package/dist/router.d.ts +9 -2
  162. package/dist/router.d.ts.map +1 -1
  163. package/dist/router.js +39 -46
  164. package/dist/router.js.map +1 -1
  165. package/dist/server-components/build-info.d.ts +3 -0
  166. package/dist/server-components/build-info.d.ts.map +1 -0
  167. package/dist/server-components/build-info.js +7 -0
  168. package/dist/server-components/build-info.js.map +1 -0
  169. package/dist/server-components/build-target.d.ts +12 -0
  170. package/dist/server-components/build-target.d.ts.map +1 -0
  171. package/dist/server-components/build-target.js +153 -0
  172. package/dist/server-components/build-target.js.map +1 -0
  173. package/dist/server-components/build.d.ts +24 -0
  174. package/dist/server-components/build.d.ts.map +1 -0
  175. package/dist/server-components/build.js +366 -0
  176. package/dist/server-components/build.js.map +1 -0
  177. package/dist/server-components/bundle-proof.d.ts +3 -0
  178. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  179. package/dist/server-components/bundle-proof.js +23 -0
  180. package/dist/server-components/bundle-proof.js.map +1 -0
  181. package/dist/server-components/client.d.ts +46 -0
  182. package/dist/server-components/client.d.ts.map +1 -0
  183. package/dist/server-components/client.js +594 -0
  184. package/dist/server-components/client.js.map +1 -0
  185. package/dist/server-components/definition.d.ts +11 -0
  186. package/dist/server-components/definition.d.ts.map +1 -0
  187. package/dist/server-components/definition.js +94 -0
  188. package/dist/server-components/definition.js.map +1 -0
  189. package/dist/server-components/dev-http.d.ts +5 -0
  190. package/dist/server-components/dev-http.d.ts.map +1 -0
  191. package/dist/server-components/dev-http.js +71 -0
  192. package/dist/server-components/dev-http.js.map +1 -0
  193. package/dist/server-components/error.d.ts +9 -0
  194. package/dist/server-components/error.d.ts.map +1 -0
  195. package/dist/server-components/error.js +24 -0
  196. package/dist/server-components/error.js.map +1 -0
  197. package/dist/server-components/frames.d.ts +7 -0
  198. package/dist/server-components/frames.d.ts.map +1 -0
  199. package/dist/server-components/frames.js +169 -0
  200. package/dist/server-components/frames.js.map +1 -0
  201. package/dist/server-components/html-policy.d.ts +6 -0
  202. package/dist/server-components/html-policy.d.ts.map +1 -0
  203. package/dist/server-components/html-policy.js +75 -0
  204. package/dist/server-components/html-policy.js.map +1 -0
  205. package/dist/server-components/html.d.ts +4 -0
  206. package/dist/server-components/html.d.ts.map +1 -0
  207. package/dist/server-components/html.js +67 -0
  208. package/dist/server-components/html.js.map +1 -0
  209. package/dist/server-components/islands.d.ts +44 -0
  210. package/dist/server-components/islands.d.ts.map +1 -0
  211. package/dist/server-components/islands.js +42 -0
  212. package/dist/server-components/islands.js.map +1 -0
  213. package/dist/server-components/native.d.ts +4 -0
  214. package/dist/server-components/native.d.ts.map +1 -0
  215. package/dist/server-components/native.js +81 -0
  216. package/dist/server-components/native.js.map +1 -0
  217. package/dist/server-components/portal.d.ts +6 -0
  218. package/dist/server-components/portal.d.ts.map +1 -0
  219. package/dist/server-components/portal.js +35 -0
  220. package/dist/server-components/portal.js.map +1 -0
  221. package/dist/server-components/props.d.ts +3 -0
  222. package/dist/server-components/props.d.ts.map +1 -0
  223. package/dist/server-components/props.js +14 -0
  224. package/dist/server-components/props.js.map +1 -0
  225. package/dist/server-components/protocol.d.ts +44 -0
  226. package/dist/server-components/protocol.d.ts.map +1 -0
  227. package/dist/server-components/protocol.js +139 -0
  228. package/dist/server-components/protocol.js.map +1 -0
  229. package/dist/server-components/range.d.ts +24 -0
  230. package/dist/server-components/range.d.ts.map +1 -0
  231. package/dist/server-components/range.js +263 -0
  232. package/dist/server-components/range.js.map +1 -0
  233. package/dist/server-components/registry.d.ts +6 -0
  234. package/dist/server-components/registry.d.ts.map +1 -0
  235. package/dist/server-components/registry.js +30 -0
  236. package/dist/server-components/registry.js.map +1 -0
  237. package/dist/server-components/response.d.ts +4 -0
  238. package/dist/server-components/response.d.ts.map +1 -0
  239. package/dist/server-components/response.js +32 -0
  240. package/dist/server-components/response.js.map +1 -0
  241. package/dist/server-components/router-integration.d.ts +9 -0
  242. package/dist/server-components/router-integration.d.ts.map +1 -0
  243. package/dist/server-components/router-integration.js +29 -0
  244. package/dist/server-components/router-integration.js.map +1 -0
  245. package/dist/server-components/runtime.d.ts +14 -0
  246. package/dist/server-components/runtime.d.ts.map +1 -0
  247. package/dist/server-components/runtime.js +276 -0
  248. package/dist/server-components/runtime.js.map +1 -0
  249. package/dist/server-components/scheduler.d.ts +14 -0
  250. package/dist/server-components/scheduler.d.ts.map +1 -0
  251. package/dist/server-components/scheduler.js +74 -0
  252. package/dist/server-components/scheduler.js.map +1 -0
  253. package/dist/server-components/schema.d.ts +4 -0
  254. package/dist/server-components/schema.d.ts.map +1 -0
  255. package/dist/server-components/schema.js +77 -0
  256. package/dist/server-components/schema.js.map +1 -0
  257. package/dist/server-components/server-entry.d.ts +12 -0
  258. package/dist/server-components/server-entry.d.ts.map +1 -0
  259. package/dist/server-components/server-entry.js +90 -0
  260. package/dist/server-components/server-entry.js.map +1 -0
  261. package/dist/server-components/source.d.ts +15 -0
  262. package/dist/server-components/source.d.ts.map +1 -0
  263. package/dist/server-components/source.js +269 -0
  264. package/dist/server-components/source.js.map +1 -0
  265. package/dist/server-components/state.d.ts +4 -0
  266. package/dist/server-components/state.d.ts.map +1 -0
  267. package/dist/server-components/state.js +123 -0
  268. package/dist/server-components/state.js.map +1 -0
  269. package/dist/server-components/stream.d.ts +6 -0
  270. package/dist/server-components/stream.d.ts.map +1 -0
  271. package/dist/server-components/stream.js +70 -0
  272. package/dist/server-components/stream.js.map +1 -0
  273. package/dist/server-components/trusted-html.d.ts +3 -0
  274. package/dist/server-components/trusted-html.d.ts.map +1 -0
  275. package/dist/server-components/trusted-html.js +14 -0
  276. package/dist/server-components/trusted-html.js.map +1 -0
  277. package/dist/server-components/types.d.ts +107 -0
  278. package/dist/server-components/types.d.ts.map +1 -0
  279. package/dist/server-components/types.js +2 -0
  280. package/dist/server-components/types.js.map +1 -0
  281. package/dist/server-components/typescript.d.ts +11 -0
  282. package/dist/server-components/typescript.d.ts.map +1 -0
  283. package/dist/server-components/typescript.js +28 -0
  284. package/dist/server-components/typescript.js.map +1 -0
  285. package/dist/server-components/value.d.ts +5 -0
  286. package/dist/server-components/value.d.ts.map +1 -0
  287. package/dist/server-components/value.js +77 -0
  288. package/dist/server-components/value.js.map +1 -0
  289. package/dist/server-components/vite.d.ts +12 -0
  290. package/dist/server-components/vite.d.ts.map +1 -0
  291. package/dist/server-components/vite.js +197 -0
  292. package/dist/server-components/vite.js.map +1 -0
  293. package/dist/server-components.d.ts +5 -0
  294. package/dist/server-components.d.ts.map +1 -0
  295. package/dist/server-components.js +4 -0
  296. package/dist/server-components.js.map +1 -0
  297. package/dist/service-worker.d.ts +8 -1
  298. package/dist/service-worker.d.ts.map +1 -1
  299. package/dist/service-worker.js +20 -6
  300. package/dist/service-worker.js.map +1 -1
  301. package/dist/vite.d.ts +12 -0
  302. package/dist/vite.d.ts.map +1 -1
  303. package/dist/vite.js +77 -17
  304. package/dist/vite.js.map +1 -1
  305. package/dist/zones.d.ts +40 -0
  306. package/dist/zones.d.ts.map +1 -1
  307. package/dist/zones.js +170 -6
  308. package/dist/zones.js.map +1 -1
  309. package/package.json +97 -4
  310. package/src/build-vite.ts +31 -9
  311. package/src/build.ts +109 -31
  312. package/src/cdn.ts +18 -12
  313. package/src/client.ts +48 -23
  314. package/src/deferred.ts +27 -9
  315. package/src/dev-feed.ts +24 -5
  316. package/src/dev-session.ts +4 -13
  317. package/src/dev.ts +29 -1
  318. package/src/draft.ts +35 -14
  319. package/src/fonts-google.ts +10 -0
  320. package/src/index.ts +3 -2
  321. package/src/internal/codegen.ts +60 -29
  322. package/src/internal/disposable-dir.ts +24 -0
  323. package/src/internal/entry-chunks.ts +18 -0
  324. package/src/internal/evidence-traversal.ts +57 -0
  325. package/src/internal/head-merge.ts +16 -0
  326. package/src/internal/js-string.ts +6 -1
  327. package/src/internal/mount-shadow.ts +1 -1
  328. package/src/internal/output-guard.ts +88 -7
  329. package/src/internal/page-execution.ts +506 -108
  330. package/src/internal/progressive-cookies.ts +60 -0
  331. package/src/internal/public-env.ts +14 -0
  332. package/src/internal/render-document.ts +26 -11
  333. package/src/internal/response-controls.ts +31 -14
  334. package/src/internal/revive-dates.ts +35 -0
  335. package/src/internal/route-path.ts +8 -0
  336. package/src/internal/secret-scan.ts +104 -17
  337. package/src/internal/server-boundary.ts +29 -5
  338. package/src/internal/web-app.ts +116 -37
  339. package/src/internal/wire-date.ts +16 -0
  340. package/src/internal/zone-graph.ts +59 -19
  341. package/src/isr.ts +351 -43
  342. package/src/llms-txt.ts +32 -3
  343. package/src/manifest.ts +43 -7
  344. package/src/open-resolved.ts +64 -0
  345. package/src/plugins/kit.ts +31 -11
  346. package/src/plugins/vite-leak-guard.ts +5 -1
  347. package/src/plugins/zone-guard.ts +11 -2
  348. package/src/public-dir.ts +27 -8
  349. package/src/query.ts +107 -26
  350. package/src/render-seam.ts +2 -0
  351. package/src/route-types.ts +34 -7
  352. package/src/router.ts +58 -55
  353. package/src/server-components/build-info.ts +11 -0
  354. package/src/server-components/build-target.ts +194 -0
  355. package/src/server-components/build.ts +419 -0
  356. package/src/server-components/bundle-proof.ts +33 -0
  357. package/src/server-components/client.ts +663 -0
  358. package/src/server-components/definition.ts +125 -0
  359. package/src/server-components/dev-http.ts +75 -0
  360. package/src/server-components/error.ts +24 -0
  361. package/src/server-components/frames.ts +181 -0
  362. package/src/server-components/html-policy.ts +94 -0
  363. package/src/server-components/html.ts +81 -0
  364. package/src/server-components/islands.ts +111 -0
  365. package/src/server-components/native.ts +101 -0
  366. package/src/server-components/portal.ts +42 -0
  367. package/src/server-components/props.ts +19 -0
  368. package/src/server-components/protocol.ts +167 -0
  369. package/src/server-components/range.ts +313 -0
  370. package/src/server-components/registry.ts +34 -0
  371. package/src/server-components/response.ts +44 -0
  372. package/src/server-components/router-integration.ts +40 -0
  373. package/src/server-components/runtime.ts +376 -0
  374. package/src/server-components/scheduler.ts +84 -0
  375. package/src/server-components/schema.ts +69 -0
  376. package/src/server-components/server-entry.ts +98 -0
  377. package/src/server-components/source.ts +324 -0
  378. package/src/server-components/state.ts +143 -0
  379. package/src/server-components/stream.ts +71 -0
  380. package/src/server-components/trusted-html.ts +21 -0
  381. package/src/server-components/types.ts +138 -0
  382. package/src/server-components/typescript.ts +39 -0
  383. package/src/server-components/value.ts +82 -0
  384. package/src/server-components/vite.ts +242 -0
  385. package/src/server-components.ts +32 -0
  386. package/src/service-worker.ts +24 -7
  387. package/src/vite.ts +68 -17
  388. package/src/zones.ts +208 -7
@@ -0,0 +1,39 @@
1
+ import { createRequire } from "node:module"
2
+ import type * as TS from "typescript"
3
+
4
+ /** The code of the error a TypeScript without the in-process compiler API raises here. */
5
+ export const UNSUPPORTED_TYPESCRIPT = "NIFRA_TS_UNSUPPORTED"
6
+
7
+ function isCompiler(value: unknown): value is typeof TS {
8
+ return (
9
+ typeof value === "object" &&
10
+ value !== null &&
11
+ "createSourceFile" in value &&
12
+ typeof value.createSourceFile === "function"
13
+ )
14
+ }
15
+
16
+ /**
17
+ * The TypeScript compiler API, resolved from `from` the way `import "typescript"` there would be.
18
+ * TypeScript 7's package ships no in-process compiler API (its root export is only `version`), so
19
+ * it raises an error that names the fix instead of a TypeError on first use.
20
+ */
21
+ export function loadTypeScript(from: string = import.meta.url): typeof TS {
22
+ const loaded: unknown = createRequire(from)("typescript")
23
+ if (isCompiler(loaded)) return loaded
24
+ const version =
25
+ typeof loaded === "object" &&
26
+ loaded !== null &&
27
+ "version" in loaded &&
28
+ typeof loaded.version === "string"
29
+ ? loaded.version
30
+ : "unknown"
31
+ throw Object.assign(
32
+ new Error(
33
+ `${UNSUPPORTED_TYPESCRIPT}: server components read component modules with the TypeScript compiler API, which TypeScript ${version} does not ship. Pin the project to TypeScript 6 (for example, \`bun add -d typescript@^6\`).`,
34
+ ),
35
+ { code: UNSUPPORTED_TYPESCRIPT, version },
36
+ )
37
+ }
38
+
39
+ export const ts: typeof TS = loadTypeScript()
@@ -0,0 +1,82 @@
1
+ import { ServerComponentError } from "./error.ts"
2
+ import type { ComponentValue } from "./types.ts"
3
+
4
+ /** Property order does not identify a resource; callers supply already-validated DTO inputs. */
5
+ export function componentInputKey(value: ComponentValue): string {
6
+ if (Array.isArray(value)) return `[${value.map(componentInputKey).join(",")}]`
7
+ if (typeof value === "object" && value !== null)
8
+ return `{${Object.keys(value)
9
+ .sort()
10
+ .map((key) => `${JSON.stringify(key)}:${componentInputKey(Reflect.get(value, key))}`)
11
+ .join(",")}}`
12
+ return JSON.stringify(value)
13
+ }
14
+
15
+ const encoder = new TextEncoder()
16
+ const forbidden = new Set(["__proto__", "prototype", "constructor"])
17
+
18
+ export function componentValue(value: unknown, maxBytes: number = 65536): ComponentValue {
19
+ const seen = new Set<object>()
20
+ let nodes = 0
21
+ let bytes = 0
22
+ const charge = (text: string): void => {
23
+ bytes += encoder.encode(text).byteLength
24
+ if (bytes > maxBytes) throw new ServerComponentError("component_limit", 413)
25
+ }
26
+ const visit = (item: unknown, depth: number): ComponentValue => {
27
+ if (++nodes > 8192 || depth > 16) throw new ServerComponentError("component_limit", 413)
28
+ if (
29
+ item === null ||
30
+ typeof item === "string" ||
31
+ typeof item === "boolean" ||
32
+ (typeof item === "number" && Number.isFinite(item))
33
+ ) {
34
+ charge(JSON.stringify(item))
35
+ return item
36
+ }
37
+ if (typeof item !== "object" || item === null || seen.has(item))
38
+ throw new ServerComponentError("component_contract")
39
+ const array = Array.isArray(item)
40
+ const prototype = Object.getPrototypeOf(item)
41
+ if (
42
+ array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null
43
+ ) {
44
+ throw new ServerComponentError("component_contract")
45
+ }
46
+ // Reject custom serializers without invoking them, including inherited serializers.
47
+ if ("toJSON" in item || Object.getOwnPropertySymbols(item).length > 0)
48
+ throw new ServerComponentError("component_contract")
49
+ const descriptors = Object.getOwnPropertyDescriptors(item)
50
+ const names = Object.keys(descriptors)
51
+ if (array && (item.length > 8192 || names.length !== item.length + 1))
52
+ throw new ServerComponentError("component_contract")
53
+ const result: ComponentValue[] | Record<string, ComponentValue> = array
54
+ ? []
55
+ : Object.create(null)
56
+ seen.add(item)
57
+ charge(array ? "[]" : "{}")
58
+ let count = 0
59
+ for (const name of names) {
60
+ if (array && name === "length") continue
61
+ const property = descriptors[name]!
62
+ if (
63
+ forbidden.has(name) ||
64
+ !property.enumerable ||
65
+ !Object.hasOwn(property, "value") ||
66
+ (array && (!/^(0|[1-9][0-9]*)$/.test(name) || Number(name) >= item.length))
67
+ ) {
68
+ throw new ServerComponentError("component_contract")
69
+ }
70
+ charge((count++ === 0 ? "" : ",") + (array ? "" : `${JSON.stringify(name)}:`))
71
+ Object.defineProperty(result, name, {
72
+ value: visit(property.value, depth + 1),
73
+ enumerable: true,
74
+ writable: true,
75
+ configurable: true,
76
+ })
77
+ }
78
+ seen.delete(item)
79
+ return result
80
+ }
81
+ return visit(value, 0)
82
+ }
@@ -0,0 +1,242 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { createRequire } from "node:module"
3
+ import { resolve } from "node:path"
4
+ import type * as TS from "typescript"
5
+ import { type Plugin, version as viteVersion } from "vite"
6
+ import {
7
+ compileComponentModule,
8
+ componentReference,
9
+ directive,
10
+ exportedValues,
11
+ guardCreateElementCalls,
12
+ sourceFile,
13
+ } from "./source.ts"
14
+ import { ts } from "./typescript.ts"
15
+
16
+ const componentFile = /\.component\.server\.[cm]?[jt]sx?$/
17
+ const islandFile = /\.island\.[cm]?[jt]sx?$/
18
+ const referencePrefix = "\0nifra-component-reference:"
19
+ const nativeQuery = "nifra-native-client-reference"
20
+ const reactRuntime = "\0nifra-component-react-runtime"
21
+ // Vite uses forward slashes for module IDs even when the filesystem uses backslashes.
22
+ const bare = (id: string): string => (id.split("?")[0] ?? id).replaceAll("\\", "/")
23
+
24
+ export interface ServerComponentsPluginOptions {
25
+ readonly root: string
26
+ readonly adapter: "react" | "vue" | "svelte" | "solid" | "preact"
27
+ }
28
+
29
+ function nativeFacade(source: string, file: string): string {
30
+ const prefix = file.replace(/\.[cm]?[jt]sx?$/, ".props.")
31
+ const extensions = /\.[cm]?jsx?$/.test(file)
32
+ ? ["js", "mjs", "cjs", "ts"]
33
+ : ["ts", "mts", "cts", "js"]
34
+ const companion =
35
+ extensions.map((extension) => prefix + extension).find(existsSync) ?? prefix + extensions[0]
36
+ if (!existsSync(companion))
37
+ throw new Error(
38
+ `[nifra/components] ${file} requires ${companion}: export a closed props schema for every native client component`,
39
+ )
40
+ const schemas = sourceFile(readFileSync(companion, "utf8"), companion)
41
+ const registered = new Set(exportedValues(schemas))
42
+ const slots = new Map<string, readonly string[]>()
43
+ for (const statement of schemas.statements) {
44
+ if (!ts.isVariableStatement(statement)) continue
45
+ for (const declaration of statement.declarationList.declarations) {
46
+ if (!ts.isIdentifier(declaration.name) || !declaration.name.text.endsWith("Slots")) continue
47
+ let value = declaration.initializer
48
+ if (value !== undefined && ts.isAsExpression(value)) value = value.expression
49
+ if (
50
+ value === undefined ||
51
+ !ts.isArrayLiteralExpression(value) ||
52
+ !value.elements.every(ts.isStringLiteral)
53
+ )
54
+ throw new Error("[nifra/components] Managed slot names must be a literal array")
55
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The compiler inspects trusted AST/package metadata and its own populated stub map; application data never supplies these values.
56
+ const names = value.elements.map((node) => (node as TS.StringLiteral).text)
57
+ if (
58
+ names.some(
59
+ (name) =>
60
+ !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name) ||
61
+ name === "ref" ||
62
+ name === "key" ||
63
+ name === "constructor",
64
+ )
65
+ )
66
+ throw new Error("[nifra/components] Invalid managed slot name")
67
+ slots.set(declaration.name.text.slice(0, -5), names)
68
+ }
69
+ }
70
+ const names = exportedValues(sourceFile(source, file))
71
+ const exports = names.map((name, index) => {
72
+ if (!registered.has(name))
73
+ throw new Error(
74
+ `[nifra/components] Native client export ${name} in ${file} has no registered props schema`,
75
+ )
76
+ if (name !== "default" && !/^[$A-Z_a-z][$\w]*$/.test(name))
77
+ throw new Error("[nifra/components] Native client export names must be identifiers")
78
+ const local = `__nifra_client_${index}`
79
+ const value = `__guard(__native[${JSON.stringify(name)}], __schemas[${JSON.stringify(name)}], ${JSON.stringify(slots.get(name) ?? [])})`
80
+ return name === "default"
81
+ ? `const ${local} = ${value}; export default ${local};`
82
+ : `export const ${name} = ${value};`
83
+ })
84
+ return [
85
+ 'import { guardedClientReference as __guard } from "@nifrajs/web-react/server-components/guard";',
86
+ `import * as __native from ${JSON.stringify(`${file}?${nativeQuery}`)};`,
87
+ `import * as __schemas from ${JSON.stringify(companion)};`,
88
+ ...exports,
89
+ ].join("\n")
90
+ }
91
+
92
+ /** Compiler bridge. The ordinary build must continue rejecting arbitrary server imports. */
93
+ export function serverComponentsPlugin(
94
+ options: ServerComponentsPluginOptions,
95
+ ): Plugin & { readonly api: { readonly componentModules: ReadonlySet<string> } } {
96
+ const stubs = new Map<string, string>()
97
+ const internal = new Set<string>()
98
+ const nativeReferences = new Set<string>()
99
+ const componentModules = new Set<string>()
100
+ return {
101
+ api: { componentModules },
102
+ name: "nifra:server-components",
103
+ enforce: "pre",
104
+ configResolved(config) {
105
+ if (viteVersion !== "8.2.1")
106
+ throw new Error(
107
+ "[nifra/components] Use the certified Vite 8.2.1 component pipeline; Bun remains a supported server runtime",
108
+ )
109
+ if (options.adapter === "react") {
110
+ const require = createRequire(resolve(config.root, "package.json"))
111
+ const versions = ["react", "react-dom", "react-server-dom-webpack"].map((name) => {
112
+ try {
113
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The compiler inspects trusted AST/package metadata and its own populated stub map; application data never supplies these values.
114
+ return (require(`${name}/package.json`) as { version: string }).version
115
+ } catch {
116
+ throw new Error(
117
+ `[nifra/components] Install ${name}@19.2.8 for the React component pipeline`,
118
+ )
119
+ }
120
+ })
121
+ if (versions.some((version) => version !== "19.2.8"))
122
+ throw new Error(
123
+ "[nifra/components] React, React DOM, and Flight must all use the matching 19.2.8 version",
124
+ )
125
+ }
126
+ },
127
+ async resolveId(source, importer) {
128
+ const environment = this.environment.name
129
+ if (source === "react" && environment === "rsc" && !internal.has(bare(importer ?? ""))) {
130
+ internal.add(reactRuntime)
131
+ const guard = await this.resolve("@nifrajs/web-react/server-components/guard", importer, {
132
+ skipSelf: true,
133
+ })
134
+ if (guard !== null) internal.add(bare(guard.id))
135
+ return reactRuntime
136
+ }
137
+ if (source.startsWith("\0virtual:vite-rsc/") && environment !== "rsc") return null
138
+ if (source.startsWith("@nifrajs/web-react/server-components/")) {
139
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
140
+ if (resolved !== null) internal.add(bare(resolved.id))
141
+ return resolved
142
+ }
143
+ if (source.includes(nativeQuery)) {
144
+ const file = source.startsWith("/@fs/")
145
+ ? source.slice("/@fs".length).replace(/^\/(?=[A-Za-z]:[\\/])/, "")
146
+ : existsSync(bare(source))
147
+ ? source
148
+ : resolve(options.root, `.${source}`)
149
+ if (
150
+ !nativeReferences.has(bare(file)) ||
151
+ (environment === "rsc" && (importer === undefined || bare(importer) !== bare(file)))
152
+ )
153
+ throw new Error(
154
+ "[nifra/components] Raw native client references are compiler-owned; import the registered client component",
155
+ )
156
+ return file
157
+ }
158
+ if (source.startsWith(referencePrefix) || source.startsWith("\0")) return null
159
+ if (
160
+ environment === "rsc" &&
161
+ options.adapter === "react" &&
162
+ (source === "react/jsx-runtime" || source === "react/jsx-dev-runtime") &&
163
+ !internal.has(bare(importer ?? ""))
164
+ ) {
165
+ const resolved = await this.resolve(
166
+ `@nifrajs/web-react/server-components/${source.endsWith("dev-runtime") ? "jsx-dev-runtime" : "jsx-runtime"}`,
167
+ importer,
168
+ { skipSelf: true },
169
+ )
170
+ if (resolved === null)
171
+ throw new Error("[nifra/components] Install the matching @nifrajs/web-react adapter")
172
+ internal.add(bare(resolved.id))
173
+ const guard = await this.resolve("@nifrajs/web-react/server-components/guard", importer, {
174
+ skipSelf: true,
175
+ })
176
+ if (guard !== null) internal.add(bare(guard.id))
177
+ return resolved
178
+ }
179
+ if (environment !== "client") return null
180
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
181
+ if (resolved === null || !componentFile.test(bare(resolved.id))) return null
182
+ if (resolved.id !== bare(resolved.id))
183
+ throw new Error("[nifra/components] Server component source cannot be imported as an asset")
184
+ const compiled = compileComponentModule(
185
+ readFileSync(resolved.id, "utf8"),
186
+ resolved.id,
187
+ options.root,
188
+ "client",
189
+ )
190
+ const id = referencePrefix + componentReference(resolved.id, options.root, "module")
191
+ stubs.set(id, compiled.code)
192
+ return id
193
+ },
194
+ load(id) {
195
+ if (id === reactRuntime)
196
+ return {
197
+ code: 'import * as Native from "react"; import { guardedCreateElement as createElement, guardedCloneElement as cloneElement } from "@nifrajs/web-react/server-components/guard"; export { Children, Fragment, Profiler, StrictMode, Suspense, __SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, cache, cacheSignal, captureOwnerStack, createRef, forwardRef, isValidElement, lazy, memo, use, useCallback, useDebugValue, useId, useMemo, version } from "react"; export { createElement, cloneElement }; export default { ...Native, createElement, cloneElement };',
198
+ map: null,
199
+ }
200
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The compiler inspects trusted AST/package metadata and its own populated stub map; application data never supplies these values.
201
+ if (stubs.has(id)) return { code: stubs.get(id) as string, map: null }
202
+ const path = bare(id)
203
+ if (id.endsWith(`?${nativeQuery}`)) return { code: readFileSync(path, "utf8"), map: null }
204
+ if (!/\.[cm]?[jt]sx?$/.test(path) || !existsSync(path)) return null
205
+ const source = readFileSync(path, "utf8")
206
+ const ast = sourceFile(source, path)
207
+ if (directive(ast, "use server"))
208
+ throw new Error(
209
+ `[nifra/components] Native use-server actions in ${path} are unsupported; use a validated serverFn in a *.fn.ts module`,
210
+ )
211
+ if (
212
+ this.environment.name === "rsc" &&
213
+ options.adapter === "react" &&
214
+ directive(ast, "use client")
215
+ ) {
216
+ nativeReferences.add(path)
217
+ return { code: nativeFacade(source, path), map: null }
218
+ }
219
+ return null
220
+ },
221
+ transform(source, id) {
222
+ if (id.includes(nativeQuery) || internal.has(bare(id))) return null
223
+ const path = bare(id)
224
+ let code = source
225
+ if (componentFile.test(path)) {
226
+ code = compileComponentModule(source, path, options.root, this.environment.name).code
227
+ componentModules.add(path)
228
+ }
229
+ if (islandFile.test(path))
230
+ code = compileComponentModule(
231
+ source,
232
+ path,
233
+ options.root,
234
+ this.environment.name,
235
+ "island",
236
+ ).code
237
+ if (this.environment.name === "rsc" && options.adapter === "react")
238
+ code = guardCreateElementCalls(code, path)
239
+ return code === source ? null : { code, map: null }
240
+ },
241
+ }
242
+ }
@@ -0,0 +1,32 @@
1
+ export {
2
+ defineServerComponent,
3
+ inheritOwnerAuthorization,
4
+ isServerComponent,
5
+ prepareServerComponent,
6
+ } from "./server-components/definition.ts"
7
+ export { type ComponentErrorCode, ServerComponentError } from "./server-components/error.ts"
8
+ export {
9
+ createServerComponents,
10
+ defineComponentOwner,
11
+ type ServerComponentsOptions,
12
+ serverComponentIdentity,
13
+ serverComponentInstance,
14
+ } from "./server-components/runtime.ts"
15
+ export type {
16
+ ComponentInstance,
17
+ ComponentIsland,
18
+ ComponentKey,
19
+ ComponentOwner,
20
+ ComponentRecord,
21
+ ComponentRender,
22
+ ComponentRenderContext,
23
+ ComponentState,
24
+ ComponentValue,
25
+ PreparedServerComponent,
26
+ ServerComponent,
27
+ ServerComponentContext,
28
+ ServerComponentDefinition,
29
+ ServerComponentHandle,
30
+ ServerComponentRenderAdapter,
31
+ ServerComponentRuntime,
32
+ } from "./server-components/types.ts"
@@ -13,6 +13,10 @@
13
13
  * - **GET, same-origin, `ok`, and not `no-store`.** Anything else goes straight to the network.
14
14
  * - **The cache name carries the build id.** A deploy that changed the assets gets a new cache, and
15
15
  * activation deletes every older one, so a stale worker cannot pin an old build indefinitely.
16
+ * Only caches named `<cacheName>:<buildId>` are the worker's: the origin's other caches - another
17
+ * app's, or data the page stored itself - are never read, written or deleted.
18
+ * - **Only this build's precached URLs are answered.** Every other request, an API call included,
19
+ * goes to the network untouched.
16
20
  *
17
21
  * It is opt-in and generated at build time; an app that does not call this ships nothing.
18
22
  */
@@ -36,7 +40,10 @@ export interface ServiceWorkerOptions {
36
40
  * bytes. Omit it and failed navigations simply fail, which is the honest default.
37
41
  */
38
42
  readonly offlineUrl?: string
39
- /** Cache name prefix. Default `nifra`. */
43
+ /**
44
+ * Cache name prefix, and the namespace activation cleans: every cache named `<cacheName>:<id>` other
45
+ * than this build's is deleted. Give each worker sharing an origin its own. Default `nifra`.
46
+ */
40
47
  readonly cacheName?: string
41
48
  /** Extra same-origin URLs to precache. Only pass immutable ones. */
42
49
  readonly additionalPrecache?: readonly string[]
@@ -101,8 +108,11 @@ export function generateServiceWorker(
101
108
  const urls = [...new Set(offline === undefined ? precache : [...precache, offline])].sort()
102
109
 
103
110
  return `// Generated by @nifrajs/web/service-worker. Do not edit.
104
- const CACHE = ${JSON.stringify(`${prefix}-${options.buildId}`)}
111
+ const OWNED = ${JSON.stringify(`${prefix}:`)}
112
+ const CACHE = OWNED + ${JSON.stringify(options.buildId)}
105
113
  const PRECACHE = ${JSON.stringify(urls)}
114
+ const OWN_URLS = new Set(PRECACHE)
115
+ const OWN = { cacheName: CACHE }
106
116
  const OFFLINE = ${offline === undefined ? "null" : JSON.stringify(offline)}
107
117
 
108
118
  self.addEventListener("install", (event) => {
@@ -115,7 +125,9 @@ self.addEventListener("activate", (event) => {
115
125
  event.waitUntil(
116
126
  caches
117
127
  .keys()
118
- .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
128
+ // Neither the prefix nor a build id can hold a colon, so the first one ends the namespace.
129
+ .then((keys) => keys.filter((k) => k !== CACHE && k.startsWith(OWNED)))
130
+ .then((old) => Promise.all(old.map((k) => caches.delete(k))))
119
131
  .then(() => self.clients.claim()),
120
132
  )
121
133
  })
@@ -130,20 +142,25 @@ self.addEventListener("fetch", (event) => {
130
142
  // how one user gets served the page rendered for another, so nothing is stored here.
131
143
  if (request.mode === "navigate") {
132
144
  if (OFFLINE === null) return
133
- event.respondWith(fetch(request).catch(() => caches.match(OFFLINE).then((r) => r ?? Response.error())))
145
+ event.respondWith(
146
+ fetch(request).catch(() => caches.match(OFFLINE, OWN).then((r) => r ?? Response.error())),
147
+ )
134
148
  return
135
149
  }
136
150
 
137
- // Assets: cache first, because every precached URL is content-hashed and therefore immutable.
151
+ // Assets: cache first, because every precached URL is content-hashed and therefore immutable. Only
152
+ // this build's URLs are answered, and only from this build's cache: every other request, an API
153
+ // call included, goes to the network, and another cache on the origin is never read.
154
+ if (!OWN_URLS.has(url.pathname)) return
138
155
  event.respondWith(
139
- caches.match(request).then((hit) => {
156
+ caches.match(request, OWN).then((hit) => {
140
157
  if (hit !== undefined) return hit
141
158
  return fetch(request).then((response) => {
142
159
  const cacheable =
143
160
  response.ok &&
144
161
  response.type === "basic" &&
145
162
  !(response.headers.get("cache-control") ?? "").includes("no-store")
146
- if (cacheable && PRECACHE.includes(url.pathname)) {
163
+ if (cacheable) {
147
164
  const copy = response.clone()
148
165
  void caches.open(CACHE).then((c) => c.put(request, copy))
149
166
  }
package/src/vite.ts CHANGED
@@ -10,13 +10,15 @@
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
+
14
+ import { type Dirent, readdirSync, writeFileSync } from "node:fs"
14
15
  import {
15
16
  createServer as createHttpServer,
16
17
  type IncomingMessage,
17
18
  type ServerResponse,
18
19
  } from "node:http"
19
20
  import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path"
21
+ import { singleCopyPackageNames } from "@nifrajs/core/single-copy"
20
22
  import { CLIENT_BATCH_MAX_BYTES } from "./dev-client.ts"
21
23
  import { DEV_REQUEST_ID_HEADER, DEV_SERVER_FILES } from "./dev-feed.ts"
22
24
  import { listenOrExplain } from "./dev-port.ts"
@@ -170,6 +172,7 @@ export { LAST_ERROR_PATH } from "./diagnostic.ts"
170
172
  // The codegen'd client entry is written here (at the Vite root) so Vite serves + HMRs it.
171
173
  const DEV_ENTRY = ".nifra-vite-entry.tsx"
172
174
  const VITE_CLOSE_BOUND_MS = 2000
175
+ const POLL_INTERVAL_MS = 80
173
176
 
174
177
  /** Candidate spellings for Vite's module-graph map. Vite has used native and slash-normalized
175
178
  * absolute Windows paths across versions, while watcher events can additionally arrive as a file URL. */
@@ -184,6 +187,22 @@ const pathInside = (root: string, path: string): boolean => {
184
187
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
185
188
  }
186
189
 
190
+ /** Every file below `dir` as an absolute path. A directory removed mid-walk contributes nothing. */
191
+ const filesUnder = (dir: string, into = new Set<string>()): Set<string> => {
192
+ let entries: Dirent[]
193
+ try {
194
+ entries = readdirSync(dir, { withFileTypes: true })
195
+ } catch {
196
+ return into
197
+ }
198
+ for (const entry of entries) {
199
+ const path = join(dir, entry.name)
200
+ if (entry.isDirectory()) filesUnder(path, into)
201
+ else into.add(path)
202
+ }
203
+ return into
204
+ }
205
+
187
206
  const readNodeBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {
188
207
  if (req.method === "GET" || req.method === "HEAD") return undefined
189
208
  const chunks: Buffer[] = []
@@ -415,6 +434,9 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
415
434
  generateClientEntry(manifest, { clientModule: options.clientModule, resolve: toUrl }),
416
435
  )
417
436
  }
437
+ const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
438
+ // Listed before the entry is generated, so a file added in between is reconciled again, not lost.
439
+ const routeFiles = usePolling ? filesUnder(routesDir) : new Set<string>()
418
440
  writeClientEntry()
419
441
  const entryUrl = `/${DEV_ENTRY}`
420
442
 
@@ -578,7 +600,6 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
578
600
  // path-shaped `clientModule` (tests) names no package; there is then nothing to externalize.
579
601
  const adapterPackage = packageNameOf(options.clientModule)
580
602
  const ssrExternal = [...(adapterPackage !== undefined ? [adapterPackage] : []), "@nifrajs/web"]
581
- const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
582
603
  // The feed writes its record and log under `.nifra/`: watching them would turn every log flush into
583
604
  // a "file change" that re-creates the app and marks every entry stale.
584
605
  const watch: { ignored: RegExp[]; usePolling?: boolean; interval?: number } = {
@@ -587,7 +608,7 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
587
608
  // Poll when native fs events aren't delivered (containers/sandboxes).
588
609
  if (usePolling) {
589
610
  watch.usePolling = true
590
- watch.interval = 80
611
+ watch.interval = POLL_INTERVAL_MS
591
612
  }
592
613
  // Never `import("vite")` directly here: the guard in `importVite` has to run first, and the dev
593
614
  // server importing vite unguarded is precisely what poisoned the module for the whole process.
@@ -613,6 +634,10 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
613
634
  // Ahead of the user's plugins: the zone guard refuses backend code before anything serves or
614
635
  // transforms it, and a `*.fn` module must be replaced before anything else reads it.
615
636
  plugins: [zoneGuard, viteServerFnStub(), ...plugins],
637
+ // Vite pre-bundles what it finds by crawling `index.html`, and this app has none: every dependency
638
+ // would be found mid-session instead, each one re-optimizing and reloading the page. The client
639
+ // entry imports every route's frontend module, so crawling it covers the whole route graph.
640
+ optimizeDeps: { entries: [DEV_ENTRY] },
616
641
  resolve: {
617
642
  conditions: resolveConditions,
618
643
  // Dedupe each framework runtime to ONE copy. In a multi-root workspace a shared package can pull
@@ -621,7 +646,8 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
621
646
  // on any hook-using route (the error points at the component, not the resolution - brutal to
622
647
  // diagnose). Same policy table the production builds read, so dev matches prod. No-op per
623
648
  // framework the app does not use (the package simply isn't present to dedupe).
624
- dedupe: [...viteDedupePackages()],
649
+ // The app's single-copy declaration pins its packages the same way.
650
+ dedupe: [...viteDedupePackages(), ...singleCopyPackageNames(root)],
625
651
  },
626
652
  ssr: {
627
653
  // Explicit listing forces external even for workspace-LINKED packages (Vite's default keeps
@@ -722,20 +748,43 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
722
748
  session.markChange()
723
749
  refreshApp(normalizeFilePath(path))
724
750
  })
751
+ const onAddUnlink = (event: "add" | "unlink", path: string): void => {
752
+ const filePath = normalizeFilePath(path)
753
+ const inRoutes = pathInside(routesDir, filePath)
754
+ if (inRoutes && usePolling) {
755
+ const file = resolvePath(filePath)
756
+ // Whichever of the watcher and the rescan below reports a route file first handles it.
757
+ if (routeFiles.has(file) === (event === "add")) return
758
+ if (event === "add") routeFiles.add(file)
759
+ else routeFiles.delete(file)
760
+ }
761
+ session.markChange()
762
+ if (inRoutes) {
763
+ writeClientEntry()
764
+ // A newly added route is not in Vite's module graph yet, so invalidating only the
765
+ // route path cannot evict the cached generated entry that imports the route. Evict the
766
+ // generated entry after rewriting it before recreating the SSR app; this also prevents
767
+ // an unlink refresh from briefly loading an entry that still references the deleted file.
768
+ invalidateImporterClosure(resolvePath(root, DEV_ENTRY))
769
+ }
770
+ refreshApp(filePath)
771
+ }
725
772
  for (const event of ["add", "unlink"] as const) {
726
- vite.watcher.on(event, (path) => {
727
- session.markChange()
728
- const filePath = normalizeFilePath(path)
729
- if (pathInside(routesDir, filePath)) {
730
- writeClientEntry()
731
- // A newly added route is not in Vite's module graph yet, so invalidating only the
732
- // route path cannot evict the cached generated entry that imports the route. Evict the
733
- // generated entry after rewriting it before recreating the SSR app; this also prevents
734
- // an unlink refresh from briefly loading an entry that still references the deleted file.
735
- invalidateImporterClosure(resolvePath(root, DEV_ENTRY))
736
- }
737
- refreshApp(filePath)
738
- })
773
+ vite.watcher.on(event, (path) => onAddUnlink(event, path))
774
+ }
775
+ // Chokidar reports ready before the poller has taken its baseline stat of a directory (Bun's
776
+ // fs.watchFile takes it asynchronously), and a route added or removed in that gap is folded into the
777
+ // baseline and never reported. The manifest comes from a directory scan, so a scan diffed against the
778
+ // route files already handled recovers those events.
779
+ const reconcileRouteFiles = (): void => {
780
+ const onDisk = filesUnder(routesDir)
781
+ try {
782
+ for (const file of onDisk) if (!routeFiles.has(file)) onAddUnlink("add", file)
783
+ for (const file of [...routeFiles]) if (!onDisk.has(file)) onAddUnlink("unlink", file)
784
+ } catch (err) {
785
+ console.error("[nifra/web/vite] route rescan failed:", err)
786
+ session.buildFailed(err, "route rescan failed")
787
+ }
739
788
  }
740
789
  await watcherReady
741
790
 
@@ -756,9 +805,11 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
756
805
  const address = server.address()
757
806
  const boundPort = typeof address === "object" && address !== null ? address.port : port
758
807
  session.listening(boundPort)
808
+ const routeRescan = usePolling ? setInterval(reconcileRouteFiles, POLL_INTERVAL_MS) : undefined
759
809
  return {
760
810
  port: boundPort,
761
811
  stop: async () => {
812
+ clearInterval(routeRescan)
762
813
  session.stop()
763
814
  // Cleared with the server that owns it: the slot is process-global, so a stopped server leaving
764
815
  // its loader behind would hand the next one's adapter a graph that is closed (tests start and