@nifrajs/web 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +35 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +89 -35
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +18 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +97 -17
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +72 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +411 -98
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +21 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +226 -15
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/css-contract.d.ts +21 -0
  40. package/dist/css-contract.d.ts.map +1 -0
  41. package/dist/css-contract.js +28 -0
  42. package/dist/css-contract.js.map +1 -0
  43. package/dist/dev-client.d.ts +76 -0
  44. package/dist/dev-client.d.ts.map +1 -0
  45. package/dist/dev-client.js +280 -0
  46. package/dist/dev-client.js.map +1 -0
  47. package/dist/dev-diagnostics.d.ts +26 -2
  48. package/dist/dev-diagnostics.d.ts.map +1 -1
  49. package/dist/dev-diagnostics.js +38 -7
  50. package/dist/dev-diagnostics.js.map +1 -1
  51. package/dist/dev-error.d.ts +4 -13
  52. package/dist/dev-error.d.ts.map +1 -1
  53. package/dist/dev-error.js +46 -4
  54. package/dist/dev-error.js.map +1 -1
  55. package/dist/dev-feed.d.ts +298 -0
  56. package/dist/dev-feed.d.ts.map +1 -0
  57. package/dist/dev-feed.js +797 -0
  58. package/dist/dev-feed.js.map +1 -0
  59. package/dist/dev-indicator.d.ts +38 -0
  60. package/dist/dev-indicator.d.ts.map +1 -0
  61. package/dist/dev-indicator.js +93 -0
  62. package/dist/dev-indicator.js.map +1 -0
  63. package/dist/dev-session.d.ts +77 -0
  64. package/dist/dev-session.d.ts.map +1 -0
  65. package/dist/dev-session.js +580 -0
  66. package/dist/dev-session.js.map +1 -0
  67. package/dist/dev-sourcemap.d.ts +27 -0
  68. package/dist/dev-sourcemap.d.ts.map +1 -0
  69. package/dist/dev-sourcemap.js +235 -0
  70. package/dist/dev-sourcemap.js.map +1 -0
  71. package/dist/dev-ssr-graph.d.ts.map +1 -1
  72. package/dist/dev-ssr-graph.js +13 -5
  73. package/dist/dev-ssr-graph.js.map +1 -1
  74. package/dist/dev.d.ts +15 -2
  75. package/dist/dev.d.ts.map +1 -1
  76. package/dist/dev.js +69 -44
  77. package/dist/dev.js.map +1 -1
  78. package/dist/diagnostic-catalog.d.ts +35 -0
  79. package/dist/diagnostic-catalog.d.ts.map +1 -0
  80. package/dist/diagnostic-catalog.js +203 -0
  81. package/dist/diagnostic-catalog.js.map +1 -0
  82. package/dist/diagnostic-prompt.d.ts +49 -0
  83. package/dist/diagnostic-prompt.d.ts.map +1 -0
  84. package/dist/diagnostic-prompt.js +303 -0
  85. package/dist/diagnostic-prompt.js.map +1 -0
  86. package/dist/diagnostic.d.ts +17 -23
  87. package/dist/diagnostic.d.ts.map +1 -1
  88. package/dist/diagnostic.js +31 -39
  89. package/dist/diagnostic.js.map +1 -1
  90. package/dist/draft.d.ts.map +1 -1
  91. package/dist/draft.js +4 -1
  92. package/dist/draft.js.map +1 -1
  93. package/dist/fn.d.ts +5 -0
  94. package/dist/fn.d.ts.map +1 -1
  95. package/dist/fn.js +13 -3
  96. package/dist/fn.js.map +1 -1
  97. package/dist/fs.d.ts.map +1 -1
  98. package/dist/fs.js +4 -2
  99. package/dist/fs.js.map +1 -1
  100. package/dist/index.d.ts +15 -15
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +13 -12
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  105. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  106. package/dist/internal/cdn/cloudflare.js +94 -0
  107. package/dist/internal/cdn/cloudflare.js.map +1 -0
  108. package/dist/internal/cdn/core.d.ts +91 -0
  109. package/dist/internal/cdn/core.d.ts.map +1 -0
  110. package/dist/internal/cdn/core.js +220 -0
  111. package/dist/internal/cdn/core.js.map +1 -0
  112. package/dist/internal/cdn/fastly.d.ts +17 -0
  113. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  114. package/dist/internal/cdn/fastly.js +43 -0
  115. package/dist/internal/cdn/fastly.js.map +1 -0
  116. package/dist/internal/cdn/vercel.d.ts +31 -0
  117. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  118. package/dist/internal/cdn/vercel.js +56 -0
  119. package/dist/internal/cdn/vercel.js.map +1 -0
  120. package/dist/internal/code-unit-order.d.ts +4 -0
  121. package/dist/internal/code-unit-order.d.ts.map +1 -0
  122. package/dist/internal/code-unit-order.js +6 -0
  123. package/dist/internal/code-unit-order.js.map +1 -0
  124. package/dist/internal/codegen.d.ts +3 -0
  125. package/dist/internal/codegen.d.ts.map +1 -1
  126. package/dist/internal/codegen.js +151 -30
  127. package/dist/internal/codegen.js.map +1 -1
  128. package/dist/internal/dev-csp.d.ts +24 -0
  129. package/dist/internal/dev-csp.d.ts.map +1 -0
  130. package/dist/internal/dev-csp.js +160 -0
  131. package/dist/internal/dev-csp.js.map +1 -0
  132. package/dist/internal/dev-reserved.d.ts +25 -0
  133. package/dist/internal/dev-reserved.d.ts.map +1 -0
  134. package/dist/internal/dev-reserved.js +39 -0
  135. package/dist/internal/dev-reserved.js.map +1 -0
  136. package/dist/internal/html-spans.d.ts +10 -0
  137. package/dist/internal/html-spans.d.ts.map +1 -0
  138. package/dist/internal/html-spans.js +46 -0
  139. package/dist/internal/html-spans.js.map +1 -0
  140. package/dist/internal/loading-runtime.d.ts +44 -0
  141. package/dist/internal/loading-runtime.d.ts.map +1 -0
  142. package/dist/internal/loading-runtime.js +206 -0
  143. package/dist/internal/loading-runtime.js.map +1 -0
  144. package/dist/internal/matches-runtime.d.ts +8 -0
  145. package/dist/internal/matches-runtime.d.ts.map +1 -0
  146. package/dist/internal/matches-runtime.js +61 -0
  147. package/dist/internal/matches-runtime.js.map +1 -0
  148. package/dist/internal/mount-shadow.d.ts +36 -0
  149. package/dist/internal/mount-shadow.d.ts.map +1 -0
  150. package/dist/internal/mount-shadow.js +65 -0
  151. package/dist/internal/mount-shadow.js.map +1 -0
  152. package/dist/internal/node-builtins.d.ts +3 -0
  153. package/dist/internal/node-builtins.d.ts.map +1 -0
  154. package/dist/internal/node-builtins.js +79 -0
  155. package/dist/internal/node-builtins.js.map +1 -0
  156. package/dist/internal/output-guard.d.ts +65 -0
  157. package/dist/internal/output-guard.d.ts.map +1 -0
  158. package/dist/internal/output-guard.js +630 -0
  159. package/dist/internal/output-guard.js.map +1 -0
  160. package/dist/internal/page-execution.d.ts +69 -0
  161. package/dist/internal/page-execution.d.ts.map +1 -0
  162. package/dist/internal/page-execution.js +1021 -0
  163. package/dist/internal/page-execution.js.map +1 -0
  164. package/dist/internal/parity.d.ts +21 -0
  165. package/dist/internal/parity.d.ts.map +1 -1
  166. package/dist/internal/parity.js +104 -22
  167. package/dist/internal/parity.js.map +1 -1
  168. package/dist/internal/private-env.d.ts +12 -0
  169. package/dist/internal/private-env.d.ts.map +1 -0
  170. package/dist/internal/private-env.js +335 -0
  171. package/dist/internal/private-env.js.map +1 -0
  172. package/dist/internal/public-url.d.ts +3 -0
  173. package/dist/internal/public-url.d.ts.map +1 -0
  174. package/dist/internal/public-url.js +18 -0
  175. package/dist/internal/public-url.js.map +1 -0
  176. package/dist/internal/render-document.d.ts +37 -21
  177. package/dist/internal/render-document.d.ts.map +1 -1
  178. package/dist/internal/render-document.js +104 -65
  179. package/dist/internal/render-document.js.map +1 -1
  180. package/dist/internal/request-url.d.ts +3 -0
  181. package/dist/internal/request-url.d.ts.map +1 -0
  182. package/dist/internal/request-url.js +14 -0
  183. package/dist/internal/request-url.js.map +1 -0
  184. package/dist/internal/response-controls.d.ts +75 -0
  185. package/dist/internal/response-controls.d.ts.map +1 -0
  186. package/dist/internal/response-controls.js +256 -0
  187. package/dist/internal/response-controls.js.map +1 -0
  188. package/dist/internal/runtime-contract.d.ts +11 -16
  189. package/dist/internal/runtime-contract.d.ts.map +1 -1
  190. package/dist/internal/runtime-contract.js.map +1 -1
  191. package/dist/internal/secret-scan.d.ts +104 -0
  192. package/dist/internal/secret-scan.d.ts.map +1 -0
  193. package/dist/internal/secret-scan.js +485 -0
  194. package/dist/internal/secret-scan.js.map +1 -0
  195. package/dist/internal/server-boundary.d.ts +6 -29
  196. package/dist/internal/server-boundary.d.ts.map +1 -1
  197. package/dist/internal/server-boundary.js +6 -66
  198. package/dist/internal/server-boundary.js.map +1 -1
  199. package/dist/internal/source-map.d.ts +47 -0
  200. package/dist/internal/source-map.d.ts.map +1 -0
  201. package/dist/internal/source-map.js +225 -0
  202. package/dist/internal/source-map.js.map +1 -0
  203. package/dist/internal/target-compat.d.ts +27 -0
  204. package/dist/internal/target-compat.d.ts.map +1 -0
  205. package/dist/internal/target-compat.js +77 -0
  206. package/dist/internal/target-compat.js.map +1 -0
  207. package/dist/internal/timing-safe-equal.d.ts +6 -0
  208. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  209. package/dist/internal/timing-safe-equal.js +10 -0
  210. package/dist/internal/timing-safe-equal.js.map +1 -1
  211. package/dist/internal/vitals-runtime.d.ts +18 -0
  212. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  213. package/dist/internal/vitals-runtime.js +38 -0
  214. package/dist/internal/vitals-runtime.js.map +1 -0
  215. package/dist/internal/web-app.d.ts +56 -8
  216. package/dist/internal/web-app.d.ts.map +1 -1
  217. package/dist/internal/web-app.js +215 -772
  218. package/dist/internal/web-app.js.map +1 -1
  219. package/dist/internal/zone-graph.d.ts +89 -0
  220. package/dist/internal/zone-graph.d.ts.map +1 -0
  221. package/dist/internal/zone-graph.js +492 -0
  222. package/dist/internal/zone-graph.js.map +1 -0
  223. package/dist/isr.d.ts +83 -6
  224. package/dist/isr.d.ts.map +1 -1
  225. package/dist/isr.js +301 -39
  226. package/dist/isr.js.map +1 -1
  227. package/dist/llms-txt.d.ts +3 -0
  228. package/dist/llms-txt.d.ts.map +1 -1
  229. package/dist/llms-txt.js +37 -9
  230. package/dist/llms-txt.js.map +1 -1
  231. package/dist/manifest.d.ts +221 -14
  232. package/dist/manifest.d.ts.map +1 -1
  233. package/dist/manifest.js +386 -9
  234. package/dist/manifest.js.map +1 -1
  235. package/dist/module-graph.d.ts +26 -5
  236. package/dist/module-graph.d.ts.map +1 -1
  237. package/dist/module-graph.js +17 -7
  238. package/dist/module-graph.js.map +1 -1
  239. package/dist/navigation.d.ts +17 -3
  240. package/dist/navigation.d.ts.map +1 -1
  241. package/dist/navigation.js +11 -5
  242. package/dist/navigation.js.map +1 -1
  243. package/dist/nonce.d.ts +47 -0
  244. package/dist/nonce.d.ts.map +1 -0
  245. package/dist/nonce.js +63 -0
  246. package/dist/nonce.js.map +1 -0
  247. package/dist/plugins/css-modules.d.ts.map +1 -1
  248. package/dist/plugins/css-modules.js +4 -3
  249. package/dist/plugins/css-modules.js.map +1 -1
  250. package/dist/plugins/kit.d.ts +9 -0
  251. package/dist/plugins/kit.d.ts.map +1 -1
  252. package/dist/plugins/kit.js +18 -0
  253. package/dist/plugins/kit.js.map +1 -1
  254. package/dist/plugins/svg.d.ts +22 -3
  255. package/dist/plugins/svg.d.ts.map +1 -1
  256. package/dist/plugins/svg.js +112 -1
  257. package/dist/plugins/svg.js.map +1 -1
  258. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  259. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  260. package/dist/plugins/vite-leak-guard.js +378 -17
  261. package/dist/plugins/vite-leak-guard.js.map +1 -1
  262. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  263. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  264. package/dist/plugins/vite-zone-guard.js +208 -0
  265. package/dist/plugins/vite-zone-guard.js.map +1 -0
  266. package/dist/plugins/zone-guard.d.ts +27 -0
  267. package/dist/plugins/zone-guard.d.ts.map +1 -0
  268. package/dist/plugins/zone-guard.js +72 -0
  269. package/dist/plugins/zone-guard.js.map +1 -0
  270. package/dist/prerender.d.ts +10 -0
  271. package/dist/prerender.d.ts.map +1 -1
  272. package/dist/prerender.js +35 -3
  273. package/dist/prerender.js.map +1 -1
  274. package/dist/public-dir.d.ts +9 -0
  275. package/dist/public-dir.d.ts.map +1 -1
  276. package/dist/public-dir.js +131 -48
  277. package/dist/public-dir.js.map +1 -1
  278. package/dist/pwa-manifest.d.ts +68 -0
  279. package/dist/pwa-manifest.d.ts.map +1 -0
  280. package/dist/pwa-manifest.js +178 -0
  281. package/dist/pwa-manifest.js.map +1 -0
  282. package/dist/query.js +4 -4
  283. package/dist/query.js.map +1 -1
  284. package/dist/render-seam.d.ts +42 -2
  285. package/dist/render-seam.d.ts.map +1 -1
  286. package/dist/render-seam.js +6 -0
  287. package/dist/render-seam.js.map +1 -1
  288. package/dist/route-manifest.d.ts +3 -0
  289. package/dist/route-manifest.d.ts.map +1 -1
  290. package/dist/route-manifest.js +26 -3
  291. package/dist/route-manifest.js.map +1 -1
  292. package/dist/route-types.d.ts +24 -0
  293. package/dist/route-types.d.ts.map +1 -0
  294. package/dist/route-types.js +195 -0
  295. package/dist/route-types.js.map +1 -0
  296. package/dist/router.d.ts +28 -6
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +129 -40
  299. package/dist/router.js.map +1 -1
  300. package/dist/trusted-html.d.ts +34 -0
  301. package/dist/trusted-html.d.ts.map +1 -0
  302. package/dist/trusted-html.js +35 -0
  303. package/dist/trusted-html.js.map +1 -0
  304. package/dist/vitals.d.ts +33 -0
  305. package/dist/vitals.d.ts.map +1 -0
  306. package/dist/vitals.js +16 -0
  307. package/dist/vitals.js.map +1 -0
  308. package/dist/vite.d.ts +16 -1
  309. package/dist/vite.d.ts.map +1 -1
  310. package/dist/vite.js +187 -36
  311. package/dist/vite.js.map +1 -1
  312. package/dist/zones.d.ts +70 -0
  313. package/dist/zones.d.ts.map +1 -0
  314. package/dist/zones.js +365 -0
  315. package/dist/zones.js.map +1 -0
  316. package/package.json +67 -14
  317. package/src/backend-only.ts +16 -0
  318. package/src/boundary.ts +18 -5
  319. package/src/build-plan.ts +113 -36
  320. package/src/build-vite.ts +124 -15
  321. package/src/build.ts +535 -102
  322. package/src/bun-dev-entry.ts +1 -1
  323. package/src/cdn.ts +216 -0
  324. package/src/client.ts +251 -18
  325. package/src/conformance.ts +16 -3
  326. package/src/csp.ts +194 -0
  327. package/src/css-contract.ts +44 -0
  328. package/src/dev-client.ts +349 -0
  329. package/src/dev-diagnostics.ts +60 -7
  330. package/src/dev-error.ts +49 -5
  331. package/src/dev-feed.ts +1140 -0
  332. package/src/dev-indicator.ts +113 -0
  333. package/src/dev-session.ts +729 -0
  334. package/src/dev-sourcemap.ts +281 -0
  335. package/src/dev-ssr-graph.ts +16 -5
  336. package/src/dev.ts +86 -43
  337. package/src/diagnostic-catalog.ts +243 -0
  338. package/src/diagnostic-prompt.ts +355 -0
  339. package/src/diagnostic.ts +63 -66
  340. package/src/draft.ts +4 -1
  341. package/src/fn.ts +19 -3
  342. package/src/fs.ts +13 -2
  343. package/src/index.ts +57 -10
  344. package/src/internal/cdn/cloudflare.ts +146 -0
  345. package/src/internal/cdn/core.ts +316 -0
  346. package/src/internal/cdn/fastly.ts +62 -0
  347. package/src/internal/cdn/vercel.ts +95 -0
  348. package/src/internal/code-unit-order.ts +5 -0
  349. package/src/internal/codegen.ts +180 -29
  350. package/src/internal/dev-csp.ts +176 -0
  351. package/src/internal/dev-reserved.ts +52 -0
  352. package/src/internal/html-spans.ts +50 -0
  353. package/src/internal/loading-runtime.ts +244 -0
  354. package/src/internal/matches-runtime.ts +62 -0
  355. package/src/internal/mount-shadow.ts +88 -0
  356. package/src/internal/node-builtins.ts +79 -0
  357. package/src/internal/output-guard.ts +701 -0
  358. package/src/internal/page-execution.ts +1371 -0
  359. package/src/internal/parity.ts +150 -36
  360. package/src/internal/private-env.ts +334 -0
  361. package/src/internal/public-url.ts +22 -0
  362. package/src/internal/render-document.ts +153 -73
  363. package/src/internal/request-url.ts +14 -0
  364. package/src/internal/response-controls.ts +284 -0
  365. package/src/internal/runtime-contract.ts +10 -15
  366. package/src/internal/secret-scan.ts +623 -0
  367. package/src/internal/server-boundary.ts +6 -69
  368. package/src/internal/source-map.ts +256 -0
  369. package/src/internal/target-compat.ts +92 -0
  370. package/src/internal/timing-safe-equal.ts +13 -0
  371. package/src/internal/vitals-runtime.ts +53 -0
  372. package/src/internal/web-app.ts +290 -879
  373. package/src/internal/zone-graph.ts +593 -0
  374. package/src/isr.ts +366 -39
  375. package/src/llms-txt.ts +44 -8
  376. package/src/manifest.ts +671 -17
  377. package/src/module-graph.ts +53 -9
  378. package/src/navigation.ts +29 -6
  379. package/src/nonce.ts +104 -0
  380. package/src/plugins/css-modules.ts +5 -3
  381. package/src/plugins/kit.ts +24 -0
  382. package/src/plugins/svg.ts +135 -4
  383. package/src/plugins/vite-leak-guard.ts +529 -16
  384. package/src/plugins/vite-zone-guard.ts +263 -0
  385. package/src/plugins/zone-guard.ts +110 -0
  386. package/src/prerender.ts +49 -3
  387. package/src/public-dir.ts +135 -49
  388. package/src/pwa-manifest.ts +226 -0
  389. package/src/query.ts +4 -4
  390. package/src/render-seam.ts +46 -1
  391. package/src/route-manifest.ts +15 -3
  392. package/src/route-types.ts +264 -0
  393. package/src/router.ts +163 -63
  394. package/src/trusted-html.ts +44 -0
  395. package/src/vitals.ts +44 -0
  396. package/src/vite.ts +207 -36
  397. package/src/zones.ts +430 -0
  398. package/dist/plugins/vite-server-only.d.ts +0 -49
  399. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  400. package/dist/plugins/vite-server-only.js +0 -57
  401. package/dist/plugins/vite-server-only.js.map +0 -1
  402. package/dist/server-only.d.ts +0 -21
  403. package/dist/server-only.d.ts.map +0 -1
  404. package/dist/server-only.js +0 -25
  405. package/dist/server-only.js.map +0 -1
  406. package/src/plugins/vite-server-only.ts +0 -75
  407. package/src/server-only.ts +0 -25
