@nifrajs/web 4.0.2 → 5.1.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 (426) hide show
  1. package/README.md +19 -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 +6 -6
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +46 -18
  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-port.d.ts +3 -2
  26. package/dist/dev-port.d.ts.map +1 -1
  27. package/dist/dev-port.js +2 -6
  28. package/dist/dev-port.js.map +1 -1
  29. package/dist/dev-session.d.ts.map +1 -1
  30. package/dist/dev-session.js +4 -14
  31. package/dist/dev-session.js.map +1 -1
  32. package/dist/dev.d.ts +7 -17
  33. package/dist/dev.d.ts.map +1 -1
  34. package/dist/dev.js +13 -1
  35. package/dist/dev.js.map +1 -1
  36. package/dist/draft.d.ts +8 -5
  37. package/dist/draft.d.ts.map +1 -1
  38. package/dist/draft.js +30 -13
  39. package/dist/draft.js.map +1 -1
  40. package/dist/fonts-google.d.ts.map +1 -1
  41. package/dist/fonts-google.js +7 -0
  42. package/dist/fonts-google.js.map +1 -1
  43. package/dist/index.d.ts +5 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/codegen.d.ts +1 -0
  48. package/dist/internal/codegen.d.ts.map +1 -1
  49. package/dist/internal/codegen.js +56 -29
  50. package/dist/internal/codegen.js.map +1 -1
  51. package/dist/internal/disposable-dir.d.ts +8 -0
  52. package/dist/internal/disposable-dir.d.ts.map +1 -0
  53. package/dist/internal/disposable-dir.js +25 -0
  54. package/dist/internal/disposable-dir.js.map +1 -0
  55. package/dist/internal/entry-chunks.d.ts +9 -0
  56. package/dist/internal/entry-chunks.d.ts.map +1 -0
  57. package/dist/internal/entry-chunks.js +15 -0
  58. package/dist/internal/entry-chunks.js.map +1 -0
  59. package/dist/internal/evidence-traversal.d.ts +12 -0
  60. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  61. package/dist/internal/evidence-traversal.js +48 -0
  62. package/dist/internal/evidence-traversal.js.map +1 -0
  63. package/dist/internal/head-merge.d.ts +6 -0
  64. package/dist/internal/head-merge.d.ts.map +1 -1
  65. package/dist/internal/head-merge.js +9 -0
  66. package/dist/internal/head-merge.js.map +1 -1
  67. package/dist/internal/js-string.d.ts +2 -0
  68. package/dist/internal/js-string.d.ts.map +1 -1
  69. package/dist/internal/js-string.js +5 -1
  70. package/dist/internal/js-string.js.map +1 -1
  71. package/dist/internal/mount-shadow.js +1 -1
  72. package/dist/internal/mount-shadow.js.map +1 -1
  73. package/dist/internal/output-guard.d.ts +7 -0
  74. package/dist/internal/output-guard.d.ts.map +1 -1
  75. package/dist/internal/output-guard.js +80 -8
  76. package/dist/internal/output-guard.js.map +1 -1
  77. package/dist/internal/package-entries.d.ts +34 -0
  78. package/dist/internal/package-entries.d.ts.map +1 -0
  79. package/dist/internal/package-entries.js +324 -0
  80. package/dist/internal/package-entries.js.map +1 -0
  81. package/dist/internal/page-execution.d.ts +6 -0
  82. package/dist/internal/page-execution.d.ts.map +1 -1
  83. package/dist/internal/page-execution.js +437 -114
  84. package/dist/internal/page-execution.js.map +1 -1
  85. package/dist/internal/parity.d.ts.map +1 -1
  86. package/dist/internal/parity.js +15 -2
  87. package/dist/internal/parity.js.map +1 -1
  88. package/dist/internal/private-env.d.ts +8 -0
  89. package/dist/internal/private-env.d.ts.map +1 -1
  90. package/dist/internal/private-env.js +25 -10
  91. package/dist/internal/private-env.js.map +1 -1
  92. package/dist/internal/progressive-cookies.d.ts +14 -0
  93. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  94. package/dist/internal/progressive-cookies.js +43 -0
  95. package/dist/internal/progressive-cookies.js.map +1 -0
  96. package/dist/internal/public-env.d.ts +2 -0
  97. package/dist/internal/public-env.d.ts.map +1 -0
  98. package/dist/internal/public-env.js +13 -0
  99. package/dist/internal/public-env.js.map +1 -0
  100. package/dist/internal/render-document.d.ts +5 -44
  101. package/dist/internal/render-document.d.ts.map +1 -1
  102. package/dist/internal/render-document.js +51 -63
  103. package/dist/internal/render-document.js.map +1 -1
  104. package/dist/internal/response-controls.d.ts +2 -0
  105. package/dist/internal/response-controls.d.ts.map +1 -1
  106. package/dist/internal/response-controls.js +26 -16
  107. package/dist/internal/response-controls.js.map +1 -1
  108. package/dist/internal/revive-dates.d.ts +16 -0
  109. package/dist/internal/revive-dates.d.ts.map +1 -0
  110. package/dist/internal/revive-dates.js +32 -0
  111. package/dist/internal/revive-dates.js.map +1 -0
  112. package/dist/internal/route-path.d.ts +14 -0
  113. package/dist/internal/route-path.d.ts.map +1 -0
  114. package/dist/internal/route-path.js +19 -0
  115. package/dist/internal/route-path.js.map +1 -0
  116. package/dist/internal/secret-scan.d.ts.map +1 -1
  117. package/dist/internal/secret-scan.js +190 -20
  118. package/dist/internal/secret-scan.js.map +1 -1
  119. package/dist/internal/server-boundary.d.ts.map +1 -1
  120. package/dist/internal/server-boundary.js +33 -5
  121. package/dist/internal/server-boundary.js.map +1 -1
  122. package/dist/internal/web-app.d.ts +25 -18
  123. package/dist/internal/web-app.d.ts.map +1 -1
  124. package/dist/internal/web-app.js +81 -24
  125. package/dist/internal/web-app.js.map +1 -1
  126. package/dist/internal/wire-date.d.ts +3 -0
  127. package/dist/internal/wire-date.d.ts.map +1 -0
  128. package/dist/internal/wire-date.js +14 -0
  129. package/dist/internal/wire-date.js.map +1 -0
  130. package/dist/internal/without-query.d.ts +4 -0
  131. package/dist/internal/without-query.d.ts.map +1 -0
  132. package/dist/internal/without-query.js +9 -0
  133. package/dist/internal/without-query.js.map +1 -0
  134. package/dist/internal/zone-graph.d.ts +4 -0
  135. package/dist/internal/zone-graph.d.ts.map +1 -1
  136. package/dist/internal/zone-graph.js +71 -20
  137. package/dist/internal/zone-graph.js.map +1 -1
  138. package/dist/isr.d.ts +59 -3
  139. package/dist/isr.d.ts.map +1 -1
  140. package/dist/isr.js +311 -41
  141. package/dist/isr.js.map +1 -1
  142. package/dist/llms-txt.d.ts +2 -0
  143. package/dist/llms-txt.d.ts.map +1 -1
  144. package/dist/llms-txt.js +29 -3
  145. package/dist/llms-txt.js.map +1 -1
  146. package/dist/manifest.d.ts +37 -4
  147. package/dist/manifest.d.ts.map +1 -1
  148. package/dist/manifest.js +4 -0
  149. package/dist/manifest.js.map +1 -1
  150. package/dist/open-resolved.d.ts +18 -0
  151. package/dist/open-resolved.d.ts.map +1 -0
  152. package/dist/open-resolved.js +62 -0
  153. package/dist/open-resolved.js.map +1 -0
  154. package/dist/plugins/kit.d.ts +1 -0
  155. package/dist/plugins/kit.d.ts.map +1 -1
  156. package/dist/plugins/kit.js +26 -11
  157. package/dist/plugins/kit.js.map +1 -1
  158. package/dist/plugins/vite-leak-guard.d.ts +9 -1
  159. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  160. package/dist/plugins/vite-leak-guard.js +84 -7
  161. package/dist/plugins/vite-leak-guard.js.map +1 -1
  162. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  163. package/dist/plugins/vite-zone-guard.js +17 -21
  164. package/dist/plugins/vite-zone-guard.js.map +1 -1
  165. package/dist/plugins/zone-guard.d.ts.map +1 -1
  166. package/dist/plugins/zone-guard.js +44 -3
  167. package/dist/plugins/zone-guard.js.map +1 -1
  168. package/dist/prerender.d.ts +5 -1
  169. package/dist/prerender.d.ts.map +1 -1
  170. package/dist/prerender.js +41 -19
  171. package/dist/prerender.js.map +1 -1
  172. package/dist/public-dir.d.ts +8 -1
  173. package/dist/public-dir.d.ts.map +1 -1
  174. package/dist/public-dir.js +17 -6
  175. package/dist/public-dir.js.map +1 -1
  176. package/dist/query.d.ts +13 -4
  177. package/dist/query.d.ts.map +1 -1
  178. package/dist/query.js +89 -26
  179. package/dist/query.js.map +1 -1
  180. package/dist/render-seam.d.ts +25 -2
  181. package/dist/render-seam.d.ts.map +1 -1
  182. package/dist/render-seam.js +1 -0
  183. package/dist/render-seam.js.map +1 -1
  184. package/dist/route-types.d.ts.map +1 -1
  185. package/dist/route-types.js +20 -9
  186. package/dist/route-types.js.map +1 -1
  187. package/dist/router.d.ts +9 -2
  188. package/dist/router.d.ts.map +1 -1
  189. package/dist/router.js +47 -50
  190. package/dist/router.js.map +1 -1
  191. package/dist/seo.d.ts +49 -0
  192. package/dist/seo.d.ts.map +1 -0
  193. package/dist/seo.js +48 -0
  194. package/dist/seo.js.map +1 -0
  195. package/dist/server-components/build-info.d.ts +3 -0
  196. package/dist/server-components/build-info.d.ts.map +1 -0
  197. package/dist/server-components/build-info.js +7 -0
  198. package/dist/server-components/build-info.js.map +1 -0
  199. package/dist/server-components/build-target.d.ts +12 -0
  200. package/dist/server-components/build-target.d.ts.map +1 -0
  201. package/dist/server-components/build-target.js +153 -0
  202. package/dist/server-components/build-target.js.map +1 -0
  203. package/dist/server-components/build.d.ts +24 -0
  204. package/dist/server-components/build.d.ts.map +1 -0
  205. package/dist/server-components/build.js +366 -0
  206. package/dist/server-components/build.js.map +1 -0
  207. package/dist/server-components/bundle-proof.d.ts +3 -0
  208. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  209. package/dist/server-components/bundle-proof.js +23 -0
  210. package/dist/server-components/bundle-proof.js.map +1 -0
  211. package/dist/server-components/client.d.ts +46 -0
  212. package/dist/server-components/client.d.ts.map +1 -0
  213. package/dist/server-components/client.js +594 -0
  214. package/dist/server-components/client.js.map +1 -0
  215. package/dist/server-components/definition.d.ts +11 -0
  216. package/dist/server-components/definition.d.ts.map +1 -0
  217. package/dist/server-components/definition.js +94 -0
  218. package/dist/server-components/definition.js.map +1 -0
  219. package/dist/server-components/dev-http.d.ts +5 -0
  220. package/dist/server-components/dev-http.d.ts.map +1 -0
  221. package/dist/server-components/dev-http.js +71 -0
  222. package/dist/server-components/dev-http.js.map +1 -0
  223. package/dist/server-components/error.d.ts +9 -0
  224. package/dist/server-components/error.d.ts.map +1 -0
  225. package/dist/server-components/error.js +24 -0
  226. package/dist/server-components/error.js.map +1 -0
  227. package/dist/server-components/frames.d.ts +7 -0
  228. package/dist/server-components/frames.d.ts.map +1 -0
  229. package/dist/server-components/frames.js +169 -0
  230. package/dist/server-components/frames.js.map +1 -0
  231. package/dist/server-components/html-policy.d.ts +6 -0
  232. package/dist/server-components/html-policy.d.ts.map +1 -0
  233. package/dist/server-components/html-policy.js +75 -0
  234. package/dist/server-components/html-policy.js.map +1 -0
  235. package/dist/server-components/html.d.ts +4 -0
  236. package/dist/server-components/html.d.ts.map +1 -0
  237. package/dist/server-components/html.js +67 -0
  238. package/dist/server-components/html.js.map +1 -0
  239. package/dist/server-components/islands.d.ts +44 -0
  240. package/dist/server-components/islands.d.ts.map +1 -0
  241. package/dist/server-components/islands.js +42 -0
  242. package/dist/server-components/islands.js.map +1 -0
  243. package/dist/server-components/native.d.ts +4 -0
  244. package/dist/server-components/native.d.ts.map +1 -0
  245. package/dist/server-components/native.js +81 -0
  246. package/dist/server-components/native.js.map +1 -0
  247. package/dist/server-components/portal.d.ts +6 -0
  248. package/dist/server-components/portal.d.ts.map +1 -0
  249. package/dist/server-components/portal.js +35 -0
  250. package/dist/server-components/portal.js.map +1 -0
  251. package/dist/server-components/props.d.ts +3 -0
  252. package/dist/server-components/props.d.ts.map +1 -0
  253. package/dist/server-components/props.js +14 -0
  254. package/dist/server-components/props.js.map +1 -0
  255. package/dist/server-components/protocol.d.ts +44 -0
  256. package/dist/server-components/protocol.d.ts.map +1 -0
  257. package/dist/server-components/protocol.js +139 -0
  258. package/dist/server-components/protocol.js.map +1 -0
  259. package/dist/server-components/range.d.ts +24 -0
  260. package/dist/server-components/range.d.ts.map +1 -0
  261. package/dist/server-components/range.js +263 -0
  262. package/dist/server-components/range.js.map +1 -0
  263. package/dist/server-components/registry.d.ts +6 -0
  264. package/dist/server-components/registry.d.ts.map +1 -0
  265. package/dist/server-components/registry.js +30 -0
  266. package/dist/server-components/registry.js.map +1 -0
  267. package/dist/server-components/response.d.ts +4 -0
  268. package/dist/server-components/response.d.ts.map +1 -0
  269. package/dist/server-components/response.js +32 -0
  270. package/dist/server-components/response.js.map +1 -0
  271. package/dist/server-components/router-integration.d.ts +9 -0
  272. package/dist/server-components/router-integration.d.ts.map +1 -0
  273. package/dist/server-components/router-integration.js +29 -0
  274. package/dist/server-components/router-integration.js.map +1 -0
  275. package/dist/server-components/runtime.d.ts +14 -0
  276. package/dist/server-components/runtime.d.ts.map +1 -0
  277. package/dist/server-components/runtime.js +276 -0
  278. package/dist/server-components/runtime.js.map +1 -0
  279. package/dist/server-components/scheduler.d.ts +14 -0
  280. package/dist/server-components/scheduler.d.ts.map +1 -0
  281. package/dist/server-components/scheduler.js +74 -0
  282. package/dist/server-components/scheduler.js.map +1 -0
  283. package/dist/server-components/schema.d.ts +4 -0
  284. package/dist/server-components/schema.d.ts.map +1 -0
  285. package/dist/server-components/schema.js +77 -0
  286. package/dist/server-components/schema.js.map +1 -0
  287. package/dist/server-components/server-entry.d.ts +12 -0
  288. package/dist/server-components/server-entry.d.ts.map +1 -0
  289. package/dist/server-components/server-entry.js +90 -0
  290. package/dist/server-components/server-entry.js.map +1 -0
  291. package/dist/server-components/source.d.ts +15 -0
  292. package/dist/server-components/source.d.ts.map +1 -0
  293. package/dist/server-components/source.js +269 -0
  294. package/dist/server-components/source.js.map +1 -0
  295. package/dist/server-components/state.d.ts +4 -0
  296. package/dist/server-components/state.d.ts.map +1 -0
  297. package/dist/server-components/state.js +123 -0
  298. package/dist/server-components/state.js.map +1 -0
  299. package/dist/server-components/stream.d.ts +6 -0
  300. package/dist/server-components/stream.d.ts.map +1 -0
  301. package/dist/server-components/stream.js +70 -0
  302. package/dist/server-components/stream.js.map +1 -0
  303. package/dist/server-components/trusted-html.d.ts +3 -0
  304. package/dist/server-components/trusted-html.d.ts.map +1 -0
  305. package/dist/server-components/trusted-html.js +14 -0
  306. package/dist/server-components/trusted-html.js.map +1 -0
  307. package/dist/server-components/types.d.ts +107 -0
  308. package/dist/server-components/types.d.ts.map +1 -0
  309. package/dist/server-components/types.js +2 -0
  310. package/dist/server-components/types.js.map +1 -0
  311. package/dist/server-components/typescript.d.ts +11 -0
  312. package/dist/server-components/typescript.d.ts.map +1 -0
  313. package/dist/server-components/typescript.js +28 -0
  314. package/dist/server-components/typescript.js.map +1 -0
  315. package/dist/server-components/value.d.ts +5 -0
  316. package/dist/server-components/value.d.ts.map +1 -0
  317. package/dist/server-components/value.js +77 -0
  318. package/dist/server-components/value.js.map +1 -0
  319. package/dist/server-components/vite.d.ts +12 -0
  320. package/dist/server-components/vite.d.ts.map +1 -0
  321. package/dist/server-components/vite.js +197 -0
  322. package/dist/server-components/vite.js.map +1 -0
  323. package/dist/server-components.d.ts +5 -0
  324. package/dist/server-components.d.ts.map +1 -0
  325. package/dist/server-components.js +4 -0
  326. package/dist/server-components.js.map +1 -0
  327. package/dist/service-worker.d.ts +8 -1
  328. package/dist/service-worker.d.ts.map +1 -1
  329. package/dist/service-worker.js +20 -6
  330. package/dist/service-worker.js.map +1 -1
  331. package/dist/vite.d.ts +12 -0
  332. package/dist/vite.d.ts.map +1 -1
  333. package/dist/vite.js +7 -1
  334. package/dist/vite.js.map +1 -1
  335. package/dist/zones.d.ts +64 -2
  336. package/dist/zones.d.ts.map +1 -1
  337. package/dist/zones.js +331 -28
  338. package/dist/zones.js.map +1 -1
  339. package/package.json +102 -4
  340. package/src/build-vite.ts +31 -9
  341. package/src/build.ts +109 -31
  342. package/src/cdn.ts +18 -12
  343. package/src/client.ts +48 -17
  344. package/src/deferred.ts +27 -9
  345. package/src/dev-feed.ts +24 -5
  346. package/src/dev-port.ts +4 -10
  347. package/src/dev-session.ts +4 -13
  348. package/src/dev.ts +29 -1
  349. package/src/draft.ts +35 -14
  350. package/src/fonts-google.ts +10 -0
  351. package/src/index.ts +5 -6
  352. package/src/internal/codegen.ts +60 -29
  353. package/src/internal/disposable-dir.ts +24 -0
  354. package/src/internal/entry-chunks.ts +18 -0
  355. package/src/internal/evidence-traversal.ts +57 -0
  356. package/src/internal/head-merge.ts +16 -0
  357. package/src/internal/js-string.ts +6 -1
  358. package/src/internal/mount-shadow.ts +1 -1
  359. package/src/internal/output-guard.ts +88 -7
  360. package/src/internal/package-entries.ts +331 -0
  361. package/src/internal/page-execution.ts +573 -114
  362. package/src/internal/parity.ts +15 -2
  363. package/src/internal/private-env.ts +27 -13
  364. package/src/internal/progressive-cookies.ts +60 -0
  365. package/src/internal/public-env.ts +14 -0
  366. package/src/internal/render-document.ts +66 -89
  367. package/src/internal/response-controls.ts +31 -14
  368. package/src/internal/revive-dates.ts +35 -0
  369. package/src/internal/route-path.ts +24 -0
  370. package/src/internal/secret-scan.ts +190 -18
  371. package/src/internal/server-boundary.ts +29 -5
  372. package/src/internal/web-app.ts +126 -42
  373. package/src/internal/wire-date.ts +16 -0
  374. package/src/internal/without-query.ts +8 -0
  375. package/src/internal/zone-graph.ts +78 -25
  376. package/src/isr.ts +364 -45
  377. package/src/llms-txt.ts +32 -3
  378. package/src/manifest.ts +38 -4
  379. package/src/open-resolved.ts +64 -0
  380. package/src/plugins/kit.ts +31 -11
  381. package/src/plugins/vite-leak-guard.ts +104 -8
  382. package/src/plugins/vite-zone-guard.ts +21 -19
  383. package/src/plugins/zone-guard.ts +41 -2
  384. package/src/prerender.ts +39 -18
  385. package/src/public-dir.ts +27 -8
  386. package/src/query.ts +107 -26
  387. package/src/render-seam.ts +30 -2
  388. package/src/route-types.ts +34 -7
  389. package/src/router.ts +66 -59
  390. package/src/seo.ts +68 -0
  391. package/src/server-components/build-info.ts +11 -0
  392. package/src/server-components/build-target.ts +194 -0
  393. package/src/server-components/build.ts +419 -0
  394. package/src/server-components/bundle-proof.ts +33 -0
  395. package/src/server-components/client.ts +663 -0
  396. package/src/server-components/definition.ts +125 -0
  397. package/src/server-components/dev-http.ts +75 -0
  398. package/src/server-components/error.ts +24 -0
  399. package/src/server-components/frames.ts +181 -0
  400. package/src/server-components/html-policy.ts +94 -0
  401. package/src/server-components/html.ts +81 -0
  402. package/src/server-components/islands.ts +111 -0
  403. package/src/server-components/native.ts +101 -0
  404. package/src/server-components/portal.ts +42 -0
  405. package/src/server-components/props.ts +19 -0
  406. package/src/server-components/protocol.ts +167 -0
  407. package/src/server-components/range.ts +313 -0
  408. package/src/server-components/registry.ts +34 -0
  409. package/src/server-components/response.ts +44 -0
  410. package/src/server-components/router-integration.ts +40 -0
  411. package/src/server-components/runtime.ts +376 -0
  412. package/src/server-components/scheduler.ts +84 -0
  413. package/src/server-components/schema.ts +69 -0
  414. package/src/server-components/server-entry.ts +98 -0
  415. package/src/server-components/source.ts +324 -0
  416. package/src/server-components/state.ts +143 -0
  417. package/src/server-components/stream.ts +71 -0
  418. package/src/server-components/trusted-html.ts +21 -0
  419. package/src/server-components/types.ts +138 -0
  420. package/src/server-components/typescript.ts +39 -0
  421. package/src/server-components/value.ts +82 -0
  422. package/src/server-components/vite.ts +242 -0
  423. package/src/server-components.ts +32 -0
  424. package/src/service-worker.ts +24 -7
  425. package/src/vite.ts +8 -1
  426. package/src/zones.ts +418 -32
