@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
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[]
@@ -123,5 +124,6 @@ export const ROUTE_GLOBAL = "__NIFRA_ROUTE__"
123
124
  export const ACTION_GLOBAL = "__NIFRA_ACTION__"
124
125
  /** Dynamic-boundary states for hydration; absent when a route declares no boundaries. */
125
126
  export const BOUNDARY_GLOBAL = "__NIFRA_BOUNDARIES__"
127
+ export const COMPONENT_GLOBAL = "__NIFRA_COMPONENTS__"
126
128
  /** Marker attribute used to find a non-default hydration container. */
127
129
  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 {",
package/src/router.ts CHANGED
@@ -9,6 +9,8 @@ import { Router as CoreRouter } from "@nifrajs/core/router"
9
9
  import type { StandardSchemaV1 } from "@nifrajs/core/server"
10
10
  import type { BoundaryDescriptor, BoundaryStates } from "./boundary.ts"
11
11
  import { parseNdjsonData } from "./deferred.ts"
12
+ import { parsePageData } from "./internal/revive-dates.ts"
13
+ import { requestPattern } from "./internal/route-path.ts"
12
14
  import type { ClientActionResult, ClientRequestBody, ClientRouteHooks } from "./manifest.ts"
13
15
  import type { MatchChain } from "./render-seam.ts"
14
16
  import { isClientOnlySearchChange } from "./search.ts"
@@ -90,6 +92,7 @@ export const NAV_FROM_HEADER = "x-nifra-from"
90
92
  * structure rather than by assuming the deploy was atomic.
91
93
  */
92
94
  export interface RouteDataEnvelope {
95
+ readonly serverComponents?: import("./server-components/types.ts").ComponentState | undefined
93
96
  readonly v: 1
94
97
  readonly data: unknown
95
98
  readonly layoutData?: readonly unknown[]
@@ -129,6 +132,7 @@ export interface Submission {
129
132
 
130
133
  /** The router's observable state. A new object is published on every transition. */
131
134
  export interface RouterState {
135
+ readonly serverComponents?: import("./server-components/types.ts").ComponentState | undefined
132
136
  readonly routeId: string
133
137
  readonly params: Record<string, string>
134
138
  /** The current URL path (used to revalidate the active loader after an action). */
@@ -173,7 +177,7 @@ export function createMatcher(
173
177
  for (const pattern of patterns) {
174
178
  // Pass the literal to the public router seam: package type resolution may point the two core
175
179
  // subpaths at different source/dist declarations, while registration-time compilation is cheap.
176
- routeIndex.add("GET", pattern.pattern, pattern.routeId)
180
+ routeIndex.add("GET", requestPattern(pattern.pattern), pattern.routeId)
177
181
  }
178
182
  return (path) => {
179
183
  // Strip the query without allocating a `split("?")` array - matcher runs per match.
@@ -258,8 +262,9 @@ export interface ClientRouter {
258
262
  * loader is revalidated so the mutation is reflected. Pass `{ revalidate: false }` to SKIP that
259
263
  * revalidation - keep the current `data` and just publish the action's `actionData` (useful when
260
264
  * the action already returned everything that changed, saving the extra round-trip). A redirect
261
- * always loads its target regardless. Rejects on failure (caller falls back); once the action has
262
- * run, the rejection's `redirectTo` names the page to load instead of posting again.
265
+ * always loads its target regardless. Rejects on failure (caller falls back); once the server has
266
+ * answered, at any status, the rejection's `redirectTo` names the page to load instead of posting
267
+ * again.
263
268
  */
264
269
  submit: (
265
270
  action: string,
@@ -310,6 +315,10 @@ export interface ClientRouterOptions {
310
315
  /** Terminal status → client route id. Generated entries populate this from `_404` and
311
316
  * `_<status>` files so soft navigation renders the same boundary as a hard request. */
312
317
  readonly statusRoutes?: Readonly<Record<number, string>>
318
+ /** Status route ids whose page renders inside layouts (all but `inLayouts = false`), filled in by the generated entry's
319
+ * `loadModule`. A soft navigation that ends on one loads the URL as a document, as one ending on a
320
+ * nested `_404` does: only the server runs those layouts' loaders. */
321
+ readonly layoutStatusRoutes?: Readonly<Record<string, true>>
313
322
  /** routeId → the route's client-only search keys (its `searchClientKeys` export). When a soft
314
323
  * navigation stays on the same route + pathname and changes ONLY these keys, `navigate` publishes the
315
324
  * new URL WITHOUT re-running the loader (re-render, not revalidate). Populated lazily by the generated
@@ -346,7 +355,7 @@ export interface MountRouterOptions {
346
355
  const readResponseData = (res: Response, signal?: AbortSignal): Promise<unknown> =>
347
356
  (res.headers.get("content-type") ?? "").includes("application/x-ndjson") && res.body !== null
348
357
  ? parseNdjsonData(res.body, signal)
349
- : res.json()
358
+ : res.text().then(parsePageData)
350
359
 
351
360
  // Index scan, not `/\/+$/` - the unanchored-start trailing-run replace backtracks quadratically and
352
361
  // the pathname is caller-controlled.
@@ -404,8 +413,8 @@ const targetUrl = (to: string): URL | undefined => {
404
413
  }
405
414
  }
406
415
 
407
- const redirected = (to: string): Error =>
408
- Object.assign(new Error(`[nifra/web] redirected to ${to}`), { redirectTo: to })
416
+ const redirected = (to: string, cause?: unknown): Error =>
417
+ Object.assign(new Error(`[nifra/web] redirected to ${to}`, { cause }), { redirectTo: to })
409
418
 
410
419
  const defaultFetchData: FetchRouteData = async (path, _match, signal, navigation) => {
411
420
  // SSG fast path: if this path was prerendered, its loader data is a static file - fetch that (no
@@ -519,11 +528,18 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
519
528
  * an older build be read by newer client code.
520
529
  */
521
530
  type LoadedRouteData = {
531
+ readonly serverComponents?: import("./server-components/types.ts").ComponentState | undefined
522
532
  readonly data: unknown
523
533
  readonly layoutData?: readonly unknown[] | undefined
524
534
  readonly terminalRouteId?: string | undefined
525
535
  readonly boundaries?: BoundaryStates
526
536
  }
537
+ const dataFields = (value: LoadedRouteData) => ({
538
+ data: value.data,
539
+ layoutData: value.layoutData,
540
+ serverComponents: value.serverComponents,
541
+ ...(value.boundaries === undefined ? {} : { boundaries: value.boundaries }),
542
+ })
527
543
  const loadRouteData = async (
528
544
  path: string,
529
545
  match: { routeId: string; params: Record<string, string> },
@@ -551,24 +567,21 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
551
567
  }
552
568
  return { data: null, terminalRouteId }
553
569
  }
554
- if (retainFrom === undefined || payload.retained === undefined) {
555
- return {
556
- data: payload.data,
557
- layoutData: payload.layoutData,
558
- ...(payload.boundaries !== undefined ? { boundaries: payload.boundaries } : {}),
559
- }
570
+ let layoutData = payload.layoutData
571
+ if (retainFrom !== undefined && payload.retained !== undefined) {
572
+ const retained = new Set(
573
+ payload.retained.filter(
574
+ (index) => Number.isInteger(index) && index >= 0 && index < retainFrom.layoutData.length,
575
+ ),
576
+ )
577
+ layoutData = layoutData?.map((value, index) =>
578
+ retained.has(index) ? retainFrom.layoutData[index] : value,
579
+ )
560
580
  }
561
- const retained = new Set(
562
- payload.retained.filter(
563
- (index) => Number.isInteger(index) && index >= 0 && index < retainFrom.layoutData.length,
564
- ),
565
- )
566
- const layoutData = payload.layoutData?.map((value, index) =>
567
- retained.has(index) ? retainFrom.layoutData[index] : value,
568
- )
569
581
  return {
570
582
  data: payload.data,
571
583
  layoutData,
584
+ serverComponents: payload.serverComponents,
572
585
  ...(payload.boundaries !== undefined ? { boundaries: payload.boundaries } : {}),
573
586
  }
574
587
  }
@@ -644,11 +657,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
644
657
  navAbort?.abort()
645
658
  const ac = new AbortController()
646
659
  navAbort = ac
647
- const initial = {
648
- data: state.data,
649
- layoutData: state.layoutData,
650
- ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
651
- }
660
+ const initial = dataFields(state)
652
661
  const loaded = await applyClientLoader(
653
662
  state.path,
654
663
  route,
@@ -663,9 +672,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
663
672
  } else {
664
673
  state = {
665
674
  ...state,
666
- data: loaded.data,
667
- layoutData: loaded.layoutData,
668
- ...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
675
+ ...dataFields(loaded),
669
676
  }
670
677
  }
671
678
  emit()
@@ -707,6 +714,10 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
707
714
  }
708
715
  >()
709
716
  const cachePut = (path: string, loaded: LoadedRouteData): void => {
717
+ if (loaded.serverComponents !== undefined) {
718
+ cache.delete(path)
719
+ return
720
+ }
710
721
  if (!cache.has(path) && cache.size >= MAX_CACHE) {
711
722
  const oldest = cache.keys().next().value
712
723
  if (oldest !== undefined) cache.delete(oldest)
@@ -789,7 +800,14 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
789
800
  () => loadRouteData(at, route, signal, keepLayouts),
790
801
  prefetchedData,
791
802
  )
792
- if (loaded.terminalRouteId !== undefined) await loadModule?.(loaded.terminalRouteId)
803
+ if (loaded.terminalRouteId !== undefined) {
804
+ await loadModule?.(loaded.terminalRouteId)
805
+ if (options.layoutStatusRoutes?.[loaded.terminalRouteId] === true) {
806
+ throw new Error(
807
+ `[nifra/web] ${loaded.terminalRouteId} renders inside layouts; loading ${at} as a document`,
808
+ )
809
+ }
810
+ }
793
811
  return [at, route, loaded]
794
812
  } catch (error) {
795
813
  const next = hops < 20 ? appRedirect(redirectOf(error)) : undefined
@@ -817,9 +835,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
817
835
  cachePut(state.path, loaded)
818
836
  state = {
819
837
  ...state,
820
- data: loaded.data,
821
- layoutData: loaded.layoutData,
822
- ...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
838
+ ...dataFields(loaded),
823
839
  pending: false,
824
840
  }
825
841
  emit()
@@ -989,11 +1005,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
989
1005
  ) {
990
1006
  ++generation // supersede any in-flight navigation so its late result is dropped
991
1007
  navAbort?.abort()
992
- cachePut(path, {
993
- data: state.data,
994
- layoutData: state.layoutData,
995
- ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
996
- })
1008
+ cachePut(path, dataFields(state))
997
1009
  state = { ...state, path, pending: false, pendingPath: undefined }
998
1010
  emit()
999
1011
  return
@@ -1041,9 +1053,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1041
1053
  routeId: loaded.terminalRouteId ?? target.routeId,
1042
1054
  params: loaded.terminalRouteId === undefined ? target.params : {},
1043
1055
  path: to,
1044
- data: loaded.data,
1045
- layoutData: loaded.layoutData,
1046
- ...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
1056
+ ...dataFields(loaded),
1047
1057
  actionData: undefined, // a fresh navigation has no action result
1048
1058
  pending: false,
1049
1059
  }
@@ -1089,8 +1099,9 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1089
1099
  body: prepared.body,
1090
1100
  headers: { [DATA_HEADER]: "1" },
1091
1101
  })
1092
- if (!res.ok) throw new Error(`[nifra/web] action failed (${res.status}): ${action}`)
1102
+ // Whatever the status, the server took the POST: the action may have run before it failed.
1093
1103
  posted = true
1104
+ if (!res.ok) throw new Error(`[nifra/web] action failed (${res.status}): ${action}`)
1094
1105
  const redirectTo = res.headers.get(REDIRECT_HEADER)
1095
1106
  if (redirectTo !== null) {
1096
1107
  // The action redirected (Post/Redirect/Get) - treat it as a client navigation.
@@ -1114,9 +1125,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1114
1125
  routeId: loaded.terminalRouteId ?? target.routeId,
1115
1126
  params: loaded.terminalRouteId === undefined ? target.params : {},
1116
1127
  path: to,
1117
- data: loaded.data,
1118
- layoutData: loaded.layoutData,
1119
- ...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
1128
+ ...dataFields(loaded),
1120
1129
  actionData: undefined,
1121
1130
  pending: false,
1122
1131
  }
@@ -1137,9 +1146,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1137
1146
  const active = { routeId: state.routeId, params: state.params }
1138
1147
  const loaded = skipActive
1139
1148
  ? {
1140
- data: state.data,
1141
- layoutData: state.layoutData,
1142
- ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
1149
+ ...dataFields(state),
1143
1150
  }
1144
1151
  : await applyClientLoader(state.path, active, ac.signal, () =>
1145
1152
  loadRouteData(state.path, active, ac.signal),
@@ -1154,9 +1161,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1154
1161
  routeId: state.routeId,
1155
1162
  params: state.params,
1156
1163
  path: state.path,
1157
- data: loaded.data,
1158
- layoutData: loaded.layoutData,
1159
- ...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
1164
+ ...dataFields(loaded),
1160
1165
  actionData,
1161
1166
  pending: false,
1162
1167
  }
@@ -1172,9 +1177,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1172
1177
  routeId: state.routeId,
1173
1178
  params: state.params,
1174
1179
  path: state.path,
1175
- data: state.data,
1176
- layoutData: state.layoutData,
1177
- ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
1180
+ ...dataFields(state),
1178
1181
  actionData: previousActionData,
1179
1182
  pending: false,
1180
1183
  }
@@ -1182,7 +1185,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1182
1185
  }
1183
1186
  if (!posted) throw err
1184
1187
  if (mine !== generation) return // a newer navigation or submit owns the page
1185
- throw redirectOf(err) === undefined ? redirected(landing ?? state.path) : err
1188
+ throw redirectOf(err) === undefined ? redirected(landing ?? state.path, err) : err
1186
1189
  }
1187
1190
  },
1188
1191
  hydrate,
@@ -1216,7 +1219,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
1216
1219
  const oldest = prefetched.keys().next().value
1217
1220
  if (oldest !== undefined) prefetched.delete(oldest)
1218
1221
  }
1219
- prefetched.set(path, [loaded, Date.now()])
1222
+ if (loaded.serverComponents === undefined) prefetched.set(path, [loaded, Date.now()])
1220
1223
  } catch {
1221
1224
  // Best-effort: a failed prefetch just means the eventual navigate fetches normally.
1222
1225
  } finally {
@@ -0,0 +1,11 @@
1
+ declare const __NIFRA_COMPONENT_BUILD__: string
2
+
3
+ /** The coordinated compiler replaces this value in every environment. */
4
+ export const componentBuild: string =
5
+ typeof __NIFRA_COMPONENT_BUILD__ === "string"
6
+ ? __NIFRA_COMPONENT_BUILD__
7
+ : (() => {
8
+ throw new Error(
9
+ "[nifra/components] build-info requires serverComponentsConfig or nifra build",
10
+ )
11
+ })()