package/src/public-dir.ts CHANGED
@@ -12,8 +12,10 @@
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 { realpath } from "node:fs/promises"
16
- import { normalize, resolve, sep } from "node:path"
15
+ import { constants as FS } from "node:fs"
16
+ import { open, realpath } from "node:fs/promises"
17
+ import { extname, normalize, resolve, sep } from "node:path"
18
+ import { Readable } from "node:stream"
17
19
  import { parseByteRange } from "@nifrajs/core/range"
18
20
  import { pathnameOf } from "@nifrajs/core/server"
19
21
 
@@ -36,8 +38,67 @@ export interface ServePublicDirOptions {
36
38
  readonly files?: ReadonlySet<string>
37
39
  }
38
40
 
39
- const IMMUTABLE = "public, max-age=31536000, immutable"
40
- const ONE_DAY = "public, max-age=86400"
41
+ export const IMMUTABLE = "public, max-age=31536000, immutable"
42
+ export const ONE_DAY = "public, max-age=86400"
43
+
44
+ /**
45
+ * Media types by extension. A table rather than `Bun.file(path).type`: this handler also runs on the
46
+ * Node and Deno adapters, where `Bun` does not exist and every hit would throw. Unknown extensions
47
+ * (and extensionless files such as ACME tokens) are served as `application/octet-stream`, which
48
+ * together with `nosniff` keeps the browser from guessing a more dangerous type.
49
+ */
50
+ export const CONTENT_TYPES: Readonly<Record<string, string>> = {
51
+ ".html": "text/html; charset=utf-8",
52
+ ".htm": "text/html; charset=utf-8",
53
+ ".css": "text/css; charset=utf-8",
54
+ ".js": "text/javascript; charset=utf-8",
55
+ ".mjs": "text/javascript; charset=utf-8",
56
+ ".cjs": "text/javascript; charset=utf-8",
57
+ ".json": "application/json; charset=utf-8",
58
+ ".map": "application/json; charset=utf-8",
59
+ ".webmanifest": "application/manifest+json",
60
+ ".txt": "text/plain; charset=utf-8",
61
+ ".md": "text/markdown; charset=utf-8",
62
+ ".csv": "text/csv; charset=utf-8",
63
+ ".xml": "application/xml",
64
+ ".rss": "application/rss+xml",
65
+ ".atom": "application/atom+xml",
66
+ ".svg": "image/svg+xml",
67
+ ".png": "image/png",
68
+ ".jpg": "image/jpeg",
69
+ ".jpeg": "image/jpeg",
70
+ ".gif": "image/gif",
71
+ ".webp": "image/webp",
72
+ ".avif": "image/avif",
73
+ ".ico": "image/x-icon",
74
+ ".bmp": "image/bmp",
75
+ ".woff": "font/woff",
76
+ ".woff2": "font/woff2",
77
+ ".ttf": "font/ttf",
78
+ ".otf": "font/otf",
79
+ ".wasm": "application/wasm",
80
+ ".pdf": "application/pdf",
81
+ ".zip": "application/zip",
82
+ ".mp4": "video/mp4",
83
+ ".webm": "video/webm",
84
+ ".ogg": "audio/ogg",
85
+ ".mp3": "audio/mpeg",
86
+ ".wav": "audio/wav",
87
+ ".m4a": "audio/mp4",
88
+ ".vtt": "text/vtt; charset=utf-8",
89
+ }
90
+
91
+ /**
92
+ * Whether a confined path crosses a dot-segment other than `.well-known`. The build copies `public/`
93
+ * with dotfiles included, so an editor or VCS artifact (`.env`, `.git/`, `.DS_Store`) dropped there
94
+ * would otherwise be downloadable. Checked on the resolved path, so `%2e` encodings are covered.
95
+ */
96
+ function isHiddenPath(root: string, abs: string): boolean {
97
+ for (const segment of abs.slice(root.length + 1).split(sep)) {
98
+ if (segment.startsWith(".") && segment !== ".well-known") return true
99
+ }
100
+ return false
101
+ }
41
102
 