@@ -10,11 +10,14 @@
10
10
  import { statSync } from "node:fs"
11
11
  import { dirname, isAbsolute, relative, resolve } from "node:path"
12
12
  import type { BunPlugin } from "bun"
13
+ import { installedManifest } from "../internal/package-entries.ts"
13
14
  import { privateEnvDenial } from "../internal/private-env.ts"
15
+ import { withoutQuery } from "../internal/without-query.ts"
14
16
  import {
15
17
  BACKEND_ONLY_MARKER,
16
18
  browserDenial,
17
19
  createZoneClassifier,
20
+ specifierPackage,
18
21
  type ZoneClassifier,
19
22
  } from "../zones.ts"
20
23
 
@@ -58,6 +61,40 @@ export interface ZoneGuardOptions {
58
61
  /** Zones whose code runs in a browser, so may read only public environment variables. */
59
62
  const BROWSER_CODE = new Set<string>(["route-frontend", "frontend", "shared"])
60
63
 
64
+ const loaderFor = (file: string): "ts" | "tsx" | "js" | "jsx" | undefined => {
65
+ const extension = /\.[cm]?([jt]sx?)$/.exec(file)?.[1]
66
+ return extension === "ts" || extension === "tsx" || extension === "js" || extension === "jsx"
67
+ ? extension
68
+ : undefined
69
+ }
70
+
71
+ /**
72
+ * Why browser code at `file` may not make one of its package imports: one naming an entry its package
73
+ * declares backend-only, or one its per-entry map leaves out. A path cannot tell two entries that share
74
+ * a file apart, so the dev server judges the specifier here, where it is written.
75
+ */
76
+ function entryDenial(classifier: ZoneClassifier, file: string, source: string): string | undefined {
77
+ const loader = loaderFor(file)
78
+ if (loader === undefined) return undefined
79
+ let imports: ReadonlyArray<{ readonly path: string }>
80
+ try {
81
+ imports = new Bun.Transpiler({ loader }).scanImports(source)
82
+ } catch {
83
+ // Source Bun cannot read fails its own load with the parse error.
84
+ return undefined
85
+ }
86
+ for (const { path } of imports) {
87
+ const name = specifierPackage(path)
88
+ const manifest = name === undefined ? undefined : installedManifest(dirname(file), name)
89
+ if (manifest === undefined) continue
90
+ const classification = classifier.classify(manifest, path)
91
+ if (classification.entry === undefined) continue
92
+ const reason = browserDenial(classification, path)
93
+ if (reason !== undefined) return `it imports "${path}": ${reason}`
94
+ }
95
+ return undefined
96
+ }
97
+
61
98
  export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
62
99
  const classifier = options.classifier ?? createZoneClassifier(options)
63
100
  const display = (file: string): string => {
@@ -86,14 +123,16 @@ export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
86
123
  BROWSER_CODE.has(classification.zone)
87
124
  ) {
88
125
  const source = await Bun.file(args.path).text()
89
- reason = privateEnvDenial(args.path, source, options.publicEnvPrefix ?? "PUBLIC_")
126
+ reason =
127
+ privateEnvDenial(args.path, source, options.publicEnvPrefix ?? "PUBLIC_") ??
128
+ entryDenial(classifier, args.path, source)
90
129
  }
91
130
  return reason === undefined ? undefined : refuse(args.path, reason)
92
131
  })
