@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
@@ -0,0 +1,263 @@
1
+ /**
2
+ * The zone guard for Vite's dev server. A dev server serves source by URL, so the guard has to hold
3
+ * before any byte is sent, whatever the URL form:
4
+ *
5
+ * - a middleware ahead of Vite's own answers 403 for any request that maps to a file the browser may
6
+ * not have: a direct path, `/@fs/`, `?raw`, `?url`, `?import`, a `.map`, a pre-bundled dependency;
7
+ * - `resolveId` refuses a browser import of backend code at the importer, so the overlay names the line
8
+ * instead of the browser hitting a bare 403;
9
+ * - `transform` refuses whatever reaches the client environment by another route.
10
+ *
11
+ * The SSR environment loads the same files legitimately and is never checked here.
12
+ */
13
+ import { existsSync, readFileSync, statSync } from "node:fs"
14
+ import type { IncomingMessage, ServerResponse } from "node:http"
15
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
16
+ import { privateEnvDenial } from "../internal/private-env.ts"
17
+ import {
18
+ browserDenial,
19
+ type Classification,
20
+ createZoneClassifier,
21
+ SERVER_PACKAGES,
22
+ specifierPackage,
23
+ } from "../zones.ts"
24
+
25
+ export interface ViteZoneGuardOptions {
26
+ /** The app root: the directory holding `routes/`, `frontend/`, `backend/` and `shared/`. */
27
+ readonly appRoot: string
28
+ /** The routes directory (default `<appRoot>/routes`). */
29
+ readonly routesDir?: string
30
+ /** Generated client modules (the dev entry). */
31
+ readonly generatedFiles?: readonly string[]
32
+ /** The public-env prefix browser code may read (default `"PUBLIC_"`). */
33
+ readonly publicEnvPrefix?: string
34
+ }
35
+
36
+ /** Zones whose code runs in a browser, so may read only public environment variables. */
37
+ const BROWSER_CODE = new Set<string>(["route-frontend", "frontend", "shared"])
38
+
39
+ type Next = (error?: unknown) => void
40
+ type Middleware = (req: IncomingMessage, res: ServerResponse, next: Next) => void
41
+
42
+ interface PluginContext {
43
+ readonly environment?: { readonly name?: string }
44
+ error(error: Error | string): never
45
+ resolve(
46
+ source: string,
47
+ importer: string | undefined,
48
+ options: { readonly skipSelf: boolean } & Record<string, unknown>,
49
+ ): Promise<{ readonly id: string; readonly external?: boolean | string } | null>
50
+ }
51
+
52
+ /** The slice of a Vite plugin this returns. Structural, so `vite` stays an optional peer. */
53
+ export interface ViteZoneGuardPlugin {
54
+ readonly name: string
55
+ readonly enforce: "pre"
56
+ config(): { optimizeDeps: { exclude: string[] } }
57
+ configResolved(config: { readonly root: string }): void
58
+ configureServer(server: { readonly middlewares: { use(middleware: Middleware): void } }): void
59
+ resolveId(
60
+ this: PluginContext,
61
+ source: string,
62
+ importer: string | undefined,
63
+ options?: { readonly ssr?: boolean; readonly scan?: boolean },
64
+ ): Promise<null>
65
+ transform(
66
+ this: PluginContext,
67
+ code: string,
68
+ id: string,
69
+ options?: { readonly ssr?: boolean },
70
+ ): null
71
+ /**
72
+ * Why the module a request URL names failed to serve, when the guard refused one of its imports.
73
+ * Vite in middleware mode reports a transform error to the overlay and then passes the request on
74
+ * instead of answering it; the dev server answers with this.
75
+ */
76
+ refusalFor(url: string): string | undefined
77
+ /** The source file a request URL names, when it names one. */
78
+ fileForUrl(url: string): string | undefined
79
+ }
80
+
81
+ const stripQuery = (id: string): string => id.replace(/[?#].*$/, "")
82
+ /** Query forms that hand a module's source, or a URL to it, to the browser untransformed. */
83
+ const RAW_QUERY = /(?:^|&)(?:raw|url|inline)(?:[&=]|$)/
84
+
85
+ const isFile = (path: string): boolean => {
86
+ try {
87
+ return statSync(path).isFile()
88
+ } catch {
89
+ return false
90
+ }
91
+ }
92
+
93
+ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugin {
94
+ const classifier = createZoneClassifier(options)
95
+ let root = resolve(options.appRoot)
96
+ const display = (file: string): string => {
97
+ const rel = relative(classifier.appRoot, file).replaceAll("\\", "/")
98
+ return rel.startsWith("..") || isAbsolute(rel) ? file : rel
99
+ }
100
+ const isClient = (context: PluginContext, ssr: boolean | undefined): boolean =>
101
+ ssr !== true && (context.environment?.name ?? "client") === "client"
102
+
103
+ /** What a pre-bundled dependency file was built from, per Vite's `_metadata.json`. */
104
+ const depsSources = new Map<string, { mtime: number; sources: Map<string, string> }>()
105
+ const depSource = (file: string): string | undefined => {
106
+ const dir = dirname(file)
107
+ const metadata = join(dir, "_metadata.json")
108
+ let mtime: number
109
+ try {
110
+ mtime = statSync(metadata).mtimeMs
111
+ } catch {
112
+ return undefined
113
+ }
114
+ let cached = depsSources.get(dir)
115
+ if (cached === undefined || cached.mtime !== mtime) {
116
+ const sources = new Map<string, string>()
117
+ try {
118
+ const parsed = JSON.parse(readFileSync(metadata, "utf8")) as {
119
+ optimized?: Record<string, { src?: string; file?: string }>
120
+ }
121
+ for (const dep of Object.values(parsed.optimized ?? {})) {
122
+ if (dep.src !== undefined && dep.file !== undefined) {
123
+ sources.set(resolve(dir, dep.file), resolve(dir, dep.src))
124
+ }
125
+ }
126
+ } catch {
127
+ // An unreadable metadata file maps nothing; the request then classifies as the cache file.
128
+ }
129
+ cached = { mtime, sources }
130
+ depsSources.set(dir, cached)
131
+ }
132
+ return cached.sources.get(file)
133
+ }
134
+
135
+ /** The file a request URL serves, or `undefined` when it names none (an app route, a Vite internal). */
136
+ const fileForUrl = (url: string): string | undefined => {
137
+ let path: string
138
+ try {
139
+ path = decodeURIComponent(stripQuery(url))
140
+ } catch {
141
+ return undefined
142
+ }
143
+ let file: string
144
+ if (path.startsWith("/@fs/")) file = path.slice("/@fs".length).replace(/^\/([a-zA-Z]:)/, "$1")
145
+ else if (path.startsWith("/@id/")) {
146
+ const id = path.slice("/@id/".length)
147
+ if (!isAbsolute(id)) return undefined
148
+ file = id
149
+ } else if (path.startsWith("/@")) return undefined
150
+ else file = join(root, path)
151
+ for (const candidate of [file, file.replace(/\.map$/, "")]) {
152
+ // One spelling per file: Vite names it `C:/app/x.ts` on Windows, `join` names it `C:\app\x.ts`.
153
+ if (isFile(candidate)) return resolve(candidate)
154
+ }
155
+ return undefined
156
+ }
157
+
158
+ const middleware: Middleware = (req, res, next) => {
159
+ const url = req.url ?? "/"
160
+ const file = fileForUrl(url)
161
+ if (file === undefined) return next()
162
+ const source = file.replaceAll("\\", "/").includes("/node_modules/.vite/")
163
+ ? (depSource(file) ?? file)
164
+ : file
165
+ const classification = classifier.classify(source)
166
+ let reason = browserDenial(classification)
167
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
168
+ if (reason === undefined && classification.zone === "fn" && RAW_QUERY.test(query)) {
169
+ reason = "a server-function module reaches the browser only as its generated stub"
170
+ }
171
+ if (reason === undefined) return next()
172
+ res.statusCode = 403
173
+ res.setHeader("content-type", "text/plain; charset=utf-8")
174
+ res.setHeader("cache-control", "no-store")
175
+ res.end(`[nifra] ${display(source)} may not reach the browser: ${reason}\n`)
176
+ }
177
+
178
+ const refusals = new Map<string, string>()
179
+ const refuse = (
180
+ context: PluginContext,
181
+ file: string,
182
+ reason: string,
183
+ importer?: string,
184
+ ): never => {
185
+ const message = `[nifra/web] ${display(file)} may not reach the browser${importer === undefined ? "" : ` (imported by ${display(importer)})`}: ${reason}`
186
+ refusals.set(resolve(importer ?? file), message)
187
+ return context.error(new Error(message))
188
+ }
189
+
190
+ /** A bare specifier's package, classified through its manifest so a workspace link is first-party. */
191
+ const packageClassification = (name: string, importer: string): Classification => {
192
+ let dir = dirname(importer)
193
+ for (;;) {
194
+ const manifest = join(dir, "node_modules", name, "package.json")
195
+ if (existsSync(manifest)) return classifier.classify(manifest)
196
+ const parent = dirname(dir)
197
+ if (parent === dir) return { zone: "library", packageName: name }
198
+ dir = parent
199
+ }
200
+ }
201
+
202
+ return {
203
+ name: "nifra:zone-guard",
204
+ enforce: "pre",
205
+ refusalFor(url) {
206
+ const file = fileForUrl(url)
207
+ return file === undefined ? undefined : refusals.get(file)
208
+ },
209
+ fileForUrl,
210
+ // A pre-bundled copy would serve a server package under `.vite/deps/` instead of its own path.
211
+ config: () => ({ optimizeDeps: { exclude: [...SERVER_PACKAGES] } }),
212
+ configResolved(config) {
213
+ root = config.root
214
+ },
215
+ configureServer(server) {
216
+ // Registered directly (not in a returned post hook), so it runs ahead of Vite's own middlewares.
217
+ server.middlewares.use(middleware)
218
+ },
219
+ async resolveId(source, importer, resolveOptions) {
220
+ if (importer === undefined || resolveOptions?.scan === true) return null
221
+ if (!isClient(this, resolveOptions?.ssr)) return null
222
+ const importerFile = stripQuery(importer)
223
+ if (!isAbsolute(importerFile)) return null
224
+ if (source.startsWith(".")) {
225
+ const target = resolve(dirname(importerFile), stripQuery(source))
226
+ if (isFile(target)) {
227
+ const reason = browserDenial(classifier.classify(target))
228
+ if (reason !== undefined) refuse(this, target, reason, importerFile)
229
+ }
230
+ return null
231
+ }
232
+ const name = specifierPackage(source)
233
+ if (name !== undefined) {
234
+ const reason = browserDenial(packageClassification(name, importerFile), source)
235
+ if (reason !== undefined) refuse(this, source, reason, importerFile)
236
+ }
237
+ // An alias, a tsconfig path or a package export: what the specifier resolves to decides.
238
+ const resolved = await this.resolve(source, importer, { ...resolveOptions, skipSelf: true })
239
+ const target = resolved === null || resolved.external ? undefined : stripQuery(resolved.id)
240
+ if (target !== undefined && isAbsolute(target) && isFile(target)) {
241
+ const reason = browserDenial(classifier.classify(target), source)
242
+ if (reason !== undefined) refuse(this, target, reason, importerFile)
243
+ }
244
+ return null
245
+ },
246
+ transform(code, id, transformOptions) {
247
+ if (!isClient(this, transformOptions?.ssr)) return null
248
+ const file = stripQuery(id)
249
+ if (!isAbsolute(file) || !isFile(file)) return null
250
+ // A fresh transform re-resolves every import, so an earlier refusal is stale until it recurs.
251
+ refusals.delete(resolve(file))
252
+ const classification = classifier.classify(file)
253
+ // A `pre` transform sees the file as written, so the env check reads the source the author wrote.
254
+ const reason =
255
+ browserDenial(classification) ??
256
+ (BROWSER_CODE.has(classification.zone) && id === file
257
+ ? privateEnvDenial(file, code, options.publicEnvPrefix ?? "PUBLIC_")
258
+ : undefined)
259
+ if (reason !== undefined) refuse(this, file, reason)
260
+ return null
261
+ },
262
+ }
263
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The zone guard for Bun's bundler: every file the browser bundle loads is classified, and one that may
3
+ * not reach a browser stops the build (or the dev request) before its code is bundled.
4
+ *
5
+ * The client build runs it in `record` mode and reports every refusal together with its import chain,
6
+ * read from the finished graph; the dev server runs it in `throw` mode, where there is no finished graph
7
+ * and the refusal must stop the response.
8
+ */
9
+
10
+ import { dirname, isAbsolute, relative, resolve } from "node:path"
11
+ import type { BunPlugin } from "bun"
12
+ import { privateEnvDenial } from "../internal/private-env.ts"
13
+ import {
14
+ BACKEND_ONLY_MARKER,
15
+ browserDenial,
16
+ createZoneClassifier,
17
+ type ZoneClassifier,
18
+ } from "../zones.ts"
19
+
20
+ const SOURCE_EXTENSIONS =
21
+ "[cm]?[jt]sx?|svelte|vue|mdx|astro|css|scss|sass|less|json|jsonc|toml|ya?ml|txt|md"
22
+ /** Source files Bun loads through a loader that tolerates a declining `onLoad`. Every other file
23
+ * (images, fonts, wasm, and a `.pem` or `.sql` imported `with { type: "text" }`) goes through
24
+ * `onResolve`: a file-loader asset panics Bun 1.4 on a declining `onLoad`. */
25
+ const SOURCE_FILE = new RegExp(`\\.(?:${SOURCE_EXTENSIONS})$`)
26
+ /**
27
+ * A relative or absolute import whose last segment has an extension, other than a source file.
28
+ * Source files are excluded by the FILTER, not declined by the handler: on Bun's dev server a filter
29
+ * match alone changes how the import resolves, even when the handler declines (see
30
+ * `internal/dev-reserved.ts`), and a source file is the onLoad hook's to judge anyway.
31
+ */
32
+ const NON_SOURCE_PATH = new RegExp(
33
+ `^(?:\\.|\\/|[A-Za-z]:[\\\\/])(?![^?#]*\\.(?:${SOURCE_EXTENSIONS})(?:[?#]|$)).*\\.[^./\\\\?#]+(?:[?#].*)?$`,
34
+ )
35
+
36
+ export interface ZoneGuardOptions {
37
+ /** The app root: the directory holding `routes/`, `frontend/`, `backend/` and `shared/`. */
38
+ readonly appRoot: string
39
+ /** The routes directory (default `<appRoot>/routes`). */
40
+ readonly routesDir?: string
41
+ /** The build's own generated entry modules. */
42
+ readonly generatedFiles?: readonly string[]
43
+ /** Collect refusals instead of throwing; the caller reports them with chains after the build. */
44
+ readonly onDenied?: (file: string, reason: string) => void
45
+ /** Share a classifier with post-build verification. */
46
+ readonly classifier?: ZoneClassifier
47
+ /** The public-env prefix browser code may read (default `"PUBLIC_"`). Checked here only in throw
48
+ * mode; a build checks the finished graph instead. */
49
+ readonly publicEnvPrefix?: string
50
+ }
51
+
52
+ /** Zones whose code runs in a browser, so may read only public environment variables. */
53
+ const BROWSER_CODE = new Set<string>(["route-frontend", "frontend", "shared"])
54
+
55
+ export function zoneGuardPlugin(options: ZoneGuardOptions): BunPlugin {
56
+ const classifier = options.classifier ?? createZoneClassifier(options)
57
+ const display = (file: string): string => {
58
+ const rel = relative(classifier.appRoot, file).replaceAll("\\", "/")
59
+ return rel.startsWith("..") || isAbsolute(rel) ? file : rel
60
+ }
61
+ const refuse = (file: string, reason: string, importer?: string): undefined => {
62
+ if (options.onDenied !== undefined) {
63
+ options.onDenied(file, reason)
64
+ return undefined
65
+ }
66
+ const via =
67
+ importer === undefined || importer === "" ? "" : ` (imported by ${display(importer)})`
68
+ throw new Error(`[nifra/web] ${display(file)} may not reach the browser${via}: ${reason}`)
69
+ }
70
+ return {
71
+ name: "nifra-zone-guard",
72
+ setup(build) {
73
+ build.onLoad({ filter: SOURCE_FILE }, async (args) => {
74
+ if (args.namespace !== "file") return undefined
75
+ const classification = classifier.classify(args.path)
76
+ let reason = browserDenial(classification)
77
+ if (
78
+ reason === undefined &&
79
+ options.onDenied === undefined &&
80
+ BROWSER_CODE.has(classification.zone)
81
+ ) {
82
+ const source = await Bun.file(args.path).text()
83
+ reason = privateEnvDenial(args.path, source, options.publicEnvPrefix ?? "PUBLIC_")
84
+ }
85
+ return reason === undefined ? undefined : refuse(args.path, reason)
86
+ })
87
+ build.onResolve({ filter: NON_SOURCE_PATH }, (args) => {
88
+ // An entry point (a dev server's HTML page) is the build's own choice, not an import.
89
+ if (args.importer === "") return undefined
90
+ const file = resolve(dirname(args.importer), args.path.replace(/[?#].*$/, ""))
91
+ const reason = browserDenial(classifier.classify(file))
92
+ return reason === undefined ? undefined : refuse(file, reason, args.importer)
93
+ })
94
+ // In record mode the finished graph reports these, with chains; only the dev server needs them here.
95
+ if (options.onDenied !== undefined) return
96
+ build.onResolve({ filter: /^(?:node|bun):/ }, (args) =>
97
+ refuse(
98
+ args.path,
99
+ "Node and Bun built-ins run on the server only; use them in a route's backend half or under backend/",
100
+ args.importer,
101
+ ),
102
+ )
103
+ build.onResolve(
104
+ { filter: new RegExp(`^${BACKEND_ONLY_MARKER.replaceAll("/", "\\/")}$`) },
105
+ (args) =>
106
+ refuse(args.importer, `it imports "${BACKEND_ONLY_MARKER}", which marks it backend-only`),
107
+ )
108
+ },
109
+ }
110
+ }
package/src/prerender.ts CHANGED
@@ -10,8 +10,9 @@
10
10
  */
11
11
  import { mkdirSync, writeFileSync } from "node:fs"
12
12
  import { dirname, resolve, sep } from "node:path"
13
+ import { assertNoSecrets, buildEnvironment, type SecretExemption } from "./internal/secret-scan.ts"
13
14
  import { fillRoutePattern, type RouteEntry } from "./manifest.ts"
14
- import { DATA_HEADER } from "./router.ts"
15
+ import { DATA_HEADER, PRERENDERED_GLOBAL, PRERENDERED_LIST_PATH } from "./router.ts"
15
16
 
16
17
  /** Minimal app surface the driver needs - just a fetch handler (a built `createWebApp`). */
17
18
  export interface PrerenderApp {
@@ -30,6 +31,15 @@ export interface PrerenderOptions {
30
31
  /** Origin for the synthetic request URL. Default `http://localhost`. Only the path is meaningful;
31
32
  * loaders see this as `request.url`, so set it if a loader builds absolute URLs from the origin. */
32
33
  readonly origin?: string
34
+ /** The secret scan every page and `_data.json` passes before it is written: what looks like a
35
+ * credential, or the value of a non-public build environment variable, fails the prerender. */
36
+ readonly secrets?: {
37
+ /** Default: the build environment (`Bun.env`, else `process.env`). */
38
+ readonly env?: Readonly<Record<string, string | undefined>>
39
+ /** Default `"PUBLIC_"`. */
40
+ readonly publicEnvPrefix?: string
41
+ readonly exemptions?: readonly SecretExemption[]
42
+ }
33
43
  }
34
44
 
35
45
  export interface PrerenderEntry {
@@ -66,6 +76,11 @@ export function htmlFileFor(pattern: string): string {
66
76
  return trimmed === "" ? "index.html" : `${trimmed}/index.html`
67
77
  }
68
78
 
79
+ // The handover of a page whose prerendered set is served as a list rather than inlined.
80
+ const PRERENDERED_LIST_URL = new RegExp(
81
+ `"${PRERENDERED_GLOBAL}":"(${PRERENDERED_LIST_PATH.replaceAll(".", "\\.")}\\?v=[0-9a-f]+)"`,
82
+ )
83
+
69
84
  /** The static loader-data file next to a route's `index.html`: `/` → `_data.json`, `/a/b` →
70
85
  * `a/b/_data.json`. The client fetches it on soft-nav into a prerendered route (no worker). */
71
86
  export function dataFileFor(pattern: string): string {
@@ -90,9 +105,19 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
90
105
  const skipped: { path: string; reason: string }[] = []
91
106
  const fallbacks: Record<string, "ssr" | "404"> = {}
92
107
  const outRoot = resolve(options.outDir)
108
+ let listUrl: string | undefined
109
+
110
+ const secretScan = {
111
+ env: options.secrets?.env ?? buildEnvironment(),
112
+ publicEnvPrefix: options.secrets?.publicEnvPrefix ?? "PUBLIC_",
113
+ ...(options.secrets?.exemptions ? { exemptions: options.secrets.exemptions } : {}),
114
+ }
115
+ const assertClean = (name: string, text: string): void =>
116
+ assertNoSecrets({ ...secretScan, artifacts: [{ name, text }] })
93
117
 
94
118
  // Render one concrete path → drain the streamed document to bytes → write its index.html. Shared by
95
- // static (one path) and dynamic (one path per getStaticPaths entry) routes; never throws.
119
+ // static (one path) and dynamic (one path per getStaticPaths entry) routes. A failed render is a skip;
120
+ // a credential in the output fails the whole prerender.
96
121
  const renderPath = async (path: string): Promise<void> => {
97
122
  const res = await options.app.fetch(new Request(`${origin}${path}`))
98
123
  if (!res.ok) {
@@ -101,12 +126,14 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
101
126
  return
102
127
  }
103
128
  const html = await res.text()
129
+ listUrl ??= PRERENDERED_LIST_URL.exec(html)?.[1]
104
130
  const file = htmlFileFor(path)
105
131
  const abs = outputPath(outRoot, file)
106
132
  if (abs === undefined) {
107
133
  skipped.push({ path, reason: "unsafe output path" })
108
134
  return
109
135
  }
136
+ assertClean(file, html)
110
137
  mkdirSync(dirname(abs), { recursive: true })
111
138
  writeFileSync(abs, html)
112
139
 
@@ -128,7 +155,9 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
128
155
  await dataRes.body?.cancel()
129
156
  dataFile = undefined
130
157
  } else {
131
- writeFileSync(dataAbs, await dataRes.text())
158
+ const data = await dataRes.text()
159
+ assertClean(dataFile, data)
160
+ writeFileSync(dataAbs, data)
132
161
  }
133
162
  } else {
134
163
  await dataRes.body?.cancel() // not emitting (non-OK or NDJSON) - release the stream
@@ -177,6 +206,23 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
177
206
  await renderPath(route.pattern)
178
207
  }
179
208
 
209
+ // A set too large to inline is handed over as the URL of a list the app serves; the static output
210
+ // has to carry the file those pages fetch.
211
+ if (listUrl !== undefined) {
212
+ const listRes = await options.app.fetch(new Request(`${origin}${listUrl}`))
213
+ const listFile = PRERENDERED_LIST_PATH.slice(1)
214
+ const listAbs = outputPath(outRoot, listFile)
215
+ if (listRes.ok && listAbs !== undefined) {
216
+ const text = await listRes.text()
217
+ assertClean(listFile, text)
218
+ mkdirSync(dirname(listAbs), { recursive: true })
219
+ writeFileSync(listAbs, text)
220
+ } else {
221
+ await listRes.body?.cancel()
222
+ skipped.push({ path: PRERENDERED_LIST_PATH, reason: `list returned HTTP ${listRes.status}` })
223
+ }
224
+ }
225
+
180
226
  return { prerendered, skipped, fallbacks }
181
227
  }
182
228