@nifrajs/web 3.5.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 (390) 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 +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  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 +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +111 -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/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +485 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +386 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +72 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +187 -36
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +111 -18
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +623 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +670 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +110 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +207 -36
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/web/plugins/vite-leak-guard` - nifra's two client-leak guards, for a Vite/Rollup production
2
+ * `@nifrajs/web/plugins/vite-leak-guard` - nifra's client-leak guards, for a Vite/Rollup production
3
3
  * build.
4
4
  *
5
5
  * ## Why this exists
@@ -12,9 +12,9 @@
12
12
  * pipeline arriving WITHOUT them, or with a hastily re-implemented "mostly ported" copy, is the failure the
13
13
  * neutral module-graph seam was built to prevent.
14
14
  *
15
- * So this is not a second implementation. The detection logic (`detectNodeBuiltinsInClient`,
16
- * `detectServerOnlyInClient`) and the failure MESSAGES (`formatNodeBuiltinLeak`, `formatServerOnlyLeak`)
17
- * are the exact same functions the Bun build calls. This plugin only adapts Rollup's bundle shape into the
15
+ * So this is not a second implementation. The zone verification (`verifyClientGraph`,
16
+ * `accountEmittedFiles`), the `node:` and `backend-only` detectors and their failure MESSAGES are the
17
+ * exact same functions the Bun build calls. This plugin only adapts Rollup's bundle shape into the
18
18
  * neutral `ClientModuleGraph` (`fromRollupBundle`) and runs them. A leak reads identically whichever
19
19
  * bundler produced it, and there is one place to change if a guard changes.
20
20
  *
@@ -25,19 +25,57 @@
25
25
  * `fromRollupBundle` needs. Findings fail the build through Rollup's `this.error`, so the message lands in
26
26
  * the build output the same way a Bun-build throw does.
27
27
  *
28
- * Add it to the `plugins` of a Vite production build (the LAST plugin, so it sees the final graph):
28
+ * Add it to the `plugins` of a Vite production build (the LAST plugin, so it sees the final graph),
29
+ * with `viteBareBuiltinExternal()` first so a bare built-in such as `fs/promises` stays named:
29
30
  *
30
31
  * // vite.config.ts (production client build)
31
- * import { viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
32
- * export default { build: { rollupOptions: { plugins: [viteLeakGuard()] } } }
32
+ * import { viteBareBuiltinExternal, viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
33
+ * export default {
34
+ * plugins: [viteBareBuiltinExternal(), viteAssetUrlGuard({ appRoot: import.meta.dirname })],
35
+ * build: {
36
+ * rollupOptions: { external: [/^node:/], plugins: [viteLeakGuard({ appRoot: import.meta.dirname })] },
37
+ * },
38
+ * }
33
39
  */
40
+ import { existsSync, statSync } from "node:fs"
41
+ import { dirname, isAbsolute, relative, resolve } from "node:path"
34
42
  import {
35
43
  detectNodeBuiltinsInClient,
36
44
  detectServerOnlyInClient,
37
45
  formatNodeBuiltinLeak,
38
46
  formatServerOnlyLeak,
39
- } from "../build.ts"
40
- import { fromRollupBundle, type RollupBundleLike } from "../module-graph.ts"
47
+ } from "../build-plan.ts"
48
+ import { isBareNodeBuiltin } from "../internal/node-builtins.ts"
49
+ import { privateEnvCheck } from "../internal/private-env.ts"
50
+ import {
51
+ emittedScanFiles,
52
+ formatSecretFindings,
53
+ graphScanInput,
54
+ originName,
55
+ publicScanFiles,
56
+ type SecretExemption,
57
+ type SecretScanFile,
58
+ scanForSecrets,
59
+ textOf,
60
+ } from "../internal/secret-scan.ts"
61
+ import {
62
+ accountEmittedFiles,
63
+ assetDenial,
64
+ type EmittedFile,
65
+ formatClientGraphVerdict,
66
+ formatServerGraphVerdict,
67
+ formatUnaccountedOutput,
68
+ rollupModuleSource,
69
+ verifyClientGraph,
70
+ verifyServerGraph,
71
+ } from "../internal/zone-graph.ts"
72
+ import {
73
+ type ClientModuleGraph,
74
+ fromRollupBundle,
75
+ type GraphModule,
76
+ type RollupBundleLike,
77
+ } from "../module-graph.ts"
78
+ import { createZoneClassifier, type ZoneClassifier } from "../zones.ts"
41
79
 
42
80
  /**
43
81
  * The Rollup plugin-context slice this uses: `getModuleInfo` for a module's resolved imports, and `error`
@@ -47,7 +85,10 @@ interface RollupPluginContext {
47
85
  getModuleInfo(id: string): {
48
86
  readonly importedIds?: readonly string[]
49
87
  readonly dynamicallyImportedIds?: readonly string[]
88
+ readonly isExternal?: boolean
50
89
  } | null
90
+ /** Every module the build loaded, including ones tree-shaking left out of every chunk. */
91
+ getModuleIds?(): Iterable<string>
51
92
  /**
52
93
  * Takes an `Error`, never a string.
53
94
  *
@@ -60,6 +101,50 @@ interface RollupPluginContext {
60
101
  error(error: Error): never
61
102
  }
62
103
 
104
+ /** The parts of a Rollup output file the accounting reads. */
105
+ interface RollupOutputLike {
106
+ readonly type?: string
107
+ readonly fileName?: string
108
+ readonly code?: string
109
+ readonly source?: string | Uint8Array
110
+ readonly map?: { readonly sources?: readonly (string | null)[] } | null
111
+ readonly originalFileNames?: readonly string[]
112
+ readonly viteMetadata?: {
113
+ readonly importedCss?: ReadonlySet<string>
114
+ readonly importedAssets?: ReadonlySet<string>
115
+ }
116
+ readonly moduleIds?: readonly string[]
117
+ }
118
+
119
+ export interface LeakGuardOptions {
120
+ /** The app root: the directory holding `routes/`, `frontend/`, `backend/` and `shared/` (default
121
+ * the working directory). */
122
+ readonly appRoot?: string
123
+ /** The routes directory (default `<appRoot>/routes`). */
124
+ readonly routesDir?: string
125
+ /** Vite's `root`, which emitted asset names are relative to (default `appRoot`). */
126
+ readonly root?: string
127
+ /** Where the bundle is written; source map paths resolve against it (default `<root>/dist`). */
128
+ readonly outDir?: string
129
+ /** The build's own generated entry modules. */
130
+ readonly generatedFiles?: readonly string[]
131
+ /** Output files another guarded build already verified (a worker sub-build's chunks). */
132
+ readonly verified?: Set<string>
133
+ /** The public-env prefix browser code may read (default `"PUBLIC_"`; `""` exposes nothing). */
134
+ readonly publicEnvPrefix?: string
135
+ /** The secret scan of what the bundle publishes. */
136
+ readonly secrets?: LeakGuardSecretOptions
137
+ }
138
+
139
+ export interface LeakGuardSecretOptions {
140
+ /** The build environment whose non-public values must not ship (default `process.env`). */
141
+ readonly env?: Readonly<Record<string, string | undefined>>
142
+ /** Reviewed false positives; each names its rule, its file (or env variable) and a reason. */
143
+ readonly exemptions?: readonly SecretExemption[]
144
+ /** A `public/` directory whose files ship beside the bundle, scanned with it. */
145
+ readonly publicDir?: string
146
+ }
147
+
63
148
  /** The minimal Rollup plugin shape this returns - `generateBundle` bound to the plugin context. */
64
149
  export interface LeakGuardPlugin {
65
150
  readonly name: string
@@ -78,14 +163,22 @@ export interface LeakGuardPlugin {
78
163
  generateBundle(this: RollupPluginContext, options: unknown, bundle: RollupBundleLike): void
79
164
  }
80
165
 
166
+ const CSS_FILE = /\.(?:css|scss|sass|less|styl)(?:$|\?)/
167
+
81
168
  /**
82
- * A Vite/Rollup plugin that fails the build when server-only code or a `node:` builtin reaches the client
83
- * bundle - the same two guards, and the same error messages, as nifra's Bun production build.
169
+ * A Vite/Rollup plugin that fails the build when anything the zones keep on the server reaches the client
170
+ * bundle, with the same checks and messages as nifra's Bun build: every module classified, the graph
171
+ * evidence complete, every emitted file traced back to it, and the `node:` and `backend-only` guards.
172
+ * It runs in `generateBundle`, before Rollup writes a byte.
84
173
  */
85
- export function viteLeakGuard(): LeakGuardPlugin {
174
+ export function viteLeakGuard(options: LeakGuardOptions = {}): LeakGuardPlugin {
175
+ const appRoot = resolve(options.appRoot ?? process.cwd())
176
+ const root = resolve(options.root ?? appRoot)
177
+ const outDir = resolve(root, options.outDir ?? "dist")
86
178
  const plugin: LeakGuardPlugin = {
87
179
  name: "nifra:leak-guard",
88
180
  generateBundle(_options, bundle) {
181
+ const classifier = classifierFor(appRoot, options)
89
182
  // Resolved import edges per module, straight from Rollup's graph. Dynamic imports count too: a
90
183
  // `node:`/server-only module reached only via `import()` still ships to the browser.
91
184
  const importsOf = (id: string): readonly string[] => {
@@ -93,12 +186,25 @@ export function viteLeakGuard(): LeakGuardPlugin {
93
186
  if (info === null) return []
94
187
  return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
95
188
  }
96
- const graph = fromRollupBundle(bundle, importsOf)
97
- // Node-builtin guard first, then server-only - the same order as the Bun build, so the first error a
98
- // dev sees is the same across pipelines when a module trips both.
189
+ const graph = completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf)
190
+ const sources = { classifier, sourceOf: rollupModuleSource }
191
+ const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_")
192
+ const outputs = Object.values(bundle as Readonly<Record<string, RollupOutputLike>>)
193
+ for (const output of outputs) {
194
+ if (output.type !== "asset" && output.fileName !== undefined)
195
+ options.verified?.add(output.fileName)
196
+ }
99
197
  const leak =
198
+ formatClientGraphVerdict(verifyClientGraph(graph, { ...sources, privateEnv })) ??
100
199
  formatNodeBuiltinLeak(detectNodeBuiltinsInClient(graph)) ??
101
- formatServerOnlyLeak(detectServerOnlyInClient(graph))
200
+ formatServerOnlyLeak(detectServerOnlyInClient(graph)) ??
201
+ formatUnaccountedOutput(
202
+ accountEmittedFiles(emittedFiles(outputs, graph, root, options.verified), graph, {
203
+ ...sources,
204
+ outDir,
205
+ }),
206
+ ) ??
207
+ secretLeak(outputs, graph, classifier, appRoot, options)
102
208
  if (leak === undefined) return
103
209
  // Record before throwing: the throw is what fails the build for a standalone user of this plugin,
104
210
  // and the record is what lets a caller that owns the build re-raise the real message (see `leak`).
@@ -108,3 +214,410 @@ export function viteLeakGuard(): LeakGuardPlugin {
108
214
  }
109
215
  return plugin
110
216
  }
217
+
218
+ /** The secret scan over a finished bundle and the `public/` files that ship beside it. */
219
+ function secretLeak(
220
+ outputs: readonly RollupOutputLike[],
221
+ graph: ClientModuleGraph,
222
+ classifier: ReturnType<typeof createZoneClassifier>,
223
+ appRoot: string,
224
+ options: LeakGuardOptions,
225
+ ): string | undefined {
226
+ const { sources, originsOf } = graphScanInput(graph, rollupModuleSource, classifier)
227
+ const artifacts: SecretScanFile[] = []
228
+ for (const output of outputs) {
229
+ const name = output.fileName
230
+ if (name === undefined || name.startsWith(".vite/")) continue
231
+ if (output.type === "chunk") {
232
+ artifacts.push(...emittedScanFiles(name, output.code ?? "", originsOf(name)))
233
+ if (output.map) artifacts.push({ name: `${name}.map`, text: JSON.stringify(output.map) })
234
+ continue
235
+ }
236
+ const text =
237
+ typeof output.source === "string"
238
+ ? output.source
239
+ : output.source === undefined
240
+ ? undefined
241
+ : textOf(output.source)
242
+ if (text !== undefined) artifacts.push({ name, text })
243
+ }
244
+ const publicDir = options.secrets?.publicDir
245
+ if (publicDir !== undefined && existsSync(publicDir)) {
246
+ artifacts.push(...publicScanFiles(publicDir, originName(appRoot, resolve(publicDir))))
247
+ }
248
+ return formatSecretFindings(
249
+ scanForSecrets({
250
+ sources,
251
+ artifacts,
252
+ env: options.secrets?.env ?? process.env,
253
+ publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
254
+ ...(options.secrets?.exemptions ? { exemptions: options.secrets.exemptions } : {}),
255
+ }),
256
+ )
257
+ }
258
+
259
+ /** What {@link viteServerZoneGuard} needs: the zones of the app, nothing about the output. */
260
+ export type ServerZoneGuardOptions = Pick<
261
+ LeakGuardOptions,
262
+ "appRoot" | "routesDir" | "generatedFiles" | "publicEnvPrefix"
263
+ >
264
+
265
+ /**
266
+ * The server build's half of the zone rules, with the same checks and message as nifra's Bun server
267
+ * build: every first-party module zoned, backend code never importing frontend code, shared code
268
+ * importing only shared code. It records the refusal in `leak` for the same reason the client guard
269
+ * does.
270
+ */
271
+ export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakGuardPlugin {
272
+ const appRoot = resolve(options.appRoot ?? process.cwd())
273
+ const plugin: LeakGuardPlugin = {
274
+ name: "nifra:server-zone-guard",
275
+ generateBundle(_options, bundle) {
276
+ const classifier = classifierFor(appRoot, options)
277
+ const importsOf = (id: string): readonly string[] => {
278
+ const info = this.getModuleInfo(id)
279
+ if (info === null) return []
280
+ return [...(info.importedIds ?? []), ...(info.dynamicallyImportedIds ?? [])]
281
+ }
282
+ const graph = completeGraph(fromRollupBundle(bundle, importsOf), this, importsOf)
283
+ const leak = formatServerGraphVerdict(
284
+ verifyServerGraph(graph, {
285
+ classifier,
286
+ sourceOf: rollupModuleSource,
287
+ privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
288
+ }),
289
+ )
290
+ if (leak === undefined) return
291
+ plugin.leak = leak
292
+ this.error(new Error(leak))
293
+ },
294
+ }
295
+ return plugin
296
+ }
297
+
298
+ /** The zone classifier for the app `options` names. */
299
+ function classifierFor(appRoot: string, options: ServerZoneGuardOptions): ZoneClassifier {
300
+ return createZoneClassifier({
301
+ appRoot,
302
+ ...(options.routesDir !== undefined ? { routesDir: options.routesDir } : {}),
303
+ ...(options.generatedFiles !== undefined ? { generatedFiles: options.generatedFiles } : {}),
304
+ })
305
+ }
306
+
307
+ /**
308
+ * The chunk graph plus every module the build loaded but tree-shook out of every chunk, with external
309
+ * edges marked: an import the bundle left external is evidence the graph has to account for.
310
+ */
311
+ function completeGraph(
312
+ graph: ClientModuleGraph,
313
+ context: RollupPluginContext,
314
+ importsOf: (id: string) => readonly string[],
315
+ ): ClientModuleGraph {
316
+ const external = (id: string): boolean => context.getModuleInfo(id)?.isExternal === true
317
+ const modules: Record<string, GraphModule> = {}
318
+ for (const [id, module] of Object.entries(graph.modules)) {
319
+ modules[id] = {
320
+ imports: module.imports.map((im) =>
321
+ im.path !== undefined && !im.path.startsWith("node:") && external(im.path)
322
+ ? { ...im, external: true }
323
+ : im,
324
+ ),
325
+ }
326
+ }
327
+ for (const id of context.getModuleIds?.() ?? []) {
328
+ if (modules[id] !== undefined || external(id)) continue
329
+ modules[id] = {
330
+ imports: importsOf(id).map((path) => (external(path) ? { path, external: true } : { path })),
331
+ }
332
+ }
333
+ return { modules, chunks: graph.chunks }
334
+ }
335
+
336
+ /** Every file in the bundle, with what it claims to come from. */
337
+ function emittedFiles(
338
+ outputs: readonly RollupOutputLike[],
339
+ graph: ClientModuleGraph,
340
+ root: string,
341
+ verified: ReadonlySet<string> | undefined,
342
+ ): EmittedFile[] {
343
+ const fromRoot = (name: string): string => (isAbsolute(name) ? name : resolve(root, name))
344
+ const moduleFiles = (ids: readonly string[]): string[] =>
345
+ ids.flatMap((id) => {
346
+ const source = rollupModuleSource(id)
347
+ return source.kind === "builtin" || source.file === undefined ? [] : [source.file]
348
+ })
349
+ const referencedBy = new Map<string, string[]>()
350
+ for (const output of outputs) {
351
+ if (output.type !== "chunk") continue
352
+ for (const name of [
353
+ ...(output.viteMetadata?.importedCss ?? []),
354
+ ...(output.viteMetadata?.importedAssets ?? []),
355
+ ]) {
356
+ referencedBy.set(name, [
357
+ ...(referencedBy.get(name) ?? []),
358
+ ...moduleFiles(output.moduleIds ?? []),
359
+ ])
360
+ }
361
+ }
362
+ const cssModules = moduleFiles(Object.keys(graph.modules).filter((id) => CSS_FILE.test(id)))
363
+ const files: EmittedFile[] = []
364
+ for (const output of outputs) {
365
+ const name = output.fileName
366
+ // Vite's own bookkeeping (`.vite/manifest.json`); nifra deletes it before anything is deployed.
367
+ if (name === undefined || name.startsWith(".vite/")) continue
368
+ if (output.type === "chunk") {
369
+ files.push({ name, kind: "code", text: output.code ?? "" })
370
+ if (output.map)
371
+ files.push({ name: `${name}.map`, kind: "map", text: JSON.stringify(output.map) })
372
+ continue
373
+ }
374
+ const text = typeof output.source === "string" ? output.source : undefined
375
+ if (name.endsWith(".map")) {
376
+ files.push({ name, kind: "map", ...(text !== undefined ? { text } : {}) })
377
+ continue
378
+ }
379
+ if (verified?.has(name)) continue
380
+ // Vite names a synthesized asset (the `cssCodeSplit: false` aggregate is `style.css`) after no
381
+ // real file; only names that exist are evidence.
382
+ const original = (output.originalFileNames ?? [])
383
+ .map(fromRoot)
384
+ .filter((file) => existsSync(file))
385
+ const referenced = referencedBy.get(name) ?? []
386
+ const sources =
387
+ original.length > 0
388
+ ? original
389
+ : referenced.length > 0
390
+ ? referenced
391
+ : name.endsWith(".css")
392
+ ? cssModules
393
+ : []
394
+ files.push({ name, kind: "asset", sources })
395
+ }
396
+ return files
397
+ }
398
+
399
+ /** The resolve-hook context slice {@link viteBareBuiltinExternal} uses. */
400
+ interface ResolveContext {
401
+ resolve(
402
+ source: string,
403
+ importer: string | undefined,
404
+ options: { readonly skipSelf: boolean },
405
+ ): Promise<{ readonly id: string } | null>
406
+ }
407
+
408
+ /** The minimal Vite plugin shape {@link viteBareBuiltinExternal} returns. */
409
+ export interface BareBuiltinPlugin {
410
+ readonly name: string
411
+ readonly enforce: "pre"
412
+ resolveId(
413
+ this: ResolveContext,
414
+ source: string,
415
+ importer: string | undefined,
416
+ ): Promise<{ readonly id: string; readonly external?: boolean } | null>
417
+ }
418
+
419
+ /**
420
+ * Keep a bare Node built-in (`fs/promises`, `path`) visible to {@link viteLeakGuard}. Vite resolves a
421
+ * bare built-in that is not an installed package to one shared `__vite-browser-external` stub: the
422
+ * import builds, does nothing in the browser, and no longer names the module. This plugin externalizes
423
+ * it as `node:<name>` instead, so the guard fails the build naming it, as the Bun build does. A package
424
+ * of the same name the app installed (`events`, `buffer`) still resolves to that package.
425
+ */
426
+ export function viteBareBuiltinExternal(): BareBuiltinPlugin {
427
+ return {
428
+ name: "nifra:bare-builtin-external",
429
+ enforce: "pre",
430
+ async resolveId(source, importer) {
431
+ if (!isBareNodeBuiltin(source)) return null
432
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
433
+ if (resolved !== null && !resolved.id.startsWith("__vite-browser-external")) return resolved
434
+ return { id: `node:${source}`, external: true }
435
+ },
436
+ }
437
+ }
438
+
439
+ /** The plugin-context slice {@link viteAssetUrlGuard} uses. */
440
+ interface TransformContext extends ResolveContext {
441
+ parse(code: string): unknown
442
+ error(error: Error): never
443
+ }
444
+
445
+ type InlineLimit = number | ((file: string, content: Uint8Array) => boolean | undefined)
446
+
447
+ /** The minimal Vite plugin shape {@link viteAssetUrlGuard} returns. */
448
+ export interface AssetUrlGuardPlugin {
449
+ readonly name: string
450
+ readonly apply: "build"
451
+ /** The refusal that failed the build, recorded for the reason {@link LeakGuardPlugin.leak} gives. */
452
+ leak?: string
453
+ config(config: { readonly build?: { readonly assetsInlineLimit?: InlineLimit } }): {
454
+ readonly build: { readonly assetsInlineLimit: InlineLimit }
455
+ }
456
+ transform(
457
+ this: TransformContext,
458
+ code: string,
459
+ id: string,
460
+ options?: { readonly ssr?: boolean },
461
+ ): Promise<null>
462
+ }
463
+
464
+ const POSTFIX = /[?#].*$/s
465
+ const URL_SCHEME = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i
466
+
467
+ /**
468
+ * Keep server files out of a Vite client build's assets. A `new URL("./x", import.meta.url)` makes
469
+ * Vite copy the file it names into the output, or inline it into the chunk as a `data:` URL when it is
470
+ * small - no module and no import edge, so {@link viteLeakGuard}'s graph never sees it. This refuses a
471
+ * reference whose file the zones keep off the browser (backend code, a route's backend half, a server
472
+ * function's source, a file in no zone), and stops Vite inlining such a file anywhere else (a
473
+ * stylesheet's `url()`), so it is emitted where the leak guard's output accounting names it.
474
+ *
475
+ * Add it to the top-level `plugins`, after any framework plugin: it reads each module once that
476
+ * module is JavaScript, before Vite resolves the references.
477
+ */
478
+ export function viteAssetUrlGuard(options: ServerZoneGuardOptions = {}): AssetUrlGuardPlugin {
479
+ const appRoot = resolve(options.appRoot ?? process.cwd())
480
+ let classifier: ZoneClassifier | undefined
481
+ const denial = (file: string): string | undefined => {
482
+ classifier ??= classifierFor(appRoot, options)
483
+ return assetDenial(classifier.classify(file))
484
+ }
485
+ const shown = (file: string): string => relative(appRoot, file).replaceAll("\\", "/")
486
+ const plugin: AssetUrlGuardPlugin = {
487
+ name: "nifra:asset-url-guard",
488
+ apply: "build",
489
+ config(config) {
490
+ const own = config.build?.assetsInlineLimit
491
+ return {
492
+ build: {
493
+ assetsInlineLimit: (file, content) =>
494
+ denial(file) !== undefined
495
+ ? false
496
+ : typeof own === "function"
497
+ ? own(file, content)
498
+ : own === undefined
499
+ ? undefined
500
+ : content.length < own,
501
+ },
502
+ }
503
+ },
504
+ async transform(code, id, transformOptions) {
505
+ if (transformOptions?.ssr === true || !code.includes("import.meta.url")) return null
506
+ const importer = id.replace(POSTFIX, "")
507
+ if (!isAbsolute(importer) || importer.includes("/node_modules/") || CSS_FILE.test(importer))
508
+ return null
509
+ let program: unknown
510
+ try {
511
+ program = this.parse(code)
512
+ } catch {
513
+ return null
514
+ }
515
+ const problems: string[] = []
516
+ for (const url of newUrlLiterals(program)) {
517
+ if (URL_SCHEME.test(url)) continue
518
+ for (const file of await assetUrlFiles(this, url, importer)) {
519
+ const reason = denial(file)
520
+ if (reason !== undefined) {
521
+ problems.push(
522
+ `${shown(importer)}: new URL(${JSON.stringify(url)}, import.meta.url) names ${shown(file)}: ${reason}`,
523
+ )
524
+ }
525
+ }
526
+ }
527
+ if (problems.length === 0) return null
528
+ const leak =
529
+ `[nifra/web] the browser build references files that may not ship to a browser:\n${problems.map((line) => ` - ${line}`).join("\n")}\n` +
530
+ "The bundler copies or inlines the file such a URL names, so it would ship as it is. Reach " +
531
+ "backend code through a route's backend half (x.backend.ts) or a *.fn.ts server function; a " +
532
+ "file the browser loads belongs in frontend/ or public/."
533
+ plugin.leak = leak
534
+ return this.error(new Error(leak))
535
+ },
536
+ }
537
+ return plugin
538
+ }
539
+
540
+ /**
541
+ * The files a `new URL(url, import.meta.url)` can name, resolved the ways Vite tries: beside the
542
+ * module, then through the resolver (aliases, extensions, a root-relative path). Existing files only;
543
+ * a reference to nothing ships nothing.
544
+ */
545
+ async function assetUrlFiles(
546
+ context: ResolveContext,
547
+ url: string,
548
+ importer: string,
549
+ ): Promise<string[]> {
550
+ const files = new Set<string>()
551
+ const add = (path: string | undefined): void => {
552
+ const file = path?.replace(POSTFIX, "")
553
+ if (file === undefined || !isAbsolute(file) || file.includes("\0")) return
554
+ if (statSync(file, { throwIfNoEntry: false })?.isFile() === true) files.add(file)
555
+ }
556
+ if (!url.startsWith("/")) add(resolve(dirname(importer), url.replace(POSTFIX, "")))
557
+ const sources = url.startsWith(".") || url.startsWith("/") ? [url] : [url, `./${url}`]
558
+ for (const source of sources) {
559
+ try {
560
+ add((await context.resolve(source, importer, { skipSelf: true }))?.id)
561
+ } catch {
562
+ // An unresolvable reference is one Vite leaves unchanged.
563
+ }
564
+ }
565
+ return [...files]
566
+ }
567
+
568
+ const field = (node: unknown, key: string): unknown =>
569
+ typeof node === "object" && node !== null ? Reflect.get(node, key) : undefined
570
+
571
+ /** `import.meta.url`, and not `new.target.url`. */
572
+ const isImportMetaUrl = (node: unknown): boolean =>
573
+ field(node, "type") === "MemberExpression" &&
574
+ field(node, "computed") === false &&
575
+ field(field(node, "property"), "name") === "url" &&
576
+ field(field(field(node, "object"), "meta"), "name") === "import"
577
+
578
+ /** A string literal's text as written and as it evaluates; nothing for a template with holes. */
579
+ function literalTexts(node: unknown): string[] {
580
+ const type = field(node, "type")
581
+ if (type === "Literal") {
582
+ const value = field(node, "value")
583
+ const raw = field(node, "raw")
584
+ if (typeof value !== "string") return []
585
+ return typeof raw === "string" ? [value, raw.slice(1, -1)] : [value]
586
+ }
587
+ if (type !== "TemplateLiteral") return []
588
+ const holes = field(node, "expressions")
589
+ const quasis = field(node, "quasis")
590
+ if (!Array.isArray(holes) || holes.length > 0 || !Array.isArray(quasis)) return []
591
+ const value = field(quasis[0], "value")
592
+ return [field(value, "cooked"), field(value, "raw")].filter(
593
+ (text): text is string => typeof text === "string",
594
+ )
595
+ }
596
+
597
+ /**
598
+ * The URL text of every `new URL("...", import.meta.url)` in a parsed module. Parsed rather than
599
+ * matched, so a comment inside the call cannot hide one; a template with holes is skipped, as Vite
600
+ * turns it into an `import.meta.glob` the module graph sees.
601
+ */
602
+ function newUrlLiterals(program: unknown): string[] {
603
+ const found = new Set<string>()
604
+ const stack: unknown[] = [program]
605
+ while (stack.length > 0) {
606
+ const node = stack.pop()
607
+ if (typeof node !== "object" || node === null) continue
608
+ const args = field(node, "arguments")
609
+ if (
610
+ field(node, "type") === "NewExpression" &&
611
+ field(field(node, "callee"), "type") === "Identifier" &&
612
+ field(field(node, "callee"), "name") === "URL" &&
613
+ Array.isArray(args) &&
614
+ isImportMetaUrl(args[1])
615
+ ) {
616
+ for (const text of literalTexts(args[0])) found.add(text)
617
+ }
618
+ for (const child of Object.values(node)) {
619
+ if (typeof child === "object" && child !== null) stack.push(child)
620
+ }
621
+ }
622
+ return [...found]
623
+ }