93
132
  build.onResolve({ filter: NON_SOURCE_PATH }, (args) => {
94
133
  // An entry point (a dev server's HTML page) is the build's own choice, not an import.
95
134
  if (args.importer === "") return undefined
96
- const file = resolve(dirname(args.importer), args.path.replace(/[?#].*$/, ""))
135
+ const file = resolve(dirname(args.importer), withoutQuery(args.path))
97
136
  // No such file means an extensionless import of a dotted source name (`./date.utils`): Bun resolves
98
137
  // it to the source file, which onLoad judges. Judging the bare path would refuse a file never loaded.
99
138
  if (!isFile(file)) return undefined
package/src/prerender.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * routes are not supported.
10
10
  */
11
11
  import { mkdirSync, writeFileSync } from "node:fs"
12
- import { dirname, resolve, sep } from "node:path"
12
+ import { dirname, join, resolve, sep } from "node:path"
13
13
  import { assertNoSecrets, buildEnvironment, type SecretExemption } from "./internal/secret-scan.ts"
14
14
  import { fillRoutePattern, type RouteEntry } from "./manifest.ts"
15
15
  import { DATA_HEADER, PRERENDERED_GLOBAL, PRERENDERED_LIST_PATH } from "./router.ts"
@@ -70,10 +70,32 @@ function trimTrailingSlashes(path: string): string {
70
70
  return path.slice(0, end)
71
71
  }
72
72
 
73
- /** Map a route path to its output file: `/` → `index.html`, `/a/b` → `a/b/index.html`. */
73
+ /**
74
+ * One request-path segment as the file name a static host looks up, which is the segment decoded, or
75
+ * `undefined` when no file name maps back to it: malformed percent-encoding, `.` or `..`, or a decoded
76
+ * `/`, `\` or NUL.
77
+ */
78
+ function fileSegment(segment: string): string | undefined {
79
+ let decoded: string
80
+ try {
81
+ decoded = decodeURIComponent(segment)
82
+ } catch {
83
+ return undefined
84
+ }
85
+ if (decoded === "." || decoded === ".." || /[/\\\0]/.test(decoded)) return undefined
86
+ return decoded
87
+ }
88
+
89
+ /**
90
+ * Map a route path to its output file: `/` → `index.html`, `/a/b` → `a/b/index.html`. Segments are
91
+ * written decoded (`/caf%C3%A9` → `café/index.html`) because a static host decodes the request path
92
+ * before it looks for a file; a segment with no such file name stays as written.
93
+ */
74
94
  export function htmlFileFor(pattern: string): string {
75
95
  const trimmed = trimTrailingSlashes(pattern.replace(/^\/+/, ""))
76
- return trimmed === "" ? "index.html" : `${trimmed}/index.html`
96
+ if (trimmed === "") return "index.html"
97
+ const segments = trimmed.split("/").map((segment) => fileSegment(segment) ?? segment)
98
+ return `${segments.join("/")}/index.html`
77
99
  }
78
100
 
79
101
  // The handover of a page whose prerendered set is served as a list rather than inlined.
@@ -119,6 +141,16 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
119
141
  // static (one path) and dynamic (one path per getStaticPaths entry) routes. A failed render is a skip;
120
142
  // a credential in the output fails the whole prerender.
121
143
  const renderPath = async (path: string): Promise<void> => {
144
+ // Refused before rendering: a segment no file name maps back to, or a file outside `outDir`.
145
+ const file = htmlFileFor(path)
146
+ const segments = trimTrailingSlashes(path.replace(/^\/+/, "")).split("/")
147
+ const abs = segments.every((s) => s === "" || fileSegment(s) !== undefined)
148
+ ? outputPath(outRoot, file)
149
+ : undefined
150
+ if (abs === undefined) {
151
+ skipped.push({ path, reason: "unsafe output path" })
152
+ return
153
+ }
122
154
  const res = await options.app.fetch(new Request(`${origin}${path}`))
123
155
  if (!res.ok) {
124
156
  // A failed render is a per-route skip, not a build failure - surfaced for the caller to log.
@@ -127,12 +159,6 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
127
159
  }
128
160
  const html = await res.text()
129
161
  listUrl ??= PRERENDERED_LIST_URL.exec(html)?.[1]
130
- const file = htmlFileFor(path)
131
- const abs = outputPath(outRoot, file)
132
- if (abs === undefined) {
133
- skipped.push({ path, reason: "unsafe output path" })
134
- return
135
- }
136
162
  assertClean(file, html)
137
163
  mkdirSync(dirname(abs), { recursive: true })
138
164
  writeFileSync(abs, html)
@@ -150,15 +176,10 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
150
176
  !(dataRes.headers.get("content-type") ?? "").includes("application/x-ndjson")
151
177
  ) {
152
178
  dataFile = dataFileFor(path)
153
- const dataAbs = outputPath(outRoot, dataFile)
154
- if (dataAbs === undefined) {
155
- await dataRes.body?.cancel()
156
- dataFile = undefined
157
- } else {
158
- const data = await dataRes.text()
159
- assertClean(dataFile, data)
160
- writeFileSync(dataAbs, data)
161
- }
179
+ const data = await dataRes.text()
180
+ assertClean(dataFile, data)
181
+ // Beside `index.html`, so inside `outDir` whenever it is.
182
+ writeFileSync(join(dirname(abs), "_data.json"), data)
162
183
  } else {
163
184
  await dataRes.body?.cancel() // not emitting (non-OK or NDJSON) - release the stream
164
185
  }
package/src/public-dir.ts CHANGED
@@ -12,12 +12,12 @@
12
12
  * Distinct from `publicPath` in `build.ts`, which is the URL prefix for content-hashed bundle chunks.
13
13
  * The names collide and the concepts do not: `publicPath` never covers user-authored files.
14
14
  */
15
- import { constants as FS } from "node:fs"
16
- import { open, realpath } from "node:fs/promises"
15
+ import { realpath } from "node:fs/promises"
17
16
  import { extname, normalize, resolve, sep } from "node:path"
18
17
  import { Readable } from "node:stream"
19
18
  import { parseByteRange } from "@nifrajs/core/range"
20
19
  import { pathnameOf } from "@nifrajs/core/server"
20
+ import { openResolved } from "./open-resolved.ts"
21
21
 
22
22
  /** How long each subtree may be cached. Content-hashed bundle output can be immutable; a
23
23
  * user-authored file keeps its name across deploys, so it gets a day and a revalidation. */
@@ -31,8 +31,15 @@ export interface PublicDirCache {
31
31
  export interface ServePublicDirOptions {
32
32
  /** Absolute path of the directory to serve. */
33
33
  readonly dir: string
34
- /** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`). */
34
+ /** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`).
35
+ * Ignored when `hashedFiles` is set. */
35
36
  readonly hashedPrefix?: string
37
+ /**
38
+ * Encoded URL paths of the content-hashed files, such as a build manifest's `assets`. When set, only
39
+ * these are cached immutably and every other file gets the `assets` policy, so a user-authored file
40
+ * that happens to sit under the hashed prefix still revalidates.
41
+ */
42
+ readonly hashedFiles?: ReadonlySet<string>
36
43
  readonly cache?: PublicDirCache
37
44
  /** Optional encoded URL-path allowlist. When present, route misses avoid a filesystem probe. */
38
45
  readonly files?: ReadonlySet<string>
@@ -166,6 +173,7 @@ export function servePublicDir(
166
173
  const root = resolve(options.dir)
167
174
  const rootReal = realpath(root).catch(() => undefined)
168
175
  const hashedPrefix = options.hashedPrefix ?? "/assets/"
176
+ const hashedFiles = options.hashedFiles
169
177
  const hashed = options.cache?.hashed ?? IMMUTABLE
170
178
  const assets = options.cache?.assets ?? ONE_DAY
171
179
 
@@ -192,16 +200,27 @@ export function servePublicDir(
192
200
  }
193
201
  // Open the canonical path once and stream from that descriptor. Reopening the pathname after
194
202
  // realpath containment checks would let a writable public tree swap a symlink between validation
195
- // and the actual read. O_NOFOLLOW also rejects a final-component symlink if one appears before
196
- // the open; the descriptor then pins the bytes for the lifetime of the response.
197
- let handle: Awaited<ReturnType<typeof open>> | undefined
203
+ // and the actual read. openResolved also refuses the name once any component of it became a
204
+ // symlink; the descriptor then pins the bytes for the lifetime of the response.
205
+ let handle: Awaited<ReturnType<typeof openResolved>> | undefined
198
206
  try {
199
- handle = await open(resolvedFile, FS.O_RDONLY | FS.O_NOFOLLOW)
207
+ try {
208
+ handle = await openResolved(resolvedFile)
209
+ } catch {
210
+ // A name refused or gone since realpath falls through, as a failed realpath does.
211
+ return undefined
212
+ }
200
213
  const stat = await handle.stat()
201
214
  if (!stat.isFile()) return undefined
202
215
  const size = stat.size
203
216
  const headers = new Headers({
204
- "cache-control": pathname.startsWith(hashedPrefix) ? hashed : assets,
217
+ "cache-control": (
218
+ hashedFiles === undefined
219
+ ? pathname.startsWith(hashedPrefix)
220
+ : hashedFiles.has(pathname)
221
+ )
222
+ ? hashed
223
+ : assets,
205
224
  // Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
206
225
  // so a video or audio file under `public/` is scrubbable only once this header is present.
207
226
  "accept-ranges": "bytes",
package/src/query.ts CHANGED
@@ -111,6 +111,12 @@ export interface DehydratedState {
111
111
  readonly data: unknown
112
112
  readonly updatedAt: number
113
113
  }>
114
+ /** The successful infinite queries, pages and page params. */
115
+ readonly infiniteQueries?: ReadonlyArray<{
116
+ readonly key: unknown
117
+ readonly data: InfiniteData<unknown, unknown>
118
+ readonly updatedAt: number
119
+ }>
114
120
  }
115
121
 
116
122
  /** The keyed query cache. One per app (a binding registers it like the router). */
@@ -125,8 +131,9 @@ export interface QueryClient {
125
131
  fn: (pageParam: P) => Promise<T>,
126
132
  options: InfiniteQueryOptions<T, P>,
127
133
  ) => InfiniteQueryHandle<T, P>
128
- /** Mark matching cached queries stale and refetch the **mounted** ones (subscribers > 0). Array
129
- * keys match by **prefix** (`["todo"]` ⇒ every `["todo", …]`); other keys match exactly. */
134
+ /** Mark matching cached queries, infinite ones included, stale and refetch the **mounted** ones
135
+ * (subscribers > 0). Array keys match by **prefix** (`["todo"]` ⇒ every `["todo", …]`); other keys
136
+ * match exactly. */
130
137
  invalidateQueries: (keyOrPrefix: unknown) => void
131
138
  /** Read a cached query's data (`undefined` if absent or not yet successful). Synchronous, no fetch. */
132
139
  getQueryData: <T>(key: unknown) => T | undefined
@@ -218,17 +225,24 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
218
225
 
219
226
  // Lazily evict entries whose GC timer has elapsed (no subscribers), then enforce the hard cap by
220
227
  // dropping the oldest-inserted unsubscribed entries. Called on every `query()` - no background timer.
221
- const sweep = (): void => {
222
- const t = now()
223
- for (const [h, e] of cache) {
224
- if (e.subscribers === 0 && e.gcAt !== undefined && t >= e.gcAt) cache.delete(h)
228
+ const sweepEntries = (
229
+ entries: Map<string, { readonly subscribers: number; readonly gcAt: number | undefined }>,
230
+ t: number,
231
+ ): void => {
232
+ for (const [h, e] of entries) {
233
+ if (e.subscribers === 0 && e.gcAt !== undefined && t >= e.gcAt) entries.delete(h)
225
234
  }
226
- if (cache.size <= max) return
227
- for (const [h, e] of cache) {
228
- if (cache.size <= max) break
229
- if (e.subscribers === 0) cache.delete(h) // oldest-inserted unsubscribed first (Map order)
235
+ if (entries.size <= max) return
236
+ for (const [h, e] of entries) {
237
+ if (entries.size <= max) break
238
+ if (e.subscribers === 0) entries.delete(h) // oldest-inserted unsubscribed first (Map order)
230
239
  }
231
240
  }
241
+ const sweep = (): void => {
242
+ const t = now()
243
+ sweepEntries(cache, t)
244
+ sweepEntries(infinite, t)
245
+ }
232
246
 
233
247
  // Build a cache entry + its handle. `fn`/`staleTime` are mutated in place on re-bind (closures change
234
248
  // between renders). All the fetch/dedup/generation machinery lives here so query(), prefetchQuery(),
@@ -348,6 +362,9 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
348
362
  entry.generation++
349
363
  if (entry.subscribers > 0) void entry.handle.refetch().catch(() => {}) // refresh mounted now (best-effort)
350
364
  }
365
+ for (const entry of infinite.values()) {
366
+ if (keyMatchesPrefix(entry.key, keyOrPrefix)) entry.invalidate()
367
+ }
351
368
  }
352
369
 
353
370
  const getQueryData = <T>(key: unknown): T | undefined => {
@@ -392,7 +409,17 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
392
409
  if (entry.state.status === "success")
393
410
  queries.push({ key: entry.key, data: entry.state.data, updatedAt: entry.state.updatedAt })
394
411
  }
395
- return { queries }
412
+ const infiniteQueries: Array<{
413
+ key: unknown
414
+ data: InfiniteData<unknown, unknown>
415
+ updatedAt: number
416
+ }> = []
417
+ for (const entry of infinite.values()) {
418
+ const { status, data, updatedAt } = entry.handle.snapshot()
419
+ if (status === "success" && data !== undefined)
420
+ infiniteQueries.push({ key: entry.key, data, updatedAt })
421
+ }
422
+ return { queries, infiniteQueries }
396
423
  }
397
424
 
398
425
  const hydrate = (state: DehydratedState): void => {
@@ -409,6 +436,14 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
409
436
  if (entry.state.status !== "success" || q.updatedAt > entry.state.updatedAt)
410
437
  entry.setState(successState(q.data, q.updatedAt))
411
438
  }
439
+ // An infinite entry needs its page options before it can exist, so a snapshot for a key not yet
440
+ // queried waits here until `infiniteQuery` creates it.
441
+ for (const q of state.infiniteQueries ?? []) {
442
+ const h = hashQueryKey(q.key)
443
+ const existing = infinite.get(h)
444
+ if (existing === undefined) seeded.set(h, q)
445
+ else existing.seed(q.data, q.updatedAt)
446
+ }
412
447
  }
413
448
 
414
449
  // --- Infinite (paged) queries -----------------------------------------------------------------
@@ -427,19 +462,33 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
427
462
  )
428
463
  return existing.handle as InfiniteQueryHandle<T, P>
429
464
  }
430
- const created = makeInfinite<T, P>(fn, opts)
465
+ const created = makeInfinite<T, P>(key, fn, opts)
431
466
  infinite.set(h, created as InfiniteEntry<unknown, unknown>)
467
+ const snapshot = seeded.get(h)
468
+ if (snapshot !== undefined) {
469
+ seeded.delete(h)
470
+ created.seed(snapshot.data, snapshot.updatedAt)
471
+ }
432
472
  return created.handle
433
473
  }
434
474
 
435
475
  // Infinite entries live in their own map (their data shape + fetch model differ from plain queries).
436
476
  interface InfiniteEntry<T, P> {
477
+ readonly key: unknown
437
478
  handle: InfiniteQueryHandle<T, P>
438
479
  rebind: (fn: (p: P) => Promise<T>, opts: InfiniteQueryOptions<T, P>) => void
480
+ /** Mark stale, drop an in-flight fetch's result, and refetch when mounted. */
481
+ invalidate: () => void
482
+ /** Take a dehydrated snapshot unless the entry already holds fresher data. */
483
+ seed: (data: InfiniteData<unknown, unknown>, updatedAt: number) => void
484
+ subscribers: number
485
+ gcAt: number | undefined
439
486
  }
440
487
  const infinite = new Map<string, InfiniteEntry<unknown, unknown>>()
488
+ const seeded = new Map<string, NonNullable<DehydratedState["infiniteQueries"]>[number]>()
441
489
 
442
490
  const makeInfinite = <T, P>(
491
+ key: unknown,
443
492
  fetchPage: (pageParam: P) => Promise<T>,
444
493
  initialOpts: InfiniteQueryOptions<T, P>,
445
494
  ): InfiniteEntry<T, P> => {
@@ -447,6 +496,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
447
496
  let opts = initialOpts
448
497
  let state: QueryState<InfiniteData<T, P>> = PENDING as QueryState<InfiniteData<T, P>>
449
498
  let generation = 0
499
+ let invalidated = false
450
500
  let promise: Promise<InfiniteData<T, P>> | undefined
451
501
  const listeners = new Set<() => void>()
452
502
  const emit = (): void => {
@@ -475,7 +525,8 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
475
525
  data.pageParams,
476
526
  )
477
527
  }
478
- const isStale = (): boolean => now() - state.updatedAt >= (opts.staleTime ?? defaultStaleTime)
528
+ const isStale = (): boolean =>
529
+ invalidated || now() - state.updatedAt >= (opts.staleTime ?? defaultStaleTime)
479
530
 
480
531
  // Run a paged operation under a fresh generation, guarding against a superseding op (like `run`).
481
532
  const runPaged = (build: () => Promise<InfiniteData<T, P>>): Promise<InfiniteData<T, P>> => {
@@ -486,6 +537,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
486
537
  try {
487
538
  const data = await build()
488
539
  if (gen !== generation) return data
540
+ invalidated = false
489
541
  setState(successState(data, now()))
490
542
  return data
491
543
  } catch (error) {
@@ -511,9 +563,13 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
511
563
  const handle: InfiniteQueryHandle<T, P> = {
512
564
  snapshot: () => state,
513
565
  subscribe: (listener) => {
566
+ entry.subscribers++
567
+ entry.gcAt = undefined
514
568
  listeners.add(listener)
515
569
  return () => {
516
570
  listeners.delete(listener)
571
+ entry.subscribers--
572
+ if (entry.subscribers === 0) entry.gcAt = now() + gcTime
517
573
  }
518
574
  },
519
575
  fetch: () => fetchFirst(false),
@@ -558,13 +614,30 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
558
614
  return p !== undefined && p !== null
559
615
  },
560
616
  }
561
- return {
562
- handle: handle as InfiniteQueryHandle<unknown, unknown>,
617
+ const entry: InfiniteEntry<T, P> = {
618
+ key,
619
+ handle,
563
620
  rebind: (nextFn, nextOpts) => {
564
621
  fn = nextFn
565
622
  opts = nextOpts
566
623
  },
567
- } as InfiniteEntry<T, P>
624
+ invalidate: () => {
625
+ invalidated = true
626
+ // A fetch begun before this point read pre-mutation data: it may not publish, and a new fetch
627
+ // must not join it.
628
+ generation++
629
+ promise = undefined
630
+ if (entry.subscribers > 0) void handle.refetch().catch(() => {})
631
+ },
632
+ seed: (data, updatedAt) => {
633
+ if (state.status === "success" && state.updatedAt >= updatedAt) return
634
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the snapshot was dehydrated from an entry under this key, whose pages and params are T and P
635
+ setState(successState(data as InfiniteData<T, P>, updatedAt))
636
+ },
637
+ subscribers: 0,
638
+ gcAt: undefined,
639
+ }
640
+ return entry
568
641
  }
569
642
 
570
643
  return {
@@ -609,8 +682,10 @@ export interface MutationCallbacks<TData, TVariables> {
609
682
  export interface MutationHandle<TData, TVariables> {
610
683
  snapshot: () => MutationState<TData, TVariables>
611
684
  subscribe: (listener: () => void) => () => void
612
- /** Run the mutation. Resolves to the data (and runs onSuccess/onSettled) or rejects (onError/onSettled).
613
- * The latest concurrent call wins the published state (older results are dropped). */
685
+ /** Run the mutation. Resolves to the data (and runs onSuccess/onSettled) or rejects (onError/onSettled);
686
+ * a throwing `onMutate` fails it the same way. A throwing `onSuccess` or `onSettled` after the write
687
+ * rejects the call but leaves the state `success` and never calls `onError`. The latest concurrent
688
+ * call wins the published state (older results are dropped). */
614
689
  mutate: (variables: TVariables) => Promise<TData>
615
690
  /** Rebind the latest `fn`/callbacks (closures change between renders) without losing state. */
616
691
  rebind: (
@@ -667,21 +742,27 @@ export function createMutation<TData, TVariables>(
667
742
  mutate: async (variables) => {
668
743
  const mine = ++latest
669
744
  setState({ status: "pending", data: undefined, error: undefined, variables })
670
- // Only await a callback when one is actually supplied - an `await undefined` would insert a
671
- // needless microtask before the mutationFn runs (and complicates single-flight timing).
672
- if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
745
+ let data: TData
673
746
  try {
674
- const data = await mutationFn(variables)
675
- if (mine === latest) setState({ status: "success", data, error: undefined, variables })
676
- if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
677
- if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
678
- return data
747
+ // Only await a callback when one is actually supplied - an `await undefined` would insert a
748
+ // needless microtask before the mutationFn runs (and complicates single-flight timing).
749
+ if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
750
+ data = await mutationFn(variables)
679
751
  } catch (error) {
680
752
  if (mine === latest) setState({ status: "error", data: undefined, error, variables })
681
753
  if (cbs.onError !== undefined) await cbs.onError(error, variables)
682
754
  if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
683
755
  throw error
684
756
  }
757
+ if (mine === latest) setState({ status: "success", data, error: undefined, variables })
758
+ // The write happened: a success callback that throws rejects the call, but is no failure of the
759
+ // write, so it never reaches onError (which would roll back what the server kept).
760
+ try {
761
+ if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
762
+ } finally {
763
+ if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
764
+ }
765
+ return data
685
766
  },
686
767
  }
687
768
  }
@@ -12,6 +12,7 @@ import type { Submission } from "./router.ts"
12
12
 
13
13
  /** The data handed to a route component. */
14
14
  export interface RenderProps {
15
+ readonly serverComponents?: import("./server-components/types.ts").ComponentState
15
16
  readonly data: unknown
16
17
  /** Per-layout loader data, aligned index-for-index with the layout prefix of `chain`. */
17
18
  readonly layoutData?: readonly unknown[]
@@ -61,10 +62,24 @@ export interface UIMatch {
61
62
  readonly handle: unknown
62
63
  }
63
64
 
65
+ /**
66
+ * Markup a render produced for outside the app root, which an adapter that sets
67
+ * {@link RenderAdapter.documentParts} appends to. Each part must be complete before the render
68
+ * settles: `renderToString` resolving, or `renderToStream` returning its stream.
69
+ */
70
+ export interface DocumentParts {
71
+ /** Placed at the end of `<head>`. Svelte's `<svelte:head>` lands here. */
72
+ head: string
73
+ /** Placed first in `<body>`, before the app root. Vue's `<Teleport to="body">` lands here. */
74
+ body: string
75
+ }
76
+
64
77
  /** Per-document options for {@link RenderAdapter.renderToStream}. */
65
78
  export interface RenderStreamOptions {
66
79
  /** The document's CSP nonce, for every executable `<script>` the framework streams. */
67
80
  readonly nonce?: string
81
+ /** Present only for an adapter that sets {@link RenderAdapter.documentParts}. */
82
+ readonly parts?: DocumentParts
68
83
  }
69
84
 
70
85
  /**
@@ -83,10 +98,22 @@ export interface RenderAdapter {
83
98
  props: RenderProps,
84
99
  options?: RenderStreamOptions,
85
100
  ): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
86
- /** Render the chain to a complete HTML string when no content is deferred. */
87
- renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
101
+ /**
102
+ * Render the chain to a complete HTML string when no content is deferred. `options.nonce` is the
103
+ * document's CSP nonce, for any inline script the renderer emits (Solid's hydration data).
104
+ */
105
+ renderToString?(
106
+ chain: readonly unknown[],
107
+ props: RenderProps,
108
+ options?: RenderStreamOptions,
109
+ ): string | Promise<string>
88
110
  /** Per-document bootstrap markup injected into `<head>` for client hydration. */
89
111
  hydrationHead(nonce?: string): string
112
+ /**
113
+ * Set when a render can produce markup that belongs outside the app root. Every render is then
114
+ * passed `options.parts` to fill; an adapter without it never is, and pays nothing for the seam.
115
+ */
116
+ readonly documentParts?: boolean
90
117
  }
91
118
 
92
119
  /** Loads a module through the dev server's module graph rather than the runtime's resolver. */
@@ -123,5 +150,6 @@ export const ROUTE_GLOBAL = "__NIFRA_ROUTE__"
123
150
  export const ACTION_GLOBAL = "__NIFRA_ACTION__"
124
151
  /** Dynamic-boundary states for hydration; absent when a route declares no boundaries. */
125
152
  export const BOUNDARY_GLOBAL = "__NIFRA_BOUNDARIES__"
153
+ export const COMPONENT_GLOBAL = "__NIFRA_COMPONENTS__"
126
154
  /** Marker attribute used to find a non-default hydration container. */
127
155
  export const ROOT_ATTRIBUTE = "data-nifra-root"
@@ -30,6 +30,7 @@ import {
30
30
  } from "node:fs"
31
31
  import { dirname, join, relative } from "node:path"
32
32
  import { codeUnitOrder } from "./internal/code-unit-order.ts"
33
+ import { jsStringLiteral } from "./internal/js-string.ts"
33
34
  import {
34
35
  buildManifest,
35
36
  isMiddlewareFile,
@@ -85,6 +86,8 @@ function routeFiles(manifest: Manifest): readonly RouteFile[] {
85
86
  const PARAM = /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/
86
87
  const OPTIONAL = /^\[\[([A-Za-z_][A-Za-z0-9_]*)\]\]$/
87
88
  const CATCH_ALL = /^\[\.\.\.([A-Za-z_][A-Za-z0-9_]*)\]$/
89
+ // A `[name]` inside a part-literal segment such as `post-[id]` or `[lang]-feed.xml`.
90
+ const EMBEDDED_PARAM = /\[([A-Za-z_][A-Za-z0-9_]*)\]/g
88
91
 
89
92
  /** The params a route file's URL carries: its directories', plus its own name's for a page. */
90
93
  function paramsType(file: string, kind: Kind): string {
@@ -96,10 +99,20 @@ function paramsType(file: string, kind: Kind): string {
96
99
  const optional = OPTIONAL.exec(segment)
97
100
  const name = optional?.[1] ?? PARAM.exec(segment)?.[1] ?? CATCH_ALL.exec(segment)?.[1]
98
101
  if (name !== undefined) fields.push(`readonly ${name}${optional === null ? "" : "?"}: string`)
102
+ else
103
+ for (const embedded of segment.matchAll(EMBEDDED_PARAM))
104
+ fields.push(`readonly ${embedded[1]}: string`)
99
105
  }
100
106
  return fields.length === 0 ? "Record<string, never>" : `{ ${fields.join("; ")} }`
101
107
  }
102
108
 
109
+ // A file name may hold a line terminator, which would end the `//` header comment it is quoted in.
110
+ const commentSafe = (text: string): string =>
111
+ text.replace(
112
+ /[\n\r\u2028\u2029]/g,
113
+ (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`,
114
+ )
115
+
103
116
  /** A module specifier for `target` from a file in `fromDir`, without a script extension. */
104
117
  function specifier(fromDir: string, target: string): string {
105
118
  let path = posix(relative(fromDir, target))
@@ -121,16 +134,24 @@ function routeTypesModule(
121
134
  const backend = route.backend === undefined ? undefined : join(routesDir, route.backend)
122
135
  // Only a script module's exports are visible to TypeScript; a .svelte/.vue/.mdx one is an ambient shim.
123
136
  const frontend = /\.[jt]sx?$/.test(route.file) ? join(routesDir, route.file) : undefined
124
- const lines = [`// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`]
137
+ const lines = [
138
+ `// Generated by nifra from ${commentSafe(label)}. Do not edit: \`nifra types\` rewrites it.`,
139
+ ]
125
140
  if (register !== undefined)
126
141
  lines.push(`/// <reference path="${posix(relative(dir, register))}" />`)
127
- lines.push('import type { OutputOf, RouteLoaderArgs, SearchSchemaOf } from "@nifrajs/client"')
128
- if (route.kind === "page") lines.push('import type { RenderProps } from "@nifrajs/web"')
142
+ lines.push(
143
+ 'import type { OutputOf, RouteActionReturn, RouteLoaderArgs, RouteLoaderReturn, SearchSchemaOf } from "@nifrajs/client"',
144
+ )
145
+ lines.push(
146
+ route.kind === "page"
147
+ ? 'import type { MetaArgs as WebMetaArgs, RenderProps } from "@nifrajs/web"'
148
+ : 'import type { MetaArgs as WebMetaArgs } from "@nifrajs/web"',
149
+ )
129
150
  lines.push("")
130
151
  if (backend !== undefined)
131
- lines.push(`type Backend = typeof import("${specifier(dir, backend)}")`)
152
+ lines.push(`type Backend = typeof import(${jsStringLiteral(specifier(dir, backend))})`)
132
153
  if (frontend !== undefined)
133
- lines.push(`type Frontend = typeof import("${specifier(dir, frontend)}")`)
154
+ lines.push(`type Frontend = typeof import(${jsStringLiteral(specifier(dir, frontend))})`)
134
155
  if (backend !== undefined || frontend !== undefined) lines.push("")
135
156
  const output = (name: string) => (backend === undefined ? "null" : `OutputOf<Backend, "${name}">`)
136
157
  lines.push(
@@ -145,6 +166,12 @@ function routeTypesModule(
145
166
  ` export type LoaderArgs = RouteLoaderArgs<Params${frontend === undefined ? "" : ", SearchSchemaOf<Frontend>"}>`,
146
167
  " /** The context the backend half's action receives. */",
147
168
  " export type ActionArgs = LoaderArgs",
169
+ " /** The backend half's loader. Annotate it, `export const loader: Route.Loader = ...`, and what it returns is checked against `loaderOutput`. */",
170
+ ` export type Loader = (args: LoaderArgs) => RouteLoaderReturn<${backend === undefined ? "null" : "Backend"}>`,
171
+ " /** The backend half's action. Annotate it, `export const action: Route.Action = ...`, and what it returns is checked against `actionOutput`. */",
172
+ ` export type Action = (args: ActionArgs) => RouteActionReturn<${backend === undefined ? "null" : "Backend"}>`,
173
+ " /** What this module's `meta` function receives: its own loader data as `data`. */",
174
+ " export type MetaArgs = WebMetaArgs<LoaderData>",
148
175
  )
149
176
  if (route.kind === "page") {
150
177
  lines.push(
@@ -170,8 +197,8 @@ function routeTypesModule(
170
197
 
171
198
  function registerModule(out: string, backendApp: string, label: string): string {
172
199
  return [
173
- `// Generated by nifra from ${label}. Do not edit: \`nifra types\` rewrites it.`,
174
- `import type { backend } from "${specifier(dirname(out), backendApp)}"`,
200
+ `// Generated by nifra from ${commentSafe(label)}. Do not edit: \`nifra types\` rewrites it.`,
201
+ `import type { backend } from ${jsStringLiteral(specifier(dirname(out), backendApp))}`,
175
202
  "",
176
203
  'declare module "@nifrajs/client" {',
177
204
  " interface Register {",