@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
@@ -23,6 +23,11 @@ export interface GraphImport {
23
23
  readonly path?: string
24
24
  /** The specifier as written in source - the only form that survives an unresolved import. */
25
25
  readonly original?: string
26
+ /** The bundler left this import in the emitted code instead of bundling its target. Bun does this
27
+ * for a dynamic `import("node:fs")` in a browser build: the import ships, the module does not. */
28
+ readonly external?: boolean
29
+ /** A dynamic `import()`. A bundler can drop one in dead code while the module around it ships. */
30
+ readonly dynamic?: boolean
26
31
  }
27
32
 
28
33
  export interface GraphModule {
@@ -34,6 +39,13 @@ export interface GraphChunk {
34
39
  readonly entryPoint?: string
35
40
  /** Module ids that landed in this chunk. */
36
41
  readonly modules: readonly string[]
42
+ /** The names this chunk exports, when the bundler reports them (entry chunks). */
43
+ readonly exports?: readonly string[]
44
+ /**
45
+ * What the emitted code itself imports: another output by its key in `chunks`, anything else as
46
+ * written. Module-level edges cannot say this - a bundler drops some and keeps others.
47
+ */
48
+ readonly imports?: readonly string[]
37
49
  }
38
50
 
39
51
  /** What a client build looks like to the guards, whichever bundler produced it. */
@@ -52,12 +64,26 @@ const normalizeModuleId = (id: string): string => id.replaceAll("\\", "/")
52
64
  /** The slice of Bun's metafile this seam consumes. Not yet in `@types/bun`; shape per the docs. */
53
65
  export interface BunMetafileLike {
54
66
  readonly inputs?: Readonly<
55
- Record<string, { readonly imports?: ReadonlyArray<{ path?: string; original?: string }> }>
67
+ Record<
68
+ string,
69
+ {
70
+ readonly imports?: ReadonlyArray<{
71
+ path?: string
72
+ original?: string
73
+ external?: boolean
74
+ kind?: string
75
+ }>
76
+ }
77
+ >
56
78
  >
57
79
  readonly outputs?: Readonly<
58
80
  Record<
59
81
  string,
60
- { readonly entryPoint?: string; readonly inputs?: Readonly<Record<string, unknown>> }
82
+ {
83
+ readonly entryPoint?: string
84
+ readonly inputs?: Readonly<Record<string, unknown>>
85
+ readonly exports?: readonly string[]
86
+ }
61
87
  >
62
88
  >
63
89
  }
@@ -65,29 +91,37 @@ export interface BunMetafileLike {
65
91
  /**
66
92
  * Adapt a `Bun.build` metafile to the neutral graph.
67
93
  *
68
- * A total function: an absent or partial metafile yields an empty graph rather than throwing, because
69
- * a guard that crashes on an unexpected build shape fails the build for the wrong reason. An empty
70
- * graph reports no findings, which matches the existing behaviour when the metafile is missing.
94
+ * Fails closed: the graph is the evidence that nothing backend reached the browser, so a build that
95
+ * produced no metafile, or one without inputs or outputs, is an error rather than an empty graph that
96
+ * would report nothing.
71
97
  */
72
98
  export function fromBunMetafile(meta: BunMetafileLike | undefined): ClientModuleGraph {
99
+ if (meta?.inputs === undefined || meta.outputs === undefined) {
100
+ throw new Error(
101
+ "[nifra/web] the client build produced no module graph (metafile), so nifra cannot prove what reached the browser. Refusing to write the bundle",
102
+ )
103
+ }
73
104
  const modules: Record<string, GraphModule> = {}
74
- for (const [rawId, input] of Object.entries(meta?.inputs ?? {})) {
105
+ for (const [rawId, input] of Object.entries(meta.inputs)) {
75
106
  const id = normalizeModuleId(rawId)
76
107
  modules[id] = {
77
108
  imports: (input.imports ?? []).map((im) => ({
78
109
  ...(im.path === undefined ? {} : { path: normalizeModuleId(im.path) }),
79
110
  ...(im.original === undefined ? {} : { original: im.original }),
111
+ ...(im.external === true ? { external: true } : {}),
112
+ ...(im.kind === "dynamic-import" ? { dynamic: true } : {}),
80
113
  })),
81
114
  }
82
115
  }
83
116
  const chunks: Record<string, GraphChunk> = {}
84
- for (const [rawPath, output] of Object.entries(meta?.outputs ?? {})) {
117
+ for (const [rawPath, output] of Object.entries(meta.outputs)) {
85
118
  const path = normalizeModuleId(rawPath)
86
119
  chunks[path] = {
87
120
  ...(output.entryPoint !== undefined
88
121
  ? { entryPoint: normalizeModuleId(output.entryPoint) }
89
122
  : {}),
90
123
  modules: Object.keys(output.inputs ?? {}).map(normalizeModuleId),
124
+ ...(output.exports !== undefined ? { exports: output.exports } : {}),
91
125
  }
92
126
  }
93
127
  return { modules, chunks }
@@ -105,6 +139,12 @@ export interface RollupChunkLike {
105
139
  readonly facadeModuleId?: string | null
106
140
  /** Every module id that landed in this chunk. Rollup's `moduleIds`. */
107
141
  readonly moduleIds?: readonly string[]
142
+ /** The names this chunk exports. Rollup's `exports`. */
143
+ readonly exports?: readonly string[]
144
+ /** Outputs and externals this chunk imports statically / dynamically. Rollup's `imports` and
145
+ * `dynamicImports`. */
146
+ readonly imports?: readonly string[]
147
+ readonly dynamicImports?: readonly string[]
108
148
  }
109
149
  export type RollupBundleLike = Readonly<Record<string, RollupChunkLike>>
110
150
 
@@ -118,8 +158,8 @@ export type RollupBundleLike = Readonly<Record<string, RollupChunkLike>>
118
158
  * guards read the resolved `path` as their fallback (a `node:` prefix, the `server-only` basename), so
119
159
  * detection is unaffected; only the human-readable chain shows resolved paths instead of as-written ones.
120
160
  *
121
- * Total, like {@link fromBunMetafile}: an empty bundle yields an empty graph (no findings), never a throw
122
- * that would fail a build for the wrong reason.
161
+ * An empty bundle yields an empty graph; the Rollup output always carries its own graph, so there is
162
+ * no missing-evidence case to fail on here.
123
163
  */
124
164
  export function fromRollupBundle(
125
165
  bundle: RollupBundleLike,
@@ -154,6 +194,10 @@ export function fromRollupBundle(
154
194
  // how a non-entry Bun output omits `entryPoint`, so the guards' entry set stays the real entries.
155
195
  ...(output.facadeModuleId ? { entryPoint: normalizeModuleId(output.facadeModuleId) } : {}),
156
196
  modules: chunkModules,
197
+ ...(output.exports !== undefined ? { exports: output.exports } : {}),
198
+ ...(output.imports !== undefined
199
+ ? { imports: [...output.imports, ...(output.dynamicImports ?? [])] }
200
+ : {}),
157
201
  }
158
202
  }
159
203
  return { modules, chunks }
package/src/navigation.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * native `<a href>` full-page navigation - progressive enhancement, no throw.
13
13
  */
14
14
 
15
+ import type { ClientRouter } from "./router.ts"
15
16
  import { serializeSearch } from "./search.ts"
16
17
 
17
18
  /** Options for a programmatic navigation. */
@@ -102,6 +103,18 @@ export function resolveNavigate(
102
103
  return { to: to.to + query, options: resolved }
103
104
  }
104
105
 
106
+ /**
107
+ * When a link warms its route's chunk and loader data ahead of a click. Set it with the
108
+ * `data-nifra-prefetch` attribute on the link or on any ancestor (the nearest wins):
109
+ * - `intent` (the default) - on hover or keyboard focus.
110
+ * - `viewport` - once the link scrolls into view.
111
+ * - `render` - as soon as a page shows the link.
112
+ * - `none` - never; the route loads when the link is followed.
113
+ *
114
+ * A warmed route is used by a click within 30 seconds; after that the click loads it again.
115
+ */
116
+ export type PrefetchMode = "intent" | "viewport" | "render" | "none"
117
+
105
118
  /**
106
119
  * A history-aware navigate. A **string** `to` is a same-origin path (`/users/7?tab=a`) navigated to
107
120
  * (push, or replace with `{ replace: true }`); a **number** is a history delta (`-1` back, `1`
@@ -109,14 +122,24 @@ export function resolveNavigate(
109
122
  */
110
123
  export type BrowserNavigate = (to: string | number, options?: NavigateOptions) => void
111
124
 
112
- // The active browser navigate (set by `installHistory`, cleared on teardown). Module-scoped: one app
113
- // per page; absent on the server and before hydration.
125
+ // The active browser navigate and the router behind it (set by `installHistory`, cleared on
126
+ // teardown). Module-scoped: one app per page; absent on the server and before hydration.
114
127
  let browserNavigate: BrowserNavigate | undefined
115
-
116
- /** Register (or clear, with `undefined`) the browser navigate - called by `installHistory`. Not for
117
- * app use. */
118
- export function setBrowserNavigate(navigate: BrowserNavigate | undefined): void {
128
+ let browserRouter: ClientRouter | undefined
129
+
130
+ /** Register (or clear, with `undefined`) the browser navigate and its router - called by
131
+ * `installHistory`. Not for app use. */
132
+ export function setBrowserNavigate(
133
+ navigate: BrowserNavigate | undefined,
134
+ router?: ClientRouter,
135
+ ): void {
119
136
  browserNavigate = navigate
137
+ browserRouter = router
138
+ }
139
+
140
+ /** The mounted client router, or `undefined` on the server / before `installHistory` has run. */
141
+ export function getBrowserRouter(): ClientRouter | undefined {
142
+ return browserRouter
120
143
  }
121
144
 
122
145
  /** The active browser navigate, or `undefined` on the server / before `installHistory` has run. A
package/src/nonce.ts ADDED
@@ -0,0 +1,104 @@
1
+ /** Context supplied while resolving a document's CSP nonce. */
2
+ export interface NonceContext<Env = unknown> {
3
+ readonly request: Request
4
+ readonly env: Env
5
+ }
6
+
7
+ /** Generate a nonce for one request, or return `undefined` to keep the document nonce-free. */
8
+ export type NonceGenerator<Env = unknown> = (
9
+ context: NonceContext<Env>,
10
+ ) => string | undefined | Promise<string | undefined>
11
+
12
+ /** Context supplied to the optional Content-Security-Policy header callback. */
13
+ export interface NonceHeaderContext {
14
+ readonly nonce: string
15
+ readonly request: Request
16
+ readonly response: Response
17
+ }
18
+
19
+ /** Return the complete Content-Security-Policy value for a nonce-bearing document. */
20
+ export type NonceHeader = (context: NonceHeaderContext) => string | undefined
21
+
22
+ /**
23
+ * A request-aware nonce resolver. It is callable so existing `createWebApp({ nonce })` code can pass
24
+ * it directly. When created with a `header` callback, `createWebApp` also installs its response hook
25
+ * automatically and applies that callback's CSP value to the same request that received the nonce.
26
+ */
27
+ export interface NonceResolver<Env = unknown> extends NonceGenerator<Env> {
28
+ /** Read the currently resolved nonce for a request, if one has been generated. */
29
+ readonly get?: (request: Request) => string | undefined
30
+ /** Internal integration seam used by `createWebApp` when a header callback was supplied. */
31
+ readonly onResponse?: (response: Response, request: Request) => Response
32
+ }
33
+
34
+ export interface CreateNonceResolverOptions<Env = unknown> {
35
+ /** Custom nonce generator. The default is a fresh CSP base64-compatible nonce per request. */
36
+ readonly generate?: NonceGenerator<Env>
37
+ /** Build the complete CSP value. App-specific sources remain the caller's responsibility. */
38
+ readonly header?: NonceHeader
39
+ }
40
+
41
+ /**
42
+ * Create a request-memoized CSP nonce resolver for `createWebApp`.
43
+ *
44
+ * ```ts
45
+ * const nonce = createNonceResolver({
46
+ * header: ({ nonce }) => `default-src 'self'; script-src 'self' 'nonce-${nonce}'`,
47
+ * })
48
+ * const app = createWebApp({ adapter, manifest, clientEntry, nonce })
49
+ * ```
50
+ *
51
+ * A nonce is generated only when the page executor asks for one. Data-mode/API responses never ask,
52
+ * so the response hook has no value to attach and leaves those responses without a CSP nonce header.
53
+ */
54
+ export function createNonceResolver<Env = unknown>(
55
+ options: CreateNonceResolverOptions<Env> = {},
56
+ ): NonceResolver<Env> {
57
+ const values = new WeakMap<Request, string>()
58
+ const pending = new WeakMap<Request, Promise<string | undefined>>()
59
+ // CSP nonce-source values use the base64-value grammar. A UUID contains hyphens, so remove them
60
+ // before emitting it; the remaining 122 random bits are sufficient for a per-response nonce and
61
+ // stay valid across browsers and all fetch runtimes without a Node-only encoder.
62
+ const generate = options.generate ?? (() => crypto.randomUUID().replaceAll("-", ""))
63
+
64
+ const resolve = (context: NonceContext<Env>): string | Promise<string | undefined> => {
65
+ const existing = values.get(context.request)
66
+ if (existing !== undefined) return existing
67
+ const ongoing = pending.get(context.request)
68
+ if (ongoing !== undefined) return ongoing
69
+ const generated = (async (): Promise<string | undefined> => {
70
+ const value = await generate(context)
71
+ if (value === undefined) return undefined
72
+ if (value.trim() === "") {
73
+ throw new TypeError("[nifra/web] createNonceResolver generated an empty CSP nonce")
74
+ }
75
+ values.set(context.request, value)
76
+ return value
77
+ })()
78
+ pending.set(context.request, generated)
79
+ generated.then(
80
+ () => pending.delete(context.request),
81
+ () => pending.delete(context.request),
82
+ )
83
+ return generated
84
+ }
85
+
86
+ const get = (request: Request): string | undefined => values.get(request)
87
+ if (options.header === undefined) return Object.assign(resolve, { get })
88
+
89
+ const onResponse = (response: Response, request: Request): Response => {
90
+ const nonce = values.get(request)
91
+ values.delete(request)
92
+ if (nonce === undefined) return response
93
+ const value = options.header?.({ nonce, request, response })
94
+ if (value === undefined) return response
95
+ const headers = new Headers(response.headers)
96
+ headers.set("content-security-policy", value)
97
+ return new Response(response.body, {
98
+ status: response.status,
99
+ statusText: response.statusText,
100
+ headers,
101
+ })
102
+ }
103
+ return Object.assign(resolve, { get, onResponse })
104
+ }
@@ -51,9 +51,11 @@ function scopedKeyframeName(filePath: string, name: string): string {
51
51
 
52
52
  /** Matches `@keyframes name` / `@-webkit-keyframes name` (etc.); captures the leading keyword and name. */
53
53
  const KEYFRAMES_PRELUDE = /(@(?:-\w+-)?keyframes\s+)([A-Za-z_][\w-]*)/i
54
- /** An `animation` / `animation-name` declaration: capture the `prop:` head + the value (which holds the
55
- * keyframe-name token(s) to remap). */
56
- const ANIMATION_DECL = /^(\s*animation(?:-name)?\s*:\s*)([\s\S]*)$/i
54
+ /** An `animation` / `animation-name` declaration, vendor-prefixed or after a comment: capture the
55
+ * `prop:` head + the value (which holds the keyframe-name token(s) to remap). A comment cannot hold its
56
+ * own closing delimiter, so each one matches one way - no backtracking across them. */
57
+ const ANIMATION_DECL =
58
+ /^((?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:-[a-z]+-)?animation(?:-name)?\s*:\s*)([\s\S]*)$/i
57
59
  const isKeyframesAt = (at: string): boolean => at === "keyframes" || at.endsWith("-keyframes")
58
60
 
59
61
  /** `s[i]` is a quote: return the index just past the matching close quote (honoring `\` escapes). */
@@ -8,6 +8,7 @@ import { existsSync } 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"
11
+ import { type RawSourceMap, ssrSourceMaps } from "../internal/source-map.ts"
11
12
 
12
13
  /** The argument Bun passes to a plugin's `setup` - Bun doesn't export the type, so derive it. */
13
14
  export type PluginBuilder = Parameters<BunPlugin["setup"]>[0]
@@ -123,6 +124,29 @@ export function devServerCompile(): boolean {
123
124
  return process.env[DEV_HMR_ENV] === "1"
124
125
  }
125
126
 
127
+ export { concatSourceMaps, type RawSourceMap } from "../internal/source-map.ts"
128
+
129
+ /**
130
+ * A compiled module carrying its map back to the file it came from, during a dev server only. The client
131
+ * half gets it inline: Bun's bundler keeps a plugin's output as the bundle map's source, and the dev
132
+ * server's frame mapper follows the inline map from there. The SSR half goes in the process registry
133
+ * instead, because Bun's runtime ignores a plugin's inline map when it reports a stack.
134
+ */
135
+ export function withDevSourceMap(
136
+ code: string,
137
+ map: RawSourceMap | undefined,
138
+ path: string,
139
+ generate: "dom" | "ssr",
140
+ ): string {
141
+ if (map === undefined || !devServerCompile()) return code
142
+ if (generate === "ssr") {
143
+ ssrSourceMaps().set(portablePath(path), map)
144
+ return code
145
+ }
146
+ const encoded = Buffer.from(JSON.stringify(map)).toString("base64")
147
+ return `${code}\n//# sourceMappingURL=data:application/json;base64,${encoded}\n`
148
+ }
149
+
126
150
  /**
127
151
  * The app root and the routes directory of the running dev server, for plugins that need to tell one of
128
152
  * the app's own components from a dependency or a route module. Set alongside {@link DEV_HMR_ENV}; read
@@ -11,9 +11,10 @@
11
11
  * is untouched - only the `?component` marker is intercepted.
12
12
  *
13
13
  * Optimization is optional: pass `svgo` (or install the `svgo` peer) to minify/clean the SVG first.
14
- * The transform is regex-based and targets the common icon-set shape (well-formed, self-closed tags);
15
- * it camelCases hyphenated + `xlink:`/`xmlns:` attributes, maps `class` → `className`, parses an inline
16
- * `style="..."` into an object, and drops XML declarations/comments so the result is valid JSX.
14
+ * The file must be one well-formed `<svg>` element; anything else is refused rather than guessed at.
15
+ * Its text reaches JSX as text - a brace in a `<style>` rule or a `<title>` is a character, never an
16
+ * expression. The transform camelCases hyphenated + `xlink:`/`xmlns:` attributes, maps `class` →
17
+ * `className`, parses an inline `style="..."` into an object, and drops XML declarations/comments.
17
18
  */
18
19
  import type { BunPlugin } from "bun"
19
20
  import { normalizeFilePath, requirePeer } from "./kit.ts"
@@ -113,10 +114,140 @@ export function stripSvgPreamble(xml: string): string {
113
114
  return parts.join("").trim()
114
115
  }
115
116
 
117
+ /** How a framework's template must spell an SVG file's text and names, for {@link svgTemplateMarkup}. */
118
+ export interface SvgMarkupRules {
119
+ /** Text content as the template must spell it; `cdata` text arrives as a CDATA section held it. */
120
+ readonly text: (text: string, cdata: boolean) => string
121
+ /** An attribute value (without its quotes) as the template must spell it. Default as written. */
122
+ readonly attributeValue?: (value: string) => string
123
+ /** Why the template would read an element or attribute name as more than markup, or `undefined`. */
124
+ readonly refuse?: (name: string, kind: "element" | "attribute") => string | undefined
125
+ /** Elements whose content the template reads as raw text, copied as it is. */
126
+ readonly rawText?: ReadonlySet<string>
127
+ }
128
+
129
+ const svgMarkupError = (detail: string): Error =>
130
+ new Error(`[nifra/web] an SVG component must be plain SVG markup: ${detail}`)
131
+
132
+ const TAG_NAME = /[A-Za-z_:][\w:.-]*/y
133
+ const SPACE = /\s*/y
134
+
135
+ /**
136
+ * An SVG file's markup rewritten for a framework template: one `<svg>` root, its text and attribute
137
+ * values put through `rules` so none of it reads as template syntax. Markup a well-formed SVG cannot
138
+ * hold is refused rather than passed on - a brace, a bare name or an unquoted value in a tag, content
139
+ * after the root - since each is a way for the file to reach the template as code.
140
+ */
141
+ export function svgTemplateMarkup(xml: string, rules: SvgMarkupRules): string {
142
+ const svg = stripSvgPreamble(xml)
143
+ if (!/^<svg[\s/>]/.test(svg)) throw svgMarkupError("it does not start with an <svg> element")
144
+ let out = ""
145
+ let depth = 0
146
+ let i = 0
147
+ while (i < svg.length) {
148
+ if (svg.startsWith("<![CDATA[", i)) {
149
+ const close = svg.indexOf("]]>", i + 9)
150
+ if (close === -1) throw svgMarkupError("a CDATA section is not closed")
151
+ out += rules.text(svg.slice(i + 9, close), true)
152
+ i = close + 3
153
+ continue
154
+ }
155
+ if (svg[i] !== "<") {
156
+ const next = svg.indexOf("<", i)
157
+ const end = next === -1 ? svg.length : next
158
+ out += rules.text(svg.slice(i, end), false)
159
+ i = end
160
+ continue
161
+ }
162
+ const tag = readSvgTag(svg, i, rules)
163
+ out += tag.markup
164
+ i = tag.end
165
+ if (tag.closing) depth--
166
+ else if (!tag.selfClosing) {
167
+ depth++
168
+ if (rules.rawText?.has(tag.name) === true) {
169
+ // Wherever the template's own reading ends this element, in any case, raw text ends here too.
170
+ const close = svg.toLowerCase().indexOf(`</${tag.name.toLowerCase()}`, i)
171
+ const end = close === -1 ? svg.length : close
172
+ out += svg.slice(i, end)
173
+ i = end
174
+ }
175
+ }
176
+ if (depth === 0) {
177
+ if (svg.slice(i).trim() !== "") throw svgMarkupError("there is content after the root </svg>")
178
+ return out
179
+ }
180
+ }
181
+ throw svgMarkupError("the root <svg> element is not closed")
182
+ }
183
+
184
+ /** One tag of {@link svgTemplateMarkup}'s input, rebuilt with each attribute value through `rules`. */
185
+ function readSvgTag(
186
+ svg: string,
187
+ start: number,
188
+ rules: SvgMarkupRules,
189
+ ): { markup: string; end: number; name: string; closing: boolean; selfClosing: boolean } {
190
+ const closing = svg[start + 1] === "/"
191
+ let i = start + (closing ? 2 : 1)
192
+ TAG_NAME.lastIndex = i
193
+ const name = TAG_NAME.exec(svg)?.[0]
194
+ if (name === undefined) throw svgMarkupError(`a "<" at offset ${start} opens no tag`)
195
+ const refused = rules.refuse?.(name, "element")
196
+ if (refused !== undefined) throw svgMarkupError(refused)
197
+ i += name.length
198
+ let markup = `<${closing ? "/" : ""}${name}`
199
+ for (;;) {
200
+ SPACE.lastIndex = i
201
+ const space = SPACE.exec(svg)?.[0] ?? ""
202
+ i += space.length
203
+ if (svg[i] === ">")
204
+ return { markup: `${markup}>`, end: i + 1, name, closing, selfClosing: false }
205
+ if (!closing && svg.startsWith("/>", i)) {
206
+ return { markup: `${markup}/>`, end: i + 2, name, closing, selfClosing: true }
207
+ }
208
+ if (i >= svg.length) throw svgMarkupError(`the tag <${name}> is not closed`)
209
+ TAG_NAME.lastIndex = i
210
+ const attribute = closing || space === "" ? undefined : TAG_NAME.exec(svg)?.[0]
211
+ if (attribute === undefined) {
212
+ throw svgMarkupError(`<${name}> holds ${JSON.stringify(svg[i])} where an attribute belongs`)
213
+ }
214
+ const refusedAttribute = rules.refuse?.(attribute, "attribute")
215
+ if (refusedAttribute !== undefined) throw svgMarkupError(refusedAttribute)
216
+ i += attribute.length
217
+ SPACE.lastIndex = i
218
+ i += SPACE.exec(svg)?.[0].length ?? 0
219
+ if (svg[i] !== "=") throw svgMarkupError(`the attribute ${attribute} on <${name}> has no value`)
220
+ SPACE.lastIndex = i + 1
221
+ i += 1 + (SPACE.exec(svg)?.[0].length ?? 0)
222
+ const quote = svg[i]
223
+ const close = quote === '"' || quote === "'" ? svg.indexOf(quote, i + 1) : -1
224
+ if (close === -1) {
225
+ throw svgMarkupError(`the value of ${attribute} on <${name}> is not a quoted string`)
226
+ }
227
+ const value = svg.slice(i + 1, close)
228
+ markup += ` ${attribute}=${quote}${rules.attributeValue?.(value) ?? value}${quote}`
229
+ i = close + 1
230
+ }
231
+ }
232
+
233
+ /** Text as JSX text: a brace, `>` or `=` as a character reference, so none of it is an expression. */
234
+ const jsxText = (text: string, cdata: boolean): string =>
235
+ (cdata ? text.replaceAll("&", "&amp;").replaceAll("<", "&lt;") : text).replace(
236
+ /[{}>=]/g,
237
+ (char) => `&#${char.charCodeAt(0)};`,
238
+ )
239
+
240
+ /** JSX reads `<a.b>` and `<Name>` as a value in scope, called as a component - never an SVG element. */
241
+ const jsxRefuse = (name: string, kind: "element" | "attribute"): string | undefined =>
242
+ kind === "element" && (name.includes(".") || /^[A-Z]/.test(name))
243
+ ? `JSX reads the element <${name}> as a value in scope, not an SVG element`
244
+ : undefined
245
+
116
246
  /** Convert an SVG XML string into a JSX-safe `<svg>…</svg>` element with `{...props}` spread on the root. */
117
247
  export function svgToJsx(xml: string, options: SvgToJsxOptions = {}): string {
118
248
  const classProp = options.classProp ?? "className"
119
- let out = stripSvgPreamble(xml)
249
+ // Text is escaped first, `=` included, so the attribute rewrites below match inside tags only.
250
+ let out = svgTemplateMarkup(xml, { text: jsxText, refuse: jsxRefuse })
120
251
 
121
252
  // Namespaced attributes → camelCase (xlink:href → xlinkHref, xmlns:xlink → xmlnsXlink).
122
253
  out = out.replace(/\b([a-z]+):([a-z]+)=/gi, (_m, ns: string, name: string) => {