@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/llms-txt.ts CHANGED
@@ -3,9 +3,12 @@ import {
3
3
  reflectedRoutesFromEvidence,
4
4
  snapshotProjectEvidence,
5
5
  } from "@nifrajs/core/evidence"
6
+ import { type BackendEvidenceProvider, NIFRA_BACKEND_EVIDENCE } from "@nifrajs/core/mount"
6
7
  import { compileRoutePattern, type RoutePatternSegment } from "@nifrajs/core/pattern"
7
8
 
8
9
  const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/
10
+ const RESERVED_CLIENT_SEGMENT = /^(?:get|post|put|patch|delete|head|options)$/i
11
+ const RESERVED_EXACT_SEGMENTS = new Set(["subscribe", "ws", "index", "then"])
9
12
 
10
13
  /** The subset of JSON Schema fields {@link tsTypeOf} reads to render a TypeScript type string. */
11
14
  interface JsonSchemaNode {
@@ -93,7 +96,13 @@ function clientCall(method: string, path: string, schema: unknown): string {
93
96
  if (segs.length === 0) chain += ".index"
94
97
  else
95
98
  for (const seg of segs) {
96
- chain += paramCall(seg) ?? (IDENT.test(seg) ? `.${seg}` : `[${JSON.stringify(seg)}]`)
99
+ chain +=
100
+ paramCall(seg) ??
101
+ (RESERVED_CLIENT_SEGMENT.test(seg) || RESERVED_EXACT_SEGMENTS.has(seg)
102
+ ? `(${JSON.stringify(seg)})`
103
+ : IDENT.test(seg)
104
+ ? `.${seg}`
105
+ : `[${JSON.stringify(seg)}]`)
97
106
  }
98
107
  const isBodyVerb = verb === "post" || verb === "put" || verb === "patch"
99
108
  let call: string
@@ -101,12 +110,27 @@ function clientCall(method: string, path: string, schema: unknown): string {
101
110
  const bodyArg = s?.body ? "body" : s?.query ? "undefined" : ""
102
111
  const opts = s?.query ? (bodyArg ? ", { query }" : "{ query }") : ""
103
112
  call = `.${verb}(${bodyArg}${opts})`
113
+ } else if (verb === "delete") {
114
+ const fields = [...(s?.body ? ["body"] : []), ...(s?.query ? ["query"] : [])]
115
+ call = `.delete(${fields.length === 0 ? "" : `{ ${fields.join(", ")} }`})`
104
116
  } else {
105
117
  call = `.${verb}(${s?.query ? "{ query }" : ""})`
106
118
  }
107
119
  return `await ${chain}${call}`
108
120
  }
109
121
 
122
+ async function backendEvidence(backend: unknown): Promise<ProjectEvidenceSnapshot> {
123
+ if (backend !== null && (typeof backend === "object" || typeof backend === "function")) {
124
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Object/function check allows reading the optional symbol-keyed provider without touching the client path proxy.
125
+ const provider = (backend as { readonly [NIFRA_BACKEND_EVIDENCE]?: unknown })[
126
+ NIFRA_BACKEND_EVIDENCE
127
+ ]
128
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: A callable at NIFRA_BACKEND_EVIDENCE implements the framework's trusted evidence-provider seam.
129
+ if (typeof provider === "function") return (provider as BackendEvidenceProvider).call(backend)
130
+ }
131
+ return snapshotProjectEvidence(backend)
132
+ }
133
+
110
134
  /** Read the project's `AGENTS.md`, or `""` if there isn't one (or we're on an edge runtime with no
111
135
  * filesystem). Never throws - a missing file just means no guidelines section. */
112
136
  async function readAgentsMd(): Promise<string> {
@@ -134,6 +158,8 @@ export async function generateLlmsTxt(
134
158
  readonly includeLocalGuidelines?: boolean
135
159
  /** Reuse the project's canonical route-evidence pass when one is already available. */
136
160
  readonly evidence?: ProjectEvidenceSnapshot
161
+ /** The mount prefix a visitor's URL carries but the backend's own routes do not (`apiStrip`). */
162
+ readonly apiPathPrefix?: string
137
163
  } = {},
138
164
  ): Promise<string> {
139
165
  let output = ""
@@ -160,14 +186,17 @@ export async function generateLlmsTxt(
160
186
  // 3. API Routes
161
187
  output += `## API Routes\n\n`
162
188
  const apiRoutes = reflectedRoutesFromEvidence(
163
- options.evidence ?? snapshotProjectEvidence(backend),
189
+ options.evidence ?? (await backendEvidence(backend)),
164
190
  )
165
191
 
166
192
  if (apiRoutes.length === 0) {
167
193
  output += `No API routes registered.\n`
168
194
  } else {
195
+ const prefix = options.apiPathPrefix ?? ""
169
196
  for (const route of apiRoutes) {
170
- output += `- **${route.method}** \`${route.path}\`\n`
197
+ // The stripped backend's root is the bare prefix, as the mount matches it.
198
+ const path = prefix !== "" && route.path === "/" ? prefix : prefix + route.path
199
+ output += `- **${route.method}** \`${path}\`\n`
171
200
  if (full) {
172
201
  output += ` - Client Call: \`${clientCall(route.method, route.path, route.schema)}\`\n`
173
202
  const s = route.schema
package/src/manifest.ts CHANGED
@@ -5,7 +5,12 @@
5
5
  * portable (no fs, no DOM) and fully unit-testable. Edge deploys pre-build the manifest.
6
6
  */
7
7
  import { routePatternOverlap } from "@nifrajs/core"
8
- import { paramConstraint } from "@nifrajs/core/pattern"
8
+ import {
9
+ type CompiledRoutePattern,
10
+ compareRoutePatternSpecificity,
11
+ compileRoutePattern,
12
+ paramConstraint,
13
+ } from "@nifrajs/core/pattern"
9
14
  import type { CookieOptions, ResponseResult, StandardSchemaV1 } from "@nifrajs/core/server"
10
15
  import type { BoundaryDescriptor, BoundaryRegistration } from "./boundary.ts"
11
16
  import { guardChannel, outputGuard } from "./internal/output-guard.ts"
@@ -266,13 +271,24 @@ export interface Meta {
266
271
  }
267
272
 
268
273
  /**
269
- * Args for a route's `meta` function: the loader's `data` + the route `params` + the request `origin`.
270
- * `meta()` runs in BOTH SSR and client navigation, so it has **no `request`/`process.env`/server access** -
271
- * `origin` is the only server-resolved fact it gets (so you needn't thread `siteUrl` through loader data
272
- * for absolute `og:url`/`canonical`/`og:image` URLs). See {@link origin}.
274
+ * Args for a route's or layout's `meta` function: its own loader's `data` + the page's `pageData` + the
275
+ * route `params` + the request `origin`. `meta()` runs in BOTH SSR and client navigation, so it has
276
+ * **no `request`/`process.env`/server access** - `origin` is the only server-resolved fact it gets (so you
277
+ * needn't thread `siteUrl` through loader data for absolute `og:url`/`canonical`/`og:image` URLs). See
278
+ * {@link origin}.
273
279
  */
274
280
  export interface MetaArgs<Data = unknown> {
281
+ /**
282
+ * The loader data of the module that exports this `meta`: the page's for a page, the layout's own for a
283
+ * layout (`null` when the layout has no loader), the same value its component receives. A layout that
284
+ * loads the locale sets `lang` from here, so a soft navigation keeps it.
285
+ */
275
286
  readonly data: Data
287
+ /**
288
+ * The page's loader data, for a layout's `meta` that builds on the page (a title template). In a page's
289
+ * own `meta` it is the same value as {@link data}. Always set by the framework.
290
+ */
291
+ readonly pageData?: unknown
276
292
  readonly params: Record<string, string>
277
293
  /** The resolved document CSP nonce, when this render uses per-request nonce protection. */
278
294
  readonly nonce?: string
@@ -318,6 +334,8 @@ export type GetStaticPaths = () => StaticPaths | Promise<StaticPaths>
318
334
  export interface RouteModule {
319
335
  readonly default: unknown
320
336
  readonly loader?: Loader
337
+ /** Layout loader that must run before actions and is not retained across the action's render. */
338
+ readonly gate?: boolean
321
339
  readonly action?: Action
322
340
  /**
323
341
  * Backend half only: the schema of what {@link loader} sends to the browser. Required whenever the
@@ -379,6 +397,14 @@ export interface RouteModule {
379
397
  * the module on the server and in the browser, so it must be the same value on both.
380
398
  */
381
399
  readonly handle?: unknown
400
+ /**
401
+ * On a root status page (`_404.tsx`, `_<status>.tsx`) or an `_error.tsx`. By default (`true`) the
402
+ * page renders inside the layouts above it with their loader data and `meta`, as a nested `_404`
403
+ * renders. When those layouts cannot render it - a loader throws, a gate or the middleware refuses -
404
+ * a root status page renders alone and an `_error` inside its layouts without their data, with the
405
+ * same status. `false` always renders them that way, and runs no layout loader for them.
406
+ */
407
+ readonly inLayouts?: boolean
382
408
  /**
383
409
  * Opt a **static** route (no `:param`/`*`) into build-time prerendering (SSG): `prerenderRoutes`
384
410
  * (from `@nifrajs/web/build`) renders it to a static `index.html` at build. The loader runs at build
@@ -993,6 +1019,9 @@ const ancestorDirs = (file: string): string[] => {
993
1019
  * turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
994
1020
  * RouteConfigError ethos) on duplicate patterns. `_layout`/`_404`/`_error`/`_loading`/`_middleware`
995
1021
  * files are special; other `_`-prefixed files are ignored (private/colocated, never routed).
1022
+ * A page pairs with its `.backend.*` half only when both are in `files`, so a hand-written list that
1023
+ * omits the half serves the page without its loader and guards; `discoverRoutes` from
1024
+ * `@nifrajs/web/fs` scans a routes dir and never does.
996
1025
  */
997
1026
  export function buildManifest(
998
1027
  files: readonly string[],
@@ -1109,6 +1138,7 @@ export function buildManifest(
1109
1138
 
1110
1139
  const byPattern = new Map<string, string>()
1111
1140
  const routes: RouteEntry[] = []
1141
+ const shapes: { file: string; pattern: string; shape: CompiledRoutePattern }[] = []
1112
1142
  for (const file of routeFiles) {
1113
1143
  const dirs = ancestorDirs(file)
1114
1144
  const layoutDirsForFile = dirs.filter((dir) => layoutDirs.has(dir))
@@ -1132,14 +1162,20 @@ export function buildManifest(
1132
1162
  `[nifra/web] duplicate route: "${file}" and "${existing}" both map to "${pattern}"`,
1133
1163
  )
1134
1164
  }
1135
- for (const previous of routes) {
1165
+ // A path two shapes share goes to the more specific one (static > mixed > param > wildcard,
1166
+ // segment by segment - the router's own order), and each keeps paths of its own, so
1167
+ // `users/me.tsx` beside `users/[id].tsx` is ordinary. Only one shape twice is ambiguous.
1168
+ const shape = compileRoutePattern(pattern)
1169
+ for (const previous of shapes) {
1170
+ if (compareRoutePatternSpecificity(previous.shape, shape) !== 0) continue
1136
1171
  const witness = routePatternOverlap(previous.pattern, pattern)
1137
1172
  if (witness !== undefined) {
1138
1173
  throw new Error(
1139
- `[nifra/web] overlapping routes: "${previous.file}" (${previous.pattern}) and "${file}" (${pattern}) both match "${witness}"; make the patterns disjoint or remove one route`,
1174
+ `[nifra/web] overlapping routes: "${previous.file}" (${previous.pattern}) and "${file}" (${pattern}) both match "${witness}" with the same shape, so one of them could never be served; make the patterns disjoint or remove one route`,
1140
1175
  )
1141
1176
  }
1142
1177
  }
1178
+ shapes.push({ file, pattern, shape })
1143
1179
  byPattern.set(pattern, file)
1144
1180
  routes.push({
1145
1181
  id,
@@ -0,0 +1,64 @@
1
+ import { constants as FS } from "node:fs"
2
+ import { type FileHandle, open, readlink, realpath, stat } from "node:fs/promises"
3
+
4
+ // Darwin's open(2) flag refusing a symlink in ANY component; `O_NOFOLLOW` covers only the last one.
5
+ const O_NOFOLLOW_ANY = 0x20000000
6
+ // Deno's node:fs rejects flags it does not know with EINVAL; the first refusal turns the flag off.
7
+ let noFollowAny: boolean | undefined
8
+
9
+ /**
10
+ * Open a name `realpath` already resolved, refusing it when any component became a symlink since.
11
+ * A local writer inside the served tree can swap an intermediate directory for a link after the
12
+ * containment check; `O_NOFOLLOW` alone would follow it. Darwin refuses such a path in the kernel;
13
+ * Linux reads back the name the descriptor holds. Elsewhere, Windows included, no file API can open
14
+ * without following a middle link or name an open descriptor's path, so the check in
15
+ * {@link opensAt} is a platform limit: it narrows the race to repeated swaps but cannot close it.
16
+ */
17
+ export async function openResolved(realFile: string): Promise<FileHandle> {
18
+ // Read per open, not at load: this module can sit in a client bundle graph, where `process` is
19
+ // undefined.
20
+ if (noFollowAny ?? process.platform === "darwin") {
21
+ try {
22
+ return await open(realFile, FS.O_RDONLY | O_NOFOLLOW_ANY)
23
+ } catch (error) {
24
+ if (!(error instanceof Error && "code" in error && error.code === "EINVAL")) throw error
25
+ noFollowAny = false
26
+ }
27
+ }
28
+ const handle = await open(realFile, FS.O_RDONLY | FS.O_NOFOLLOW)
29
+ try {
30
+ if (await opensAt(handle, realFile)) return handle
31
+ } catch {}
32
+ await handle.close()
33
+ throw Object.assign(new Error(`${realFile} moved while it was opened`), { code: "ELOOP" })
34
+ }
35
+
36
+ /** Whether `handle` is the file at `realFile` with no symlink in its path. */
37
+ export async function opensAt(
38
+ handle: Pick<FileHandle, "fd" | "stat">,
39
+ realFile: string,
40
+ ): Promise<boolean> {
41
+ try {
42
+ return (await readlink(`/proc/self/fd/${handle.fd}`)) === realFile
43
+ } catch {
44
+ return sameFileAt(handle, realFile)
45
+ }
46
+ }
47
+
48
+ /**
49
+ * {@link opensAt} where no descriptor's path can be read. Exported for its test, which runs it on
50
+ * every host.
51
+ */
52
+ export async function sameFileAt(
53
+ handle: Pick<FileHandle, "stat">,
54
+ realFile: string,
55
+ ): Promise<boolean> {
56
+ // One step after another, so passing takes a swap back and a swap again between steps: the name
57
+ // resolves to itself, then to the opened file, then to itself again. Identities are compared as
58
+ // bigints because a Windows file index can exceed 2^53.
59
+ if ((await realpath(realFile)) !== realFile) return false
60
+ const opened = await handle.stat({ bigint: true })
61
+ const named = await stat(realFile, { bigint: true })
62
+ if (opened.dev !== named.dev || opened.ino !== named.ino) return false
63
+ return (await realpath(realFile)) === realFile
64
+ }
@@ -4,7 +4,7 @@
4
4
  * CSS→client-bundle wiring, the deterministic hash, or the optional-peer guard. Internal to
5
5
  * `@nifrajs/web` (imported by the sibling plugin modules); not a public subpath.
6
6
  */
7
- import { existsSync } from "node:fs"
7
+ import { existsSync, readFileSync } from "node:fs"
8
8
  import { dirname, join, relative } from "node:path"
9
9
  import { fileURLToPath, pathToFileURL } from "node:url"
10
10
  import type { BunPlugin } from "bun"
@@ -23,7 +23,7 @@ export type PluginBuilder = Parameters<BunPlugin["setup"]>[0]
23
23
  */
24
24
  export { rewriteSsrImports } from "../dev-ssr-graph.ts"
25
25
 
26
- const packageRootCache = new Map<string, string>()
26
+ const packageCache = new Map<string, PackageOf>()
27
27
 
28
28
  /**
29
29
  * Convert a plugin/runtime path into a path the host filesystem can open.
@@ -57,14 +57,31 @@ export function portablePath(path: string): string {
57
57
  return normalizeFilePath(path).replaceAll("\\", "/")
58
58
  }
59
59
 
60
- /** The nearest ancestor directory of `startDir` that holds a `package.json` (the file's package root),
61
- * cached. Falls back to `startDir` if none is found, so it never throws. */
62
- function packageRootOf(startDir: string): string {
63
- const cached = packageRootCache.get(startDir)
60
+ interface PackageOf {
61
+ readonly root: string
62
+ readonly name: string
63
+ }
64
+
65
+ /** The nearest ancestor directory of `startDir` that holds a `package.json` (the file's package root)
66
+ * and that package's `name` (`""` when it has none), cached. Falls back to `startDir` if none is found,
67
+ * so it never throws. */
68
+ function packageOf(startDir: string): PackageOf {
69
+ const cached = packageCache.get(startDir)
64
70
  if (cached !== undefined) return cached
65
71
  let dir = startDir
72
+ let name = ""
66
73
  for (;;) {
67
- if (existsSync(join(dir, "package.json"))) break
74
+ const manifest = join(dir, "package.json")
75
+ if (existsSync(manifest)) {
76
+ try {
77
+ const parsed: unknown = JSON.parse(readFileSync(manifest, "utf8"))
78
+ if (typeof parsed === "object" && parsed !== null && "name" in parsed)
79
+ name = typeof parsed.name === "string" ? parsed.name : ""
80
+ } catch {
81
+ // An unreadable manifest still marks the package root; the name is only a disambiguator.
82
+ }
83
+ break
84
+ }
68
85
  const parent = dirname(dir)
69
86
  if (parent === dir) {
70
87
  dir = startDir // reached the filesystem root with no package.json - anchor on the file's own dir
@@ -72,8 +89,9 @@ function packageRootOf(startDir: string): string {
72
89
  }
73
90
  dir = parent
74
91
  }
75
- packageRootCache.set(startDir, dir)
76
- return dir
92
+ const found = { root: dir, name }
93
+ packageCache.set(startDir, found)
94
+ return found
77
95
  }
78
96
 
79
97
  /**
@@ -84,11 +102,13 @@ function packageRootOf(startDir: string): string {
84
102
  * different machines or from different cwds - derive the SAME relative path for a given file, so their
85
103
  * scoped class maps always agree. (Hashing the absolute path would differ across CI/host; a
86
104
  * cwd-relative path would silently desync if the SSR server started from a non-project-root cwd.)
105
+ * The package's `name` leads the path, so `src/Card.vue` in two workspace packages hashes apart.
87
106
  */
88
107
  export function reproduciblePath(absolutePath: string): string {
89
108
  const filePath = normalizeFilePath(absolutePath)
90
- const root = packageRootOf(dirname(filePath))
91
- return relative(root, filePath).replaceAll("\\", "/")
109
+ const { root, name } = packageOf(dirname(filePath))
110
+ const path = relative(root, filePath).replaceAll("\\", "/")
111
+ return name === "" ? path : `${name}/${path}`
92
112
  }
93
113
 
94
114
  /**
@@ -251,6 +251,7 @@ function secretLeak(
251
251
  artifacts,
252
252
  env: options.secrets?.env ?? process.env,
253
253
  publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
254
+ // biome-ignore lint/plugin/noConditionalEmptyObjectSpread: Explicitly configured secret-scan exemptions preserve their absence by default.
254
255
  ...(options.secrets?.exemptions ? { exemptions: options.secrets.exemptions } : {}),
255
256
  }),
256
257
  )
@@ -260,7 +261,7 @@ function secretLeak(
260
261
  export type ServerZoneGuardOptions = Pick<
261
262
  LeakGuardOptions,
262
263
  "appRoot" | "routesDir" | "generatedFiles" | "publicEnvPrefix"
263
- >
264
+ > & { readonly componentModules?: ReadonlySet<string> }
264
265
 
265
266
  /**
266
267
  * The server build's half of the zone rules, with the same checks and message as nifra's Bun server
@@ -285,6 +286,9 @@ export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakG
285
286
  classifier,
286
287
  sourceOf: rollupModuleSource,
287
288
  privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
289
+ ...(options.componentModules === undefined
290
+ ? {}
291
+ : { componentModules: options.componentModules }),
288
292
  }),
289
293
  )
290
294
  if (leak === undefined) return
@@ -7,6 +7,7 @@
7
7
  * and the refusal must stop the response.
8
8
  */
9
9
 
10
+ import { statSync } from "node:fs"
10
11
  import { dirname, isAbsolute, relative, resolve } from "node:path"
11
12
  import type { BunPlugin } from "bun"
12
13
  import { privateEnvDenial } from "../internal/private-env.ts"
@@ -23,16 +24,21 @@ const SOURCE_EXTENSIONS =
23
24
  * (images, fonts, wasm, and a `.pem` or `.sql` imported `with { type: "text" }`) goes through
24
25
  * `onResolve`: a file-loader asset panics Bun 1.4 on a declining `onLoad`. */
25
26
  const SOURCE_FILE = new RegExp(`\\.(?:${SOURCE_EXTENSIONS})$`)
27
+ const ZONE_SUFFIXES = "frontend|backend|shared|fn"
26
28
  /**
27
- * A relative or absolute import whose last segment has an extension, other than a source file.
29
+ * A relative or absolute import whose last segment has an extension, other than a source file. A zone
30
+ * suffix is no extension: `"./calendar.shared"` imports `calendar.shared.ts`.
28
31
  * Source files are excluded by the FILTER, not declined by the handler: on Bun's dev server a filter
29
32
  * match alone changes how the import resolves, even when the handler declines (see
30
33
  * `internal/dev-reserved.ts`), and a source file is the onLoad hook's to judge anyway.
31
34
  */
32
35
  const NON_SOURCE_PATH = new RegExp(
33
- `^(?:\\.|\\/|[A-Za-z]:[\\\\/])(?![^?#]*\\.(?:${SOURCE_EXTENSIONS})(?:[?#]|$)).*\\.[^./\\\\?#]+(?:[?#].*)?$`,
36
+ `^(?:\\.|\\/|[A-Za-z]:[\\\\/])(?![^?#]*\\.(?:${SOURCE_EXTENSIONS}|${ZONE_SUFFIXES})(?:[?#]|$)).*\\.[^./\\\\?#]+(?:[?#].*)?$`,
34
37
  )
35
38
 
39
+ const isFile = (path: string): boolean =>
40
+ statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
41
+
36
42
  export interface ZoneGuardOptions {
37
43
  /** The app root: the directory holding `routes/`, `frontend/`, `backend/` and `shared/`. */
38
44
  readonly appRoot: string
@@ -88,6 +94,9 @@ export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
88
94
  // An entry point (a dev server's HTML page) is the build's own choice, not an import.
89
95
  if (args.importer === "") return undefined
90
96
  const file = resolve(dirname(args.importer), args.path.replace(/[?#].*$/, ""))
97
+ // No such file means an extensionless import of a dotted source name (`./date.utils`): Bun resolves
98
+ // it to the source file, which onLoad judges. Judging the bare path would refuse a file never loaded.
99
+ if (!isFile(file)) return undefined
91
100
  const reason = browserDenial(classifier.classify(file))
92
101
  return reason === undefined ? undefined : refuse(file, reason, args.importer)
93
102
  })
package/src/public-dir.ts CHANGED
@@ -12,12 +12,12 @@
12
12
  * Distinct from `publicPath` in `build.ts`, which is the URL prefix for content-hashed bundle chunks.
13
13
  * The names collide and the concepts do not: `publicPath` never covers user-authored files.
14
14
  */
15
- import { constants as FS } from "node:fs"
16
- import { open, realpath } from "node:fs/promises"
15
+ import { realpath } from "node:fs/promises"
17
16
  import { extname, normalize, resolve, sep } from "node:path"
18
17
  import { Readable } from "node:stream"
19
18
  import { parseByteRange } from "@nifrajs/core/range"
20
19
  import { pathnameOf } from "@nifrajs/core/server"
20
+ import { openResolved } from "./open-resolved.ts"
21
21
 
22
22
  /** How long each subtree may be cached. Content-hashed bundle output can be immutable; a
23
23
  * user-authored file keeps its name across deploys, so it gets a day and a revalidation. */
@@ -31,8 +31,15 @@ export interface PublicDirCache {
31
31
  export interface ServePublicDirOptions {
32
32
  /** Absolute path of the directory to serve. */
33
33
  readonly dir: string
34
- /** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`). */
34
+ /** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`).
35
+ * Ignored when `hashedFiles` is set. */
35
36
  readonly hashedPrefix?: string
37
+ /**
38
+ * Encoded URL paths of the content-hashed files, such as a build manifest's `assets`. When set, only
39
+ * these are cached immutably and every other file gets the `assets` policy, so a user-authored file
40
+ * that happens to sit under the hashed prefix still revalidates.
41
+ */
42
+ readonly hashedFiles?: ReadonlySet<string>
36
43
  readonly cache?: PublicDirCache
37
44
  /** Optional encoded URL-path allowlist. When present, route misses avoid a filesystem probe. */
38
45
  readonly files?: ReadonlySet<string>
@@ -166,6 +173,7 @@ export function servePublicDir(
166
173
  const root = resolve(options.dir)
167
174
  const rootReal = realpath(root).catch(() => undefined)
168
175
  const hashedPrefix = options.hashedPrefix ?? "/assets/"
176
+ const hashedFiles = options.hashedFiles
169
177
  const hashed = options.cache?.hashed ?? IMMUTABLE
170
178
  const assets = options.cache?.assets ?? ONE_DAY
171
179
 
@@ -192,16 +200,27 @@ export function servePublicDir(
192
200
  }
193
201
  // Open the canonical path once and stream from that descriptor. Reopening the pathname after
194
202
  // realpath containment checks would let a writable public tree swap a symlink between validation
195
- // and the actual read. O_NOFOLLOW also rejects a final-component symlink if one appears before
196
- // the open; the descriptor then pins the bytes for the lifetime of the response.
197
- let handle: Awaited<ReturnType<typeof open>> | undefined
203
+ // and the actual read. openResolved also refuses the name once any component of it became a
204
+ // symlink; the descriptor then pins the bytes for the lifetime of the response.
205
+ let handle: Awaited<ReturnType<typeof openResolved>> | undefined
198
206
  try {
199
- handle = await open(resolvedFile, FS.O_RDONLY | FS.O_NOFOLLOW)
207
+ try {
208
+ handle = await openResolved(resolvedFile)
209
+ } catch {
210
+ // A name refused or gone since realpath falls through, as a failed realpath does.
211
+ return undefined
212
+ }
200
213
  const stat = await handle.stat()
201
214
  if (!stat.isFile()) return undefined
202
215
  const size = stat.size
203
216
  const headers = new Headers({
204
- "cache-control": pathname.startsWith(hashedPrefix) ? hashed : assets,
217
+ "cache-control": (
218
+ hashedFiles === undefined
219
+ ? pathname.startsWith(hashedPrefix)
220
+ : hashedFiles.has(pathname)
221
+ )
222
+ ? hashed
223
+ : assets,
205
224
  // Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
206
225
  // so a video or audio file under `public/` is scrubbable only once this header is present.
207
226
  "accept-ranges": "bytes",