@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
@@ -1,14 +1,27 @@
1
+ import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
1
2
  import { MAP_DEFERRED_SOURCE } from "../deferred.ts"
2
3
  import type { LayoutEntry, Manifest } from "../manifest.ts"
3
4
  import {
4
5
  ACTION_GLOBAL,
5
6
  BOUNDARY_GLOBAL,
6
7
  DATA_GLOBAL,
8
+ HANDOVER_ID,
7
9
  LAYOUT_DATA_GLOBAL,
8
10
  ROOT_ATTRIBUTE,
9
11
  ROUTE_GLOBAL,
10
12
  } from "../render-seam.ts"
13
+ import { PRERENDERED_GLOBAL } from "../router.ts"
11
14
  import { jsStringLiteral } from "./js-string.ts"
15
+
16
+ /** The globals the page-state handover may set - nothing else in it reaches `window`. */
17
+ const HANDOVER_GLOBALS = [
18
+ DATA_GLOBAL,
19
+ LAYOUT_DATA_GLOBAL,
20
+ ROUTE_GLOBAL,
21
+ ACTION_GLOBAL,
22
+ BOUNDARY_GLOBAL,
23
+ PRERENDERED_GLOBAL,
24
+ ]
12
25
  export interface GenerateClientEntryOptions {
13
26
  /**
14
27
  * Module specifier for the adapter's client runtime, e.g. `"@nifrajs/web-solid/client"`.
@@ -47,6 +60,8 @@ export function generateClientEntry(
47
60
  // Routes whose loader appends a nearest `_error` module (LAST) - the client wraps the page in the
48
61
  // adapter's `errorBoundary(fallback)` for these, so a client render error shows the `_error` UI.
49
62
  const errorRouteIds: string[] = []
63
+ // Route id → its layout ids, for the chain `useMatches` reports. Routes without layouts are omitted.
64
+ const layoutIdsOf: Record<string, readonly string[]> = {}
50
65
  // Lazy loader returns the raw modules (for both the component chain + the page's `meta` export).
51
66
  const lazyLoader = (files: readonly string[]): string => {
52
67
  const imports = files.map((f) => `import(${jsStringLiteral(resolve(f))})`).join(", ")
@@ -67,6 +82,7 @@ export function generateClientEntry(
67
82
  errorRouteIds.push(route.id)
68
83
  }
69
84
  loaderRows.push(` ${JSON.stringify(route.id)}: ${lazyLoader(files)},`)
85
+ if (route.layoutIds.length > 0) layoutIdsOf[route.id] = route.layoutIds
70
86
  patternRows.push(
71
87
  ` { routeId: ${JSON.stringify(route.id)}, pattern: ${JSON.stringify(route.pattern)} },`,
72
88
  )
@@ -80,14 +96,46 @@ export function generateClientEntry(
80
96
  loaderRows.push(` ${JSON.stringify(routeId)}: ${lazyLoader([entry.file])},`)
81
97
  statusRoutes[Number(status)] = routeId
82
98
  }
99
+ // `_loading` pages. Emitted only when the app has one, so an app without them imports nothing extra.
100
+ // Each is its own lazy chunk; a route row carries its layout ids and the loading pages above it with
101
+ // the number of those layouts each sits under - all the runtime needs to choose one.
102
+ const loadings = Object.entries(manifest.loadings ?? {})
103
+ const loadingLines: string[] = []
104
+ if (loadings.length > 0) {
105
+ const routeRows = new Map<string, string>()
106
+ for (const route of manifest.routes) {
107
+ const above = (route.loadingIds ?? []).map((id) => [
108
+ id,
109
+ manifest.loadings?.[id]?.layoutIds.length ?? 0,
110
+ ])
111
+ routeRows.set(
112
+ route.id,
113
+ ` ${JSON.stringify(route.id)}: ${JSON.stringify([route.layoutIds, above])},`,
114
+ )
115
+ }
116
+ loadingLines.push(
117
+ "const loadingModules = {",
118
+ ...loadings.map(
119
+ ([id, entry]) =>
120
+ ` ${JSON.stringify(id)}: () => import(${jsStringLiteral(resolve(entry.file))}),`,
121
+ ),
122
+ "}",
123
+ "const loadingRoutes = {",
124
+ ...routeRows.values(),
125
+ "}",
126
+ )
127
+ }
83
128
 
84
129
  return `${[
85
130
  // `/client`, never the root: the root's graph carries the server (renderPage, the static-file
86
131
  // server), and Vite's dev server evaluates what it is given instead of tree-shaking it.
87
132
  'import { createClientRouter, createMatcher, mergeHeads, resolveMeta } from "@nifrajs/web/client"',
88
- 'import { applyHead, installForms, installHistory, signalHydrated } from "@nifrajs/web/client"',
133
+ 'import { applyHead, currentDocumentNonce, installForms, installHistory, signalHydrated, waitForStyles } from "@nifrajs/web/client"',
89
134
  // Namespace import: `errorBoundary` is optional (an adapter may not export it). A namespace member
90
135
  // access yields `undefined` if absent - unlike a named import, which would be a link error.
136
+ ...(loadings.length > 0
137
+ ? ['import { withLoading } from "@nifrajs/web/internal/loading-runtime"']
138
+ : []),
91
139
  `import * as __adapter from ${JSON.stringify(clientModule)}`,
92
140
  "const { mountRouter } = __adapter",
93
141
  // The assurance hook is optional: ordinary client entries pay only for the namespace lookup, while
@@ -139,6 +187,12 @@ export function generateClientEntry(
139
187
  // routeId → the page's optional post-hydration client loader/action hooks. Hooks are populated only
140
188
  // after the route chunk loads, so routes that do not declare them remain ordinary route modules.
141
189
  "const routeHooks = {}",
190
+ // routeId → the chain the server rendered for an `ssr = false` page: the same layouts (and
191
+ // boundary), with the page's `HydrateFallback` - or an empty leaf - where the component goes.
192
+ "const holds = {}",
193
+ // routeId → the ids and `handle` exports `useMatches` reports (layouts, then the page).
194
+ `const layoutIdsOf = ${JSON.stringify(layoutIdsOf)}`,
195
+ "const matchChains = {}",
142
196
  "const loadModule = async (id) => {",
143
197
  " if (chains[id]) return",
144
198
  " const mods = await loaders[id]()",
@@ -160,20 +214,34 @@ export function generateClientEntry(
160
214
  // `searchSchema` merges with the page's. Client keys come from the page (second-to-last).
161
215
  " searchSchemas[id] = mods.slice(0, mods.length - 1).map((m) => m.searchSchema)",
162
216
  " searchClientKeys[id] = mods[mods.length - 2].searchClientKeys ?? []",
217
+ " matchChains[id] = { ids: [...(layoutIdsOf[id] ?? []), id], handles: mods.slice(0, mods.length - 1).map((m) => m.handle) }",
163
218
  " } else {",
164
219
  " chains[id] = mods.map((m) => m.default)",
165
220
  " metas[id] = mods.map((m) => m.meta)",
166
221
  // The chain is every module (layouts + page); client keys come from the page (the last module).
167
222
  " searchSchemas[id] = mods.map((m) => m.searchSchema)",
168
223
  " searchClientKeys[id] = mods[mods.length - 1].searchClientKeys ?? []",
224
+ " matchChains[id] = { ids: [...(layoutIdsOf[id] ?? []), id], handles: mods.map((m) => m.handle) }",
169
225
  " }",
170
226
  " const page = errorRouteIds.has(id) ? mods[mods.length - 2] : mods[mods.length - 1]",
171
- // Keep interception client-safe: only neutral name/mode metadata crosses into the router. The
172
- // server-side boundary load function (which may close over secrets) is never stored in the client
173
- // hook table or serialized into a browser bundle by this wiring.
227
+ // Only neutral name/mode metadata crosses into the router; a boundary's loader lives in the route's
228
+ // backend half, which no browser bundle contains.
174
229
  " const boundaryMods = errorRouteIds.has(id) ? mods.slice(0, -1) : mods",
175
- " const boundaries = boundaryMods.flatMap((m) => (m.boundaries ?? []).map((b) => ({ name: b.name, mode: b.mode, hasLoad: b.load !== undefined, ...(b.errorId === undefined ? {} : { errorId: b.errorId }) })))",
230
+ " const boundaries = boundaryMods.flatMap((m) => (m.boundaries ?? []).map((b) => ({ name: b.name, mode: b.mode, ...(b.errorId === undefined ? {} : { errorId: b.errorId }) })))",
176
231
  " routeHooks[id] = { clientLoader: page.clientLoader, clientAction: page.clientAction, boundaries }",
232
+ " if (page.ssr === false) holds[id] = [...chains[id].slice(0, -1), page.HydrateFallback ?? (() => null)]",
233
+ "}",
234
+ // The server hands page state over as one inert JSON script; lift it onto the globals the
235
+ // router, the deferred mapper and the adapters read, before any of them runs. Only the LAST
236
+ // matching <script> counts, and only known keys: page HTML (sanitized user content) can carry an
237
+ // element with the same id, and arbitrary keys copied onto `window` include `location`.
238
+ `const handovers = document.querySelectorAll(${JSON.stringify(`script[type="application/json"][id="${HANDOVER_ID}"]`)})`,
239
+ "const handover = handovers[handovers.length - 1]",
240
+ "if (handover !== undefined) {",
241
+ ' const state = JSON.parse(handover.textContent || "{}")',
242
+ ` for (const key of ${JSON.stringify(HANDOVER_GLOBALS)}) {`,
243
+ " if (Object.prototype.hasOwnProperty.call(state, key)) Reflect.set(window, key, state[key])",
244
+ " }",
177
245
  "}",
178
246
  "const patterns = [",
179
247
  ...patternRows,
@@ -181,12 +249,19 @@ export function generateClientEntry(
181
249
  // Derive the initial route from the URL (correct on refresh/deep-link); fall back to the
182
250
  // server-injected route id for non-pattern routes (e.g. _404, which matches nothing).
183
251
  "const matched = createMatcher(patterns)(location.pathname)",
252
+ `const statusRoutes = ${JSON.stringify(statusRoutes)}`,
253
+ `const serverRoute = window.${ROUTE_GLOBAL} ?? ""`,
254
+ // A terminal status page the server rendered (`_404`, `_410`, ...) outranks the URL match: a
255
+ // loader that throws notFound() on `/learn/bad-slug` leaves a URL that still matches
256
+ // `learn/[slug]`, and hydrating that route with null data over the 404 markup is wrong. Membership
257
+ // in the status table, not a `_` prefix: `routes/_admin/index.tsx` is an ordinary routable id.
258
+ "const terminal = Object.values(statusRoutes).includes(serverRoute)",
184
259
  // Map any `{__nifra_deferred: id}` placeholder in the SSR data to the registry's promise, so the
185
260
  // component receives real promises to `<Await>` (a no-op when a page has no deferred data).
186
261
  MAP_DEFERRED_SOURCE,
187
262
  "const initial = {",
188
- ` routeId: matched ? matched.routeId : (window.${ROUTE_GLOBAL} ?? ""),`,
189
- " params: matched ? matched.params : {},",
263
+ " routeId: matched && !terminal ? matched.routeId : serverRoute,",
264
+ " params: matched && !terminal ? matched.params : {},",
190
265
  // pathname + search (NOT just pathname): the SSR render threads `pathname+search` into
191
266
  // `useLocation`/`useSearchParams`, so the hydrating initial state must carry the query too or a
192
267
  // page reading the search string would hydrate-mismatch. The #hash is client-only (never SSR'd).
@@ -201,8 +276,12 @@ export function generateClientEntry(
201
276
  ` actionData: mapDeferred(window.${ACTION_GLOBAL}),`,
202
277
  " pending: false,",
203
278
  "}",
204
- `const statusRoutes = ${JSON.stringify(statusRoutes)}`,
205
- "const router = createClientRouter({ patterns, initial, loadModule, statusRoutes, searchClientKeys, routeHooks })",
279
+ ...loadingLines,
280
+ // With `_loading` pages the store is wrapped: a slow navigation then names a loading chain (the
281
+ // shared layouts + the `_loading` component) for the mounted view to render in the meantime.
282
+ loadings.length > 0
283
+ ? "const router = withLoading(createClientRouter({ patterns, initial, loadModule, statusRoutes, searchClientKeys, routeHooks }), { routes: loadingRoutes, modules: loadingModules, chains, searchSchemas, matchChains })"
284
+ : "const router = createClientRouter({ patterns, initial, loadModule, statusRoutes, searchClientKeys, routeHooks })",
206
285
  "installHistory(router)",
207
286
  "installForms(router)",
208
287
  // The container is found in the DOM, not baked in. `rootId` is a per-render option and this entry
@@ -219,10 +298,45 @@ export function generateClientEntry(
219
298
  "is set, `#root` otherwise - and this document has neither. If you emit the document yourself, " +
220
299
  'keep `<div id="root">` around the SSR markup.',
221
300
  )})`,
222
- // Load the initial route's chunk, then hydrate the Router (chain is cached). The initial head
223
- // is server-rendered; subsequent navigations update it from the matched route's meta + data.
224
- "loadModule(initial.routeId).then(() => {",
225
- " mountRouter({ router, routes: chains, searchSchemas, container: root })",
301
+ // Wait for framework-owned deferred CSS before loading/mounting the initial route. The coordinator
302
+ // is a no-op for the default blocking links and always has a bounded fail-open terminal state.
303
+ "waitForStyles().then(() => loadModule(initial.routeId)).then(() => {",
304
+ // An `ssr = false` page: the server rendered its held chain, so the view hydrates THAT - under a
305
+ // chain id no route file can produce - and is handed the real route a task later, once every
306
+ // adapter has hydrated and subscribed. Only the first view is held; a client navigation to such a
307
+ // page has no server markup to match and renders the component directly.
308
+ " let view = router",
309
+ " const hold = holds[initial.routeId]",
310
+ " if (hold) {",
311
+ ' const id = "\\0ssr"',
312
+ " chains[id] = hold",
313
+ " searchSchemas[id] = searchSchemas[initial.routeId]",
314
+ " matchChains[id] = matchChains[initial.routeId]",
315
+ " const listeners = new Set()",
316
+ " let held = true",
317
+ " let base",
318
+ " let derived",
319
+ " view = {",
320
+ " ...router,",
321
+ " snapshot() {",
322
+ " const s = router.snapshot()",
323
+ " if (!held) return s",
324
+ // One derived state per router state: the view's store must hand back a stable reference.
325
+ " if (base !== s) derived = { ...(base = s), routeId: id }",
326
+ " return derived",
327
+ " },",
328
+ " subscribe(listener) {",
329
+ " listeners.add(listener)",
330
+ " const off = router.subscribe(listener)",
331
+ " return () => (listeners.delete(listener), off())",
332
+ " },",
333
+ " }",
334
+ " setTimeout(() => {",
335
+ " held = false",
336
+ " for (const listener of [...listeners]) listener()",
337
+ " }, 0)",
338
+ " }",
339
+ " mountRouter({ router: view, routes: chains, searchSchemas, matchChains, container: root })",
226
340
  // Run the optional client loader only after the adapter has mounted the SSR tree. The initial
227
341
  // server data is already in `window.__NIFRA_DATA__`, so `serverLoader()` reuses it and cannot
228
342
  // duplicate the first request.
@@ -236,11 +350,12 @@ export function generateClientEntry(
236
350
  " if (!s.pending) {",
237
351
  // `origin: location.origin` matches the SSR `originOf(req)` (both are `URL.origin` for the same
238
352
  // page URL), so a soft-nav re-resolves the SAME absolute canonical/og:url - no head drift.
239
- " const args = { data: s.data, params: s.params, origin: location.origin }",
353
+ " const nonce = currentDocumentNonce()",
354
+ " const args = { data: s.data, params: s.params, origin: location.origin, ...(nonce === undefined ? {} : { nonce }) }",
240
355
  " applyHead(mergeHeads((metas[s.routeId] ?? [undefined]).map((m) => resolveMeta(m, args))))",
241
356
  " }",
242
357
  " })",
243
- "})",
358
+ "}).catch((error) => console.error('[nifra/web] hydration failed:', error))",
244
359
  ].join("\n")}\n`
245
360
  }
246
361
 
@@ -305,6 +420,8 @@ export interface GenerateServerManifestOptions {
305
420
  /** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked as
306
421
  * `export const routeStyles` so a page links only the CSS its route chain uses. */
307
422
  readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
423
+ /** How framework-owned stylesheet links should activate before the generated client mounts. */
424
+ readonly cssLoading?: CssLoadingMode
308
425
  /** Emit **lazy** per-route loaders (`() => import("./routes/x")`, a static specifier) instead of
309
426
  * eager `import * as`, so a bundler with code-splitting emits one chunk per route - loaded on the
310
427
  * first request to it, not all at boot (smaller cold-start parse). Default `false` (eager). Both
@@ -316,7 +433,8 @@ export interface GenerateServerManifestOptions {
316
433
  * Codegen: emit a **server manifest** module (as source) for disk-less edge runtimes (Cloudflare
317
434
  * Workers, …) - and, with a `target`, any portable server bundle. `discoverRoutes` scans `node:fs`
318
435
  * and dynamic-imports each route by a *runtime* path - neither exists on workerd. This instead emits
319
- * **statically-analyzable** imports of every route/layout/`_error`/terminal status page (so the bundler includes them) and
436
+ * **statically-analyzable** imports of every route/layout/`_error`/terminal status page and each one's
437
+ * backend half (so the bundler includes them) and
320
438
  * rebuilds the manifest with `buildManifest` - the SAME pure logic `discoverRoutes` feeds, so patterns
321
439
  * + layout chains are identical. Eager (`import * as`) by default; `lazy` emits `() => import(...)` so
322
440
  * a code-splitting bundler chunks per route. The emitted module exports `manifest` (consumed by
@@ -335,20 +453,50 @@ function moduleSpecifier(resolved: string): string {
335
453
  return resolved.replace(SOURCE_EXTENSION, "")
336
454
  }
337
455
 
456
+ // A route's namespace is not assignable to `RouteModule` under a strict `tsc` (a backend half has no
457
+ // `default`, a typed `meta` narrows its data), so the table holds plain objects and the cast happens at
458
+ // `buildManifest`'s boundary - the same cast `discoverRoutes` applies to its dynamic import.
459
+ const LAZY_IMPORTER = "(file) => loaders[file] as () => Promise<RouteModule>"
460
+ const EAGER_IMPORTER = "(file) => () => Promise.resolve(modules[file] as RouteModule)"
461
+
338
462
  export function generateServerManifest(
339
463
  manifest: Manifest,
340
464
  options: GenerateServerManifestOptions,
341
465
  ): string {
342
- const { resolve, clientEntry, styles, routeStyles, lazy = false } = options
343
- // Every unique source file in the manifest (routes + layouts + error/status pages), sorted for stable output.
466
+ const {
467
+ resolve,
468
+ clientEntry,
469
+ styles,
470
+ routeStyles,
471
+ cssLoading: requestedCssLoading,
472
+ lazy = false,
473
+ } = options
474
+ // A missing entry would bake `clientEntry = undefined` and the build would still succeed, leaving
475
+ // every hydrating page to fail at request time. `""` stays valid: an app with no client script.
476
+ if (typeof clientEntry !== "string") {
477
+ throw new TypeError(
478
+ `[nifra/web] the server build needs clientEntry (the built client entry URL, buildClient's manifest.entry), got ${clientEntry === null ? "null" : typeof clientEntry}; check the option is spelled clientEntry, or pass "" for an app with no client script`,
479
+ )
480
+ }
481
+ const cssLoading = normalizeCssLoading(requestedCssLoading ?? DEFAULT_CSS_LOADING)
482
+ // Every unique source file in the manifest, both halves of each, sorted for stable output. This is the
483
+ // one generated module that imports frontend and backend halves together: it is the server's
484
+ // composition, and `buildManifest` pairs the halves again from these keys.
485
+ const sources = [
486
+ ...manifest.routes,
487
+ ...Object.values(manifest.layouts),
488
+ ...Object.values(manifest.errors ?? {}),
489
+ ...(manifest.notFound ? [manifest.notFound] : []),
490
+ ...Object.values(manifest.notFounds ?? {}),
491
+ ...Object.values(manifest.statusPages ?? {}),
492
+ ...Object.values(manifest.middlewares ?? {}),
493
+ ]
344
494
  const files = [
345
- ...new Set([
346
- ...manifest.routes.map((r) => r.file),
347
- ...Object.values(manifest.layouts).map((l) => l.file),
348
- ...Object.values(manifest.errors ?? {}).map((e) => e.file),
349
- ...(manifest.notFound ? [manifest.notFound.file] : []),
350
- ...Object.values(manifest.statusPages ?? {}).map((page) => page.file),
351
- ]),
495
+ ...new Set(
496
+ sources.flatMap((entry) =>
497
+ entry.backend === undefined ? [entry.file] : [entry.file, entry.backend],
498
+ ),
499
+ ),
352
500
  ].sort()
353
501
  const header = [
354
502
  "// GENERATED by @nifrajs/web generateServerManifest - route manifest for the disk-less edge",
@@ -361,6 +509,7 @@ export function generateServerManifest(
361
509
  // Always emitted (default empty) so consumers can `import { styles, routeStyles }` unconditionally.
362
510
  const stylesLine = `export const styles = ${JSON.stringify(styles ?? [])}`
363
511
  const routeStylesLine = `export const routeStyles = ${JSON.stringify(routeStyles ?? {})}`
512
+ const cssLoadingLine = `export const cssLoading = ${JSON.stringify(cssLoading)}`
364
513
  if (lazy) {
365
514
  // Lazy: `() => import("./routes/x")` per route (static specifier → one chunk per route under a
366
515
  // code-splitting bundler, loaded on first request). The map keys are the route-relative paths
@@ -371,13 +520,14 @@ export function generateServerManifest(
371
520
  )
372
521
  return `${[
373
522
  ...header,
374
- "const loaders: Record<string, () => Promise<RouteModule>> = {",
523
+ "const loaders: Record<string, () => Promise<object>> = {",
375
524
  ...loaders,
376
525
  "}",
377
526
  clientEntryLine,
378
527
  stylesLine,
379
528
  routeStylesLine,
380
- "export const manifest = buildManifest(Object.keys(loaders), (file) => () => loaders[file]())",
529
+ cssLoadingLine,
530
+ `export const manifest = buildManifest(Object.keys(loaders), ${LAZY_IMPORTER})`,
381
531
  ].join("\n")}\n`
382
532
  }
383
533
  // Eager: `import * as` per route (all bundled into the entry, parsed at boot). Index-based
@@ -389,12 +539,13 @@ export function generateServerManifest(
389
539
  return `${[
390
540
  ...header,
391
541
  ...imports,
392
- "const modules: Record<string, RouteModule> = {",
542
+ "const modules: Record<string, object> = {",
393
543
  ...entries,
394
544
  "}",
395
545
  clientEntryLine,
396
546
  stylesLine,
397
547
  routeStylesLine,
398
- "export const manifest = buildManifest(Object.keys(modules), (file) => () => Promise.resolve(modules[file]))",
548
+ cssLoadingLine,
549
+ `export const manifest = buildManifest(Object.keys(modules), ${EAGER_IMPORTER})`,
399
550
  ].join("\n")}\n`
400
551
  }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Vite dev adds its own tags to each SSR'd page: the HMR client, a framework's refresh preamble (an
3
+ * inline script), and at runtime a `<style>` per imported stylesheet. A page whose CSP admits scripts
4
+ * by nonce or hash refuses all of them, so the app would not boot in dev. Dev gives every such tag one
5
+ * nonce - the page's own, or a fresh one for a hash-only page - and names it in each directive that
6
+ * governs them.
7
+ */
8
+
9
+ const POLICY_HEADERS = ["content-security-policy", "content-security-policy-report-only"] as const
10
+
11
+ /** Directives that govern each element kind, most specific first: the first present one applies. */
12
+ const GOVERNING = [
13
+ ["script-src-elem", "script-src", "default-src"],
14
+ ["style-src-elem", "style-src", "default-src"],
15
+ ] as const
16
+
17
+ const NONCE_SOURCE = /'nonce-([^']+)'/
18
+
19
+ /** True when a directive admits every inline element: `'unsafe-inline'`, not voided by a nonce or hash. */
20
+ const allowsInline = (sources: readonly string[]): boolean =>
21
+ sources.includes("'unsafe-inline'") &&
22
+ !sources.some((source) => /^'(?:nonce|sha256|sha384|sha512)-/.test(source))
23
+
24
+ /** One header value (comma-separated policies), with `'nonce-<nonce>'` added where it is needed. */
25
+ function admitNonce(value: string, nonce: string): string {
26
+ const token = `'nonce-${nonce}'`
27
+ return value
28
+ .split(",")
29
+ .map((policy) => {
30
+ const directives = policy
31
+ .split(";")
32
+ .map((directive) => directive.trim())
33
+ .filter((directive) => directive !== "")
34
+ .map((directive) => directive.split(/\s+/))
35
+ for (const names of GOVERNING) {
36
+ const governing = names
37
+ .map((name) => directives.find(([directive]) => directive?.toLowerCase() === name))
38
+ .find((directive) => directive !== undefined)
39
+ if (governing === undefined) continue
40
+ const sources = governing.slice(1)
41
+ if (sources.includes(token) || sources.includes("'none'") || allowsInline(sources)) continue
42
+ governing.push(token)
43
+ }
44
+ return directives.map((directive) => directive.join(" ")).join("; ")
45
+ })
46
+ .join(", ")
47
+ }
48
+
49
+ const RAW_TEXT_START = /<(script|style)\b([^>]*)>/gi
50
+
51
+ /** `html` with `nonce` on each `<script>` / `<style>` that has none; their bodies are left as written. */
52
+ function nonceTags(html: string, nonce: string): string {
53
+ let out = ""
54
+ let at = 0
55
+ const lower = html.toLowerCase()
56
+ RAW_TEXT_START.lastIndex = 0
57
+ for (let match = RAW_TEXT_START.exec(html); match !== null; match = RAW_TEXT_START.exec(html)) {
58
+ const [tag, name = "", attrs = ""] = match
59
+ out += html.slice(at, match.index)
60
+ out += /\snonce\s*=/i.test(attrs) ? tag : `<${name} nonce="${nonce}"${attrs}>`
61
+ const close = lower.indexOf(`</${name.toLowerCase()}`, RAW_TEXT_START.lastIndex)
62
+ at = close === -1 ? html.length : close
63
+ out += html.slice(RAW_TEXT_START.lastIndex, at)
64
+ RAW_TEXT_START.lastIndex = at
65
+ }
66
+ return out + html.slice(at)
67
+ }
68
+
69
+ /**
70
+ * Let a dev page's CSP admit what Vite injected. Rewrites the policy headers in `headers` and returns
71
+ * the page with the nonce on its script and style tags, plus the `csp-nonce` meta Vite's client reads
72
+ * to nonce the styles it adds at runtime. A page with no CSP is returned as it is.
73
+ */
74
+ export function admitViteTags(
75
+ html: string,
76
+ headers: Headers,
77
+ generate: () => string = () => crypto.randomUUID().replaceAll("-", ""),
78
+ ): string {
79
+ const policies = POLICY_HEADERS.map((name) => [name, headers.get(name)] as const)
80
+ if (policies.every(([, value]) => value === null)) return html
81
+ const nonce =
82
+ policies.map(([, value]) => NONCE_SOURCE.exec(value ?? "")?.[1]).find((n) => n !== undefined) ??
83
+ generate()
84
+ for (const [name, value] of policies) {
85
+ if (value !== null) headers.set(name, admitNonce(value, nonce))
86
+ }
87
+ const tagged = nonceTags(html, nonce)
88
+ if (tagged.includes(`property="csp-nonce"`)) return tagged
89
+ const meta = `<meta property="csp-nonce" nonce="${nonce}">`
90
+ const head = /<head\b[^>]*>/i.exec(tagged)
91
+ return head === null
92
+ ? meta + tagged
93
+ : tagged.slice(0, head.index + head[0].length) +
94
+ meta +
95
+ tagged.slice(head.index + head[0].length)
96
+ }
97
+
98
+ const SCRIPT_DIRECTIVES = GOVERNING[0]
99
+ const CONNECT_DIRECTIVES = ["connect-src", "default-src"] as const
100
+
101
+ /** Whether `sources` already let the page reach `url` (a same-origin absolute URL). */
102
+ const reaches = (sources: readonly string[], url: URL): boolean =>
103
+ sources.some((source) => {
104
+ const lower = source.toLowerCase()
105
+ return (
106
+ lower === "'self'" ||
107
+ lower === "*" ||
108
+ lower === `${url.protocol}` ||
109
+ lower === url.origin ||
110
+ lower === `${url.origin}/` ||
111
+ lower === url.href
112
+ )
113
+ })
114
+
115
+ /**
116
+ * Admit nifra's own inline dev script, by `hash`, and its POSTs to `connect`, in each policy header.
117
+ * Without a `hash` the caller nonces the script itself. Returns false when a policy forbids scripts
118
+ * outright (`'none'`): such a page runs no code, so there is nothing to capture and the script stays
119
+ * out. A directive that already admits every inline script is left alone, because adding a hash would
120
+ * switch its `'unsafe-inline'` off. `module` is the one script the inline one may load; it is admitted
121
+ * by its exact URL, except under `'strict-dynamic'`, which ignores URLs and trusts what a trusted
122
+ * script loads anyway.
123
+ */
124
+ export function admitInlineScript(
125
+ headers: Headers,
126
+ hash: string | undefined,
127
+ connect: URL,
128
+ module?: URL,
129
+ ): boolean {
130
+ const updates: [string, string][] = []
131
+ let allowed = true
132
+ for (const name of POLICY_HEADERS) {
133
+ const value = headers.get(name)
134
+ if (value === null) continue
135
+ const next = value
136
+ .split(",")
137
+ .map((policy) => {
138
+ const directives = policy
139
+ .split(";")
140
+ .map((directive) => directive.trim())
141
+ .filter((directive) => directive !== "")
142
+ .map((directive) => directive.split(/\s+/))
143
+ const governing = (names: readonly string[]): string[] | undefined =>
144
+ names
145
+ .map((n) => directives.find(([directive]) => directive?.toLowerCase() === n))
146
+ .find((directive) => directive !== undefined)
147
+ const script = governing(SCRIPT_DIRECTIVES)
148
+ if (script !== undefined) {
149
+ const sources = script.slice(1)
150
+ if (sources.includes("'none'")) allowed = false
151
+ else {
152
+ if (hash !== undefined && !sources.includes(hash) && !allowsInline(sources))
153
+ script.push(hash)
154
+ if (
155
+ module !== undefined &&
156
+ !sources.includes("'strict-dynamic'") &&
157
+ !reaches(sources, module)
158
+ )
159
+ script.push(module.href)
160
+ }
161
+ }
162
+ const connectDirective = governing(CONNECT_DIRECTIVES)
163
+ if (connectDirective !== undefined && !reaches(connectDirective.slice(1), connect)) {
164
+ const none = connectDirective.indexOf("'none'")
165
+ if (none === -1) connectDirective.push(connect.href)
166
+ else connectDirective.splice(none, 1, connect.href)
167
+ }
168
+ return directives.map((directive) => directive.join(" ")).join("; ")
169
+ })
170
+ .join(", ")
171
+ updates.push([name, next])
172
+ }
173
+ if (!allowed) return false
174
+ for (const [name, value] of updates) headers.set(name, value)
175
+ return true
176
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Specifiers no plugin may match on Bun's dev-server bundler.
3
+ *
4
+ * When a `[serve.static]` plugin's `onResolve` FILTER matches an import, Bun sends it down its plugin
5
+ * path, and a handler that declines does not undo that: Bun then resolves the import on a fallback that
6
+ * breaks a nifra dev page in two ways (Bun 1.3.14 through 1.4.3, oven-sh/bun#19951):
7
+ *
8
+ * - an HTML page's `<script src>` keeps its raw specifier in the HMR module table, so the browser throws
9
+ * "Failed to load bundled module" and the page never hydrates;
10
+ * - the fallback skips the dev server's built-in modules, so the React Fast Refresh runtime Bun embeds
11
+ * for an app without `react-refresh` installed fails to resolve, and the dev server does not start.
12
+ *
13
+ * unplugin's Bun adapter registers `onResolve({ filter: /.*\/ })` for every plugin with a `resolveId`
14
+ * hook, so an app plugin can hit both without doing anything unusual. Every plugin nifra hands that
15
+ * bundler registers through {@link reserveDevSpecifiers}.
16
+ */
17
+
18
+ import type { PluginBuilder } from "bun"
19
+
20
+ /** The generated client entry, written beside the probe page that loads it. Named so that no import
21
+ * an app writes can be the reserved specifier. */
22
+ export const DEV_ENTRY_FILE = "nifra-dev-entry.tsx"
23
+
24
+ const RESERVED = [`./${DEV_ENTRY_FILE}`, "react-refresh/runtime/index.js"]
25
+ const RESERVED_SOURCE = RESERVED.map((specifier) =>
26
+ specifier.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"),
27
+ ).join("|")
28
+
29
+ /** `filter`, except that it never matches a reserved specifier. Any other string matches as before. */
30
+ export function withoutReservedSpecifiers(filter: RegExp): RegExp {
31
+ // The original runs inside a lookahead from the start, so it still matches anywhere in the string
32
+ // and its own anchors, lookbehinds and backreferences keep their meaning. `g` and `y` are dropped:
33
+ // they make `test` depend on `lastIndex`, which a filter has no use for.
34
+ return new RegExp(
35
+ `^(?!(?:${RESERVED_SOURCE})$)(?=[\\s\\S]*?(?:${filter.source}))`,
36
+ filter.flags.replace(/[gy]/g, ""),
37
+ )
38
+ }
39
+
40
+ /** A view of `build` whose `onResolve` filters never match a reserved specifier. */
41
+ export function reserveDevSpecifiers(build: PluginBuilder): PluginBuilder {
42
+ return {
43
+ ...build,
44
+ onResolve(constraints, callback) {
45
+ build.onResolve(
46
+ { ...constraints, filter: withoutReservedSpecifiers(constraints.filter) },
47
+ callback,
48
+ )
49
+ return this
50
+ },
51
+ }
52
+ }
@@ -0,0 +1,50 @@
1
+ // The comment and delimiter readers scan instead of matching one lazy RegExp: a pattern like
2
+ // `<!--[\s\S]*?-->` reads to the end of the source again from every opener that never closes.
3
+
4
+ /**
5
+ * `source` without its HTML comments. A browser ends one at `-->`, and also at `--!>`; an opener
6
+ * with no end after it is kept as text.
7
+ */
8
+ export function withoutComments(source: string): string {
9
+ const end = /--!?>/g
10
+ let kept = ""
11
+ let from = 0
12
+ for (let start = source.indexOf("<!--"); start !== -1; start = source.indexOf("<!--", from)) {
13
+ end.lastIndex = start + 4
14
+ const close = end.exec(source)
15
+ // No later opener has an end after it either.
16
+ if (close === null) break
17
+ kept += source.slice(from, start)
18
+ from = close.index + close[0].length
19
+ }
20
+ return kept + source.slice(from)
21
+ }
22
+
23
+ /** The text between each `open` and the first `close` after it, read on from that `close`. */
24
+ export function delimitedSpans(source: string, open: string, close: string): string[] {
25
+ const spans: string[] = []
26
+ for (let start = source.indexOf(open); start !== -1; ) {
27
+ const end = source.indexOf(close, start + open.length)
28
+ if (end === -1) break
29
+ spans.push(source.slice(start + open.length, end))
30
+ start = source.indexOf(open, end + close.length)
31
+ }
32
+ return spans
33
+ }
34
+
35
+ /** `source` without the spans the global `pattern` matches, each handed to `onMatch` first. */
36
+ export function withoutMatches(
37
+ source: string,
38
+ pattern: RegExp,
39
+ onMatch?: (match: RegExpMatchArray) => void,
40
+ ): string {
41
+ let kept = ""
42
+ let from = 0
43
+ for (const match of source.matchAll(pattern)) {
44
+ onMatch?.(match)
45
+ const start = match.index ?? 0
46
+ kept += source.slice(from, start)
47
+ from = start + match[0].length
48
+ }
49
+ return kept + source.slice(from)
50
+ }