42
103
  /** HTTP dates carry second precision; comparing raw milliseconds makes every file look stale. */
43
104
  function seconds(time: number): number {
@@ -117,7 +178,7 @@ export function servePublicDir(
117
178
  // A production manifest can reject page routes without touching the filesystem.
118
179
  if (options.files !== undefined && !options.files.has(pathname)) return undefined
119
180
  const abs = resolvePublicPath(root, pathname)
120
- if (abs === undefined) return undefined
181
+ if (abs === undefined || isHiddenPath(root, abs)) return undefined
121
182
  const [resolvedRoot, resolvedFile] = await Promise.all([
122
183
  rootReal,
123
184
  realpath(abs).catch(() => undefined),
@@ -129,54 +190,79 @@ export function servePublicDir(
129
190
  ) {
130
191
  return undefined
131
192
  }
132
- const file = Bun.file(resolvedFile)
133
- if (!(await file.exists())) return undefined
134
- const size = file.size
135
- const headers = new Headers({
136
- "cache-control": pathname.startsWith(hashedPrefix) ? hashed : assets,
137
- // Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
138
- // so a video or audio file under `public/` is scrubbable only once this header is present.
139
- "accept-ranges": "bytes",
140
- })
141
- // `new Response(file)` used to infer the media type, which meant HEAD - built from a null body -
142
- // silently lost it. Setting it here is what makes the two methods agree.
143
- if (file.type !== "") headers.set("content-type", file.type)
144
- const lastModified =
145
- Number.isFinite(file.lastModified) && file.lastModified > 0 ? file.lastModified : undefined
146
- if (lastModified !== undefined) {
147
- headers.set("last-modified", new Date(lastModified).toUTCString())
148
- }
149
- const head = request.method === "HEAD"
193
+ // Open the canonical path once and stream from that descriptor. Reopening the pathname after
194
+ // 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
198
+ try {
199
+ handle = await open(resolvedFile, FS.O_RDONLY | FS.O_NOFOLLOW)
200
+ const stat = await handle.stat()
201
+ if (!stat.isFile()) return undefined
202
+ const size = stat.size
203
+ const headers = new Headers({
204
+ "cache-control": pathname.startsWith(hashedPrefix) ? hashed : assets,
205
+ // Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
206
+ // so a video or audio file under `public/` is scrubbable only once this header is present.
207
+ "accept-ranges": "bytes",
208
+ // Never let a client sniff a served file into a more dangerous type (an upload-like `.txt`
209
+ // rendered as HTML, say). The declared type below is authoritative.
210
+ "x-content-type-options": "nosniff",
211
+ // Set explicitly rather than inferred from the body, so HEAD (a null body) carries it too.
212
+ "content-type":
213
+ CONTENT_TYPES[extname(resolvedFile).toLowerCase()] ?? "application/octet-stream",
214
+ })
215
+ const lastModified =
216
+ Number.isFinite(stat.mtimeMs) && stat.mtimeMs > 0 ? stat.mtimeMs : undefined
217
+ if (lastModified !== undefined) {
218
+ headers.set("last-modified", new Date(lastModified).toUTCString())
219
+ }
220
+ const head = request.method === "HEAD"
150
221
 
151
- if (isNotModified(request, lastModified)) {
152
- headers.delete("content-type")
153
- return new Response(null, { status: 304, headers })
154
- }
222
+ if (isNotModified(request, lastModified)) {
223
+ headers.delete("content-type")
224
+ return new Response(null, { status: 304, headers })
225
+ }
155
226
 
156
- const rangeHeader = request.headers.get("range")
157
- const range =
158
- rangeHeader !== null && ifRangeMatches(request.headers.get("if-range"), lastModified)
159
- ? parseByteRange(rangeHeader, size)
160
- : ({ kind: "none" } as const)
227
+ const rangeHeader = request.headers.get("range")
228
+ const range =
229
+ rangeHeader !== null && ifRangeMatches(request.headers.get("if-range"), lastModified)
230
+ ? parseByteRange(rangeHeader, size)
231
+ : ({ kind: "none" } as const)
161
232
 
162
- if (range.kind === "unsatisfiable") {
163
- headers.delete("content-type")
164
- headers.set("content-range", `bytes */${size}`)
165
- return new Response(null, { status: 416, headers })
166
- }
233
+ if (range.kind === "unsatisfiable") {
234
+ headers.delete("content-type")
235
+ headers.set("content-range", `bytes */${size}`)
236
+ return new Response(null, { status: 416, headers })
237
+ }
167
238
 
168
- if (range.kind === "satisfiable" && range.ranges.length === 1) {
169
- const { start, end } = range.ranges[0]!
170
- headers.set("content-range", `bytes ${start}-${end}/${size}`)
171
- headers.set("content-length", String(end - start + 1))
172
- // `slice` keeps the read lazy: only the selected window is ever pulled off disk.
173
- return new Response(head ? null : file.slice(start, end + 1), { status: 206, headers })
174
- }
239
+ const body = (start?: number, end?: number): ReadableStream<Uint8Array> => {
240
+ const stream = handle!.createReadStream({ start, end })
241
+ // FileHandle.createReadStream owns closing the descriptor after EOF/abort.
242
+ handle = undefined
243
+ return Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>
244
+ }
245
+
246
+ if (range.kind === "satisfiable" && range.ranges.length === 1) {
247
+ const { start, end } = range.ranges[0]!
248
+ headers.set("content-range", `bytes ${start}-${end}/${size}`)
249
+ headers.set("content-length", String(end - start + 1))
250
+ return new Response(head ? null : body(start, end), { status: 206, headers })
251
+ }
175
252
 
176
- // Multiple ranges would require assembling `multipart/byteranges`, and doing that for a file on
177
- // disk means buffering the whole representation to serve a request that asked for less of it.
178
- // RFC 9110 lets a server ignore Range entirely, so the full body is the conformant answer here.
179
- if (head) headers.set("content-length", String(size))
180
- return new Response(head ? null : file, { headers })
253
+ // Multiple ranges would require assembling `multipart/byteranges`, and doing that for a file on
254
+ // disk means buffering the whole representation to serve a request that asked for less of it.
255
+ // RFC 9110 lets a server ignore Range entirely, so the full body is the conformant answer here.
256
+ headers.set("content-length", String(size))
257
+ return new Response(head ? null : body(), { headers })
258
+ } finally {
259
+ if (handle !== undefined) {
260
+ try {
261
+ await handle.close()
262
+ } catch {
263
+ // Closing a descriptor is best-effort after the response decision is made.
264
+ }
265
+ }
266
+ }
181
267
  }
182
268
  }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * `@nifrajs/web/pwa-manifest` - a Web App Manifest builder, the declarative half of PWA next to
3
+ * `@nifrajs/web/service-worker`'s generated worker. Pure + runtime-agnostic: describe the app and
4
+ * get the `manifest.json` bytes to serve (typically `app.get("/manifest.webmanifest", ...)` with
5
+ * `content-type: application/manifest+json`), plus the `<link rel="manifest">` tag for the
6
+ * document head. Follows the [Web App Manifest spec](https://www.w3.org/TR/appmanifest/) field
7
+ * names exactly, so MDN is the reference for anything not documented here.
8
+ *
9
+ * Invalid input throws loudly at build/startup time (a missing install name, a malformed icon
10
+ * size, an unknown display mode) - a manifest the browser silently ignores is worse than no
11
+ * manifest, and these are all developer mistakes, never request input.
12
+ */
13
+
14
+ export type ManifestDisplay = "fullscreen" | "standalone" | "minimal-ui" | "browser"
15
+ export type ManifestDir = "ltr" | "rtl" | "auto"
16
+ export type ManifestPurpose = "any" | "maskable" | "monochrome"
17
+
18
+ export interface ManifestIcon {
19
+ /** Icon URL, resolved relative to `start_url` by the browser when relative. */
20
+ readonly src: string
21
+ /** Space-separated sizes, e.g. `"192x192 512x512"`, or `"any"`. Default `"any"`. */
22
+ readonly sizes?: string
23
+ /** MIME type, e.g. `"image/png"`. */
24
+ readonly type?: string
25
+ /** Space-separated purposes. Default `"any"`. */
26
+ readonly purpose?: string
27
+ }
28
+
29
+ export interface ManifestScreenshot {
30
+ readonly src: string
31
+ readonly sizes?: string
32
+ readonly type?: string
33
+ }
34
+
35
+ export interface ManifestShortcut {
36
+ readonly name: string
37
+ readonly url: string
38
+ readonly short_name?: string
39
+ readonly description?: string
40
+ readonly icons?: readonly ManifestIcon[]
41
+ }
42
+
43
+ export interface PwaManifestOptions {
44
+ /** Full install name (`"Nifra News"`). One of `name` / `short_name` is required. */
45
+ readonly name?: string
46
+ /** Short install name for launchers (`"News"`). One of `name` / `short_name` is required. */
47
+ readonly short_name?: string
48
+ readonly description?: string
49
+ /** Navigation scope root. Default `"/"`. */
50
+ readonly start_url?: string
51
+ /** URL scope the manifest (and its service worker) controls. Default `start_url`'s directory. */
52
+ readonly scope?: string
53
+ /** Preferred display mode. Default `"standalone"` (the installable PWA shape). */
54
+ readonly display?: ManifestDisplay
55
+ readonly orientation?: string
56
+ readonly background_color?: string
57
+ readonly theme_color?: string
58
+ readonly lang?: string
59
+ readonly dir?: ManifestDir
60
+ /** Opaque id distinguishing this install (defaults to `start_url` per spec). */
61
+ readonly id?: string
62
+ readonly categories?: readonly string[]
63
+ readonly icons?: readonly ManifestIcon[]
64
+ readonly screenshots?: readonly ManifestScreenshot[]
65
+ readonly shortcuts?: readonly ManifestShortcut[]
66
+ }
67
+
68
+ const SIZE_LIST = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/
69
+ const PURPOSE_TOKENS: ReadonlySet<string> = new Set(["any", "maskable", "monochrome"])
70
+
71
+ /** The start URL's directory, origin-preserving for absolute URLs. Query strings and hashes
72
+ * never belong to a scope - it is a URL prefix the browser matches against. */
73
+ function defaultScope(start_url: string): string {
74
+ if (start_url.startsWith("/")) {
75
+ const bare = start_url.split(/[?#]/)[0] as string
76
+ return bare.endsWith("/") ? bare : bare.slice(0, bare.lastIndexOf("/") + 1)
77
+ }
78
+ let url: URL
79
+ try {
80
+ url = new URL(start_url)
81
+ } catch {
82
+ throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
83
+ }
84
+ if (url.origin === "null" || (url.protocol !== "http:" && url.protocol !== "https:")) {
85
+ throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
86
+ }
87
+ const dir = url.pathname.endsWith("/")
88
+ ? url.pathname
89
+ : url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1)
90
+ return `${url.origin}${dir}`
91
+ }
92
+
93
+ function purposeOf(purpose: string | undefined, what: string): string | undefined {
94
+ if (purpose === undefined) return undefined
95
+ const tokens = purpose.split(/\s+/).filter((t) => t !== "")
96
+ if (tokens.length === 0) throw new Error(`pwaManifest: ${what} purpose must not be empty`)
97
+ for (const token of tokens) {
98
+ if (!PURPOSE_TOKENS.has(token)) {
99
+ throw new Error(
100
+ `pwaManifest: ${what} purpose ${JSON.stringify(token)} must be any/maskable/monochrome`,
101
+ )
102
+ }
103
+ }
104
+ return tokens.join(" ")
105
+ }
106
+
107
+ /** Build the manifest document object (pass through `JSON.stringify` to serve it). */
108
+ export function pwaManifest(options: PwaManifestOptions): Record<string, unknown> {
109
+ const { name, short_name } = options
110
+ if (name === undefined && short_name === undefined) {
111
+ throw new Error("pwaManifest: one of name / short_name is required")
112
+ }
113
+ if (name !== undefined && name.trim() === "") {
114
+ throw new Error("pwaManifest: name must not be empty")
115
+ }
116
+ if (short_name !== undefined && short_name.trim() === "") {
117
+ throw new Error("pwaManifest: short_name must not be empty")
118
+ }
119
+ const display = options.display ?? "standalone"
120
+ if (
121
+ display !== "fullscreen" &&
122
+ display !== "minimal-ui" &&
123
+ display !== "browser" &&
124
+ display !== "standalone"
125
+ ) {
126
+ throw new Error(`pwaManifest: unknown display ${JSON.stringify(display)}`)
127
+ }
128
+ if (
129
+ options.dir !== undefined &&
130
+ options.dir !== "ltr" &&
131
+ options.dir !== "rtl" &&
132
+ options.dir !== "auto"
133
+ ) {
134
+ throw new Error(`pwaManifest: unknown dir ${JSON.stringify(options.dir)}`)
135
+ }
136
+ const start_url = options.start_url ?? "/"
137
+ if (start_url === "" || start_url.startsWith("//")) {
138
+ throw new Error("pwaManifest: start_url must be a relative path or absolute URL")
139
+ }
140
+ const manifest: Record<string, unknown> = {}
141
+ if (name !== undefined) manifest.name = name
142
+ if (short_name !== undefined) manifest.short_name = short_name
143
+ if (options.description !== undefined) manifest.description = options.description
144
+ manifest.start_url = start_url
145
+ // The default scope is the start URL's directory ("https://x.com/app/home" → "…/app/"),
146
+ // query/hash excluded; a same-origin absolute URL keeps its origin, a relative one stays
147
+ // relative. Anything unparseable is a developer mistake, and fails loud.
148
+ manifest.scope = options.scope ?? defaultScope(start_url)
149
+ manifest.display = display
150
+ if (options.orientation !== undefined) manifest.orientation = options.orientation
151
+ if (options.background_color !== undefined) manifest.background_color = options.background_color
152
+ if (options.theme_color !== undefined) manifest.theme_color = options.theme_color
153
+ if (options.lang !== undefined) manifest.lang = options.lang
154
+ if (options.dir !== undefined) manifest.dir = options.dir
155
+ if (options.id !== undefined) manifest.id = options.id
156
+ if (options.categories !== undefined) manifest.categories = [...options.categories]
157
+ if (options.icons !== undefined) {
158
+ manifest.icons = options.icons.map((icon, i) => {
159
+ if (icon.src === "") throw new Error(`pwaManifest: icons[${i}].src must not be empty`)
160
+ const sizes = icon.sizes ?? "any"
161
+ if (!SIZE_LIST.test(sizes)) {
162
+ throw new Error(
163
+ `pwaManifest: icons[${i}].sizes ${JSON.stringify(icon.sizes)} must be "any" or space-separated WxH`,
164
+ )
165
+ }
166
+ const entry: Record<string, unknown> = { src: icon.src, sizes }
167
+ if (icon.type !== undefined) entry.type = icon.type
168
+ const purpose = purposeOf(icon.purpose, `icons[${i}]`)
169
+ if (purpose !== undefined) entry.purpose = purpose
170
+ return entry
171
+ })
172
+ }
173
+ if (options.screenshots !== undefined) {
174
+ manifest.screenshots = options.screenshots.map((shot, i) => {
175
+ if (shot.src === "") throw new Error(`pwaManifest: screenshots[${i}].src must not be empty`)
176
+ const entry: Record<string, unknown> = { src: shot.src }
177
+ if (shot.sizes !== undefined) {
178
+ if (!SIZE_LIST.test(shot.sizes)) {
179
+ throw new Error(
180
+ `pwaManifest: screenshots[${i}].sizes ${JSON.stringify(shot.sizes)} must be "any" or space-separated WxH`,
181
+ )
182
+ }
183
+ entry.sizes = shot.sizes
184
+ }
185
+ if (shot.type !== undefined) entry.type = shot.type
186
+ return entry
187
+ })
188
+ }
189
+ if (options.shortcuts !== undefined) {
190
+ manifest.shortcuts = options.shortcuts.map((cut, i) => {
191
+ if (cut.name === "") throw new Error(`pwaManifest: shortcuts[${i}].name must not be empty`)
192
+ if (cut.url === "") throw new Error(`pwaManifest: shortcuts[${i}].url must not be empty`)
193
+ const entry: Record<string, unknown> = { name: cut.name, url: cut.url }
194
+ if (cut.short_name !== undefined) entry.short_name = cut.short_name
195
+ if (cut.description !== undefined) entry.description = cut.description
196
+ if (cut.icons !== undefined) {
197
+ entry.icons = cut.icons.map((icon, j) => {
198
+ if (icon.src === "") {
199
+ throw new Error(`pwaManifest: shortcuts[${i}].icons[${j}].src must not be empty`)
200
+ }
201
+ const sizes = icon.sizes ?? "any"
202
+ if (!SIZE_LIST.test(sizes)) {
203
+ throw new Error(
204
+ `pwaManifest: shortcuts[${i}].icons[${j}].sizes must be "any" or space-separated WxH`,
205
+ )
206
+ }
207
+ return { src: icon.src, sizes }
208
+ })
209
+ }
210
+ return entry
211
+ })
212
+ }
213
+ return manifest
214
+ }
215
+
216
+ /** Serialize a manifest document to servable JSON (stable key order, no whitespace). */
217
+ export function serializeManifest(manifest: Record<string, unknown>): string {
218
+ return JSON.stringify(manifest)
219
+ }
220
+
221
+ /** The `<link rel="manifest">` tag for the document head. */
222
+ export function manifestLink(href = "/manifest.webmanifest"): string {
223
+ if (href === "") throw new Error("manifestLink: href must not be empty")
224
+ const quoted = href.replace(/"/g, "&quot;")
225
+ return `<link rel="manifest" href="${quoted}">`
226
+ }
package/src/query.ts CHANGED
@@ -642,7 +642,7 @@ export function createMutation<TData, TVariables>(
642
642
  let mutationFn = fn
643
643
  let cbs = callbacks
644
644
  let state = IDLE_MUTATION as MutationState<TData, TVariables>
645
- let token = 0
645
+ let latest = 0
646
646
  const listeners = new Set<() => void>()
647
647
  const emit = (): void => {
648
648
  for (const l of listeners) l()
@@ -665,19 +665,19 @@ export function createMutation<TData, TVariables>(
665
665
  },
666
666
  reset: () => setState(IDLE_MUTATION as MutationState<TData, TVariables>),
667
667
  mutate: async (variables) => {
668
- const mine = ++token
668
+ const mine = ++latest
669
669
  setState({ status: "pending", data: undefined, error: undefined, variables })
670
670
  // Only await a callback when one is actually supplied - an `await undefined` would insert a
671
671
  // needless microtask before the mutationFn runs (and complicates single-flight timing).
672
672
  if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
673
673
  try {
674
674
  const data = await mutationFn(variables)
675
- if (mine === token) setState({ status: "success", data, error: undefined, variables })
675
+ if (mine === latest) setState({ status: "success", data, error: undefined, variables })
676
676
  if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
677
677
  if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
678
678
  return data
679
679
  } catch (error) {
680
- if (mine === token) setState({ status: "error", data: undefined, error, variables })
680
+ if (mine === latest) setState({ status: "error", data: undefined, error, variables })
681
681
  if (cbs.onError !== undefined) await cbs.onError(error, variables)
682
682
  if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
683
683
  throw error
@@ -30,6 +30,41 @@ export interface RenderProps {
30
30
  readonly search?: Record<string, unknown>
31
31
  /** Neutral named-boundary states; adapters choose how each boundary's `render` UI is mounted. */
32
32
  readonly boundaries?: BoundaryStates
33
+ /** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
34
+ readonly matchChain?: MatchChain
35
+ }
36
+
37
+ /**
38
+ * The modules a render stacks, as `useMatches` needs them: each one's id and `handle` export,
39
+ * outermost layout first and the page (or status page) last. Built by the server for SSR and by the
40
+ * generated client entry per route, from the same manifest, so both sides report the same matches.
41
+ */
42
+ export interface MatchChain {
43
+ /** Layout ids (`orgs/[org]/_layout`), then the page's route id. */
44
+ readonly ids: readonly string[]
45
+ /** Each module's `handle` export, index-aligned with {@link ids}. */
46
+ readonly handles: readonly unknown[]
47
+ }
48
+
49
+ /** One module of the rendered chain, as `useMatches` returns it. */
50
+ export interface UIMatch {
51
+ /** The module's id: a layout's (`orgs/[org]/_layout`), or the page's route id last. */
52
+ readonly id: string
53
+ /** The part of the URL path this module wraps: a layout's directory prefix, the whole path for the
54
+ * page. Raw, as it appears in the URL. */
55
+ readonly pathname: string
56
+ /** The params this module's loader receives: a layout's own, every route param for the page. */
57
+ readonly params: Readonly<Record<string, string>>
58
+ /** This module's loader data: `null` for a layout without a loader. */
59
+ readonly data: unknown
60
+ /** The module's `handle` export - any value, commonly a breadcrumb label or a render function. */
61
+ readonly handle: unknown
62
+ }
63
+
64
+ /** Per-document options for {@link RenderAdapter.renderToStream}. */
65
+ export interface RenderStreamOptions {
66
+ /** The document's CSP nonce, for every executable `<script>` the framework streams. */
67
+ readonly nonce?: string
33
68
  }
34
69
 
35
70
  /**
@@ -39,11 +74,14 @@ export interface RenderProps {
39
74
  export interface RenderAdapter {
40
75
  /**
41
76
  * Render a route's layout `chain` (outermost layout → page) to a Web stream of HTML bytes,
42
- * including the framework's hydration markers.
77
+ * including the framework's hydration markers. When `options.nonce` is set, every executable
78
+ * `<script>` the framework streams (out-of-order Suspense runtimes, serialized resources) must
79
+ * carry it, or a nonce-based Content-Security-Policy blocks it and the boundary never resolves.
43
80
  */
44
81
  renderToStream(
45
82
  chain: readonly unknown[],
46
83
  props: RenderProps,
84
+ options?: RenderStreamOptions,
47
85
  ): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
48
86
  /** Render the chain to a complete HTML string when no content is deferred. */
49
87
  renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
@@ -67,6 +105,13 @@ export function ssrModuleLoader(): SsrModuleLoader | undefined {
67
105
  return loaderSlot[SSR_MODULE_LOADER_SLOT]
68
106
  }
69
107
 
108
+ /**
109
+ * `id` of the inert `<script type="application/json">` a hydrating document hands its page state over
110
+ * in: one JSON object keyed by the `*_GLOBAL` names below, which the client entry assigns onto
111
+ * `window` before anything reads them. Data the browser never executes needs no CSP nonce or hash.
112
+ */
113
+ export const HANDOVER_ID = "__nifra-handover"
114
+
70
115
  /** Global the server serializes loader data into; the client reads it to hydrate. */
71
116
  export const DATA_GLOBAL = "__NIFRA_DATA__"
72
117
  /** Per-layout loader data for hydration. */
@@ -16,8 +16,19 @@
16
16
  * target, and reports what the target cannot honour. Pure and fs-free: it takes an already-built
17
17
  * `Manifest` so it runs at build time, in `nifra routes`, and in a test with a hand-built manifest.
18
18
  */
19
+
20
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
19
21
  import type { Manifest, RouteModule } from "./manifest.ts"
20
22
 
23
+ export {
24
+ formatShadowedPages,
25
+ normalizeMountPath,
26
+ type ShadowedPage,
27
+ shadowedPages,
28
+ } from "./internal/mount-shadow.ts"
29
+ /** Which half of a route each export belongs in: the frontend file or its `x.backend.ts`. */
30
+ export { BACKEND_ROUTE_EXPORTS, FRONTEND_ROUTE_EXPORTS } from "./manifest.ts"
31
+
21
32
  /**
22
33
  * How a route produces its HTML.
23
34
  *
@@ -115,7 +126,8 @@ export function deriveRouteEntry(
115
126
  requires: readonly RouteCapability[]
116
127
  } = { id, pattern, mode, hydrate: module.hydrate !== false, requires }
117
128
  if (mode === "isr" && module.revalidate !== undefined) entry.revalidate = module.revalidate
118
- if (mode === "isr" && module.revalidateTags !== undefined) {
129
+ // A function's tags exist per request; the manifest records only a fixed list.
130
+ if (mode === "isr" && Array.isArray(module.revalidateTags)) {
119
131
  entry.revalidateTags = module.revalidateTags
120
132
  }
121
133
  if (prerenderedPaths !== undefined) entry.prerenderedPaths = prerenderedPaths
@@ -134,7 +146,7 @@ const TARGET_CAPABILITIES: Readonly<Record<string, readonly RouteCapability[]>>
134
146
  bun: ["server"],
135
147
  node: ["server"],
136
148
  deno: ["server"],
137
- "cf-pages": ["server"],
149
+ cloudflare: ["server"],
138
150
  vercel: ["server"],
139
151
  static: [],
140
152
  }
@@ -169,7 +181,7 @@ export async function buildRouteManifest(
169
181
  const module = (await route.load()) as RouteModule
170
182
  entries.push(deriveRouteEntry(route.id, route.pattern, module, options.prerendered?.[route.id]))
171
183
  }
172
- entries.sort((a, b) => a.pattern.localeCompare(b.pattern))
184
+ entries.sort((a, b) => codeUnitOrder(a.pattern, b.pattern))
173
185
 
174
186
  const conflicts: RouteManifestConflict[] = []
175
187
  const capabilities =