@lovrozagar/flare 0.1.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 (387) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/package.json +146 -0
  4. package/scripts/download-fonts.ts +136 -0
  5. package/scripts/populate-fonts.ts +721 -0
  6. package/src/await.ts +2 -0
  7. package/src/boundaries/index.ts +99 -0
  8. package/src/broadcast/channel.ts +65 -0
  9. package/src/broadcast/context.ts +12 -0
  10. package/src/broadcast/index.ts +75 -0
  11. package/src/broadcast/provider.tsx +7 -0
  12. package/src/build-location.ts +2 -0
  13. package/src/caches/index.ts +384 -0
  14. package/src/client/index.ts +75 -0
  15. package/src/client.tsx +2 -0
  16. package/src/codegen/index.ts +41 -0
  17. package/src/components/await.tsx +140 -0
  18. package/src/components/dev-error-overlay.tsx +154 -0
  19. package/src/components/direction-script.tsx +22 -0
  20. package/src/components/index.ts +150 -0
  21. package/src/components/locale-script.tsx +26 -0
  22. package/src/components/reset-css.tsx +14 -0
  23. package/src/components/ssr-context.tsx +37 -0
  24. package/src/components/theme-script.tsx +22 -0
  25. package/src/components/view-transition-css.tsx +21 -0
  26. package/src/config/index.ts +84 -0
  27. package/src/create-client-only-fn.ts +1 -0
  28. package/src/create-isomorphic-fn.ts +2 -0
  29. package/src/create-layout.ts +1 -0
  30. package/src/create-page.ts +1 -0
  31. package/src/create-path-segment.ts +1 -0
  32. package/src/create-query-client-getter.ts +2 -0
  33. package/src/create-root-layout.ts +1 -0
  34. package/src/create-router.ts +8 -0
  35. package/src/create-server-only-fn.ts +1 -0
  36. package/src/dedupe/index.ts +151 -0
  37. package/src/defer/index.ts +82 -0
  38. package/src/direction/index.tsx +163 -0
  39. package/src/direction.ts +3 -0
  40. package/src/duration/index.ts +18 -0
  41. package/src/env-fn/index.ts +101 -0
  42. package/src/env-fn.ts +2 -0
  43. package/src/errors/index.ts +166 -0
  44. package/src/fonts/abeezee.ts +50 -0
  45. package/src/fonts/abel.ts +26 -0
  46. package/src/fonts/abril-fatface.ts +34 -0
  47. package/src/fonts/acme.ts +26 -0
  48. package/src/fonts/advent-pro.ts +92 -0
  49. package/src/fonts/alegreya-sans.ts +228 -0
  50. package/src/fonts/alegreya.ts +228 -0
  51. package/src/fonts/alex-brush.ts +42 -0
  52. package/src/fonts/alfa-slab-one.ts +42 -0
  53. package/src/fonts/alice.ts +48 -0
  54. package/src/fonts/amatic-sc.ts +94 -0
  55. package/src/fonts/amiri.ts +114 -0
  56. package/src/fonts/antic-slab.ts +26 -0
  57. package/src/fonts/anton.ts +42 -0
  58. package/src/fonts/archivo-black.ts +34 -0
  59. package/src/fonts/archivo-narrow.ts +114 -0
  60. package/src/fonts/archivo.ts +66 -0
  61. package/src/fonts/arimo.ts +256 -0
  62. package/src/fonts/arvo.ts +50 -0
  63. package/src/fonts/asap.ts +66 -0
  64. package/src/fonts/assistant.ts +64 -0
  65. package/src/fonts/baloo-2.ts +82 -0
  66. package/src/fonts/barlow-condensed.ts +114 -0
  67. package/src/fonts/barlow-semi-condensed.ts +114 -0
  68. package/src/fonts/barlow.ts +114 -0
  69. package/src/fonts/be-vietnam-pro.ts +114 -0
  70. package/src/fonts/bebas-neue.ts +34 -0
  71. package/src/fonts/besley.ts +82 -0
  72. package/src/fonts/biorhyme.ts +50 -0
  73. package/src/fonts/bitter.ts +94 -0
  74. package/src/fonts/black-ops-one.ts +49 -0
  75. package/src/fonts/bree-serif.ts +34 -0
  76. package/src/fonts/bricolage-grotesque.ts +66 -0
  77. package/src/fonts/cabin.ts +114 -0
  78. package/src/fonts/cairo.ts +66 -0
  79. package/src/fonts/cardo.ts +193 -0
  80. package/src/fonts/catamaran.ts +41 -0
  81. package/src/fonts/caveat.ts +78 -0
  82. package/src/fonts/chakra-petch.ts +142 -0
  83. package/src/fonts/chivo.ts +66 -0
  84. package/src/fonts/cinzel.ts +50 -0
  85. package/src/fonts/comfortaa.ts +109 -0
  86. package/src/fonts/commissioner.ts +64 -0
  87. package/src/fonts/component.tsx +27 -0
  88. package/src/fonts/cormorant-garamond.ts +171 -0
  89. package/src/fonts/cormorant.ts +170 -0
  90. package/src/fonts/courgette.ts +34 -0
  91. package/src/fonts/courier-prime.ts +82 -0
  92. package/src/fonts/create-font.ts +47 -0
  93. package/src/fonts/create-registry-font.ts +40 -0
  94. package/src/fonts/crete-round.ts +50 -0
  95. package/src/fonts/crimson-text.ts +114 -0
  96. package/src/fonts/css.ts +86 -0
  97. package/src/fonts/dancing-script.ts +66 -0
  98. package/src/fonts/dm-mono.ts +50 -0
  99. package/src/fonts/dm-sans.ts +50 -0
  100. package/src/fonts/dm-serif-display.ts +50 -0
  101. package/src/fonts/domine.ts +50 -0
  102. package/src/fonts/dosis.ts +66 -0
  103. package/src/fonts/eb-garamond.ts +228 -0
  104. package/src/fonts/eczar.ts +94 -0
  105. package/src/fonts/encode-sans.ts +42 -0
  106. package/src/fonts/epilogue.ts +66 -0
  107. package/src/fonts/exo-2.ts +94 -0
  108. package/src/fonts/exo.ts +66 -0
  109. package/src/fonts/figtree.ts +82 -0
  110. package/src/fonts/fira-code.ts +121 -0
  111. package/src/fonts/fira-sans-condensed.ts +228 -0
  112. package/src/fonts/fira-sans.ts +228 -0
  113. package/src/fonts/fjalla-one.ts +49 -0
  114. package/src/fonts/font-urls.gen.json +1962 -0
  115. package/src/fonts/fraunces.ts +66 -0
  116. package/src/fonts/fredoka.ts +64 -0
  117. package/src/fonts/gelasio.ts +114 -0
  118. package/src/fonts/gloria-hallelujah.ts +34 -0
  119. package/src/fonts/gothic-a1.ts +124 -0
  120. package/src/fonts/great-vibes.ts +64 -0
  121. package/src/fonts/gudea.ts +66 -0
  122. package/src/fonts/heebo.ts +57 -0
  123. package/src/fonts/hind-siliguri.ts +66 -0
  124. package/src/fonts/hind.ts +66 -0
  125. package/src/fonts/ibm-plex-mono.ts +172 -0
  126. package/src/fonts/ibm-plex-sans-arabic.ts +80 -0
  127. package/src/fonts/ibm-plex-sans.ts +199 -0
  128. package/src/fonts/ibm-plex-serif.ts +171 -0
  129. package/src/fonts/inconsolata.ts +66 -0
  130. package/src/fonts/index.ts +4 -0
  131. package/src/fonts/indie-flower.ts +34 -0
  132. package/src/fonts/instrument-sans.ts +82 -0
  133. package/src/fonts/instrument-serif.ts +50 -0
  134. package/src/fonts/inter-display.ts +52 -0
  135. package/src/fonts/inter-tight.ts +124 -0
  136. package/src/fonts/inter.ts +123 -0
  137. package/src/fonts/jetbrains-mono.ts +199 -0
  138. package/src/fonts/josefin-sans.ts +114 -0
  139. package/src/fonts/josefin-slab.ts +50 -0
  140. package/src/fonts/jost.ts +64 -0
  141. package/src/fonts/kalam.ts +66 -0
  142. package/src/fonts/kanit.ts +142 -0
  143. package/src/fonts/karla.ts +82 -0
  144. package/src/fonts/khand.ts +66 -0
  145. package/src/fonts/lato.ts +82 -0
  146. package/src/fonts/league-spartan.ts +42 -0
  147. package/src/fonts/lexend-deca.ts +42 -0
  148. package/src/fonts/lexend.ts +42 -0
  149. package/src/fonts/libre-baskerville.ts +82 -0
  150. package/src/fonts/libre-franklin.ts +95 -0
  151. package/src/fonts/lilita-one.ts +34 -0
  152. package/src/fonts/lobster-two.ts +50 -0
  153. package/src/fonts/lobster.ts +56 -0
  154. package/src/fonts/lora.ts +235 -0
  155. package/src/fonts/m-plus-rounded-1c.ts +138 -0
  156. package/src/fonts/manrope.ts +109 -0
  157. package/src/fonts/maven-pro.ts +66 -0
  158. package/src/fonts/merriweather-sans.ts +142 -0
  159. package/src/fonts/merriweather.ts +171 -0
  160. package/src/fonts/montserrat-alternates.ts +171 -0
  161. package/src/fonts/montserrat.ts +94 -0
  162. package/src/fonts/mukta.ts +66 -0
  163. package/src/fonts/mulish.ts +170 -0
  164. package/src/fonts/nanum-gothic.ts +34 -0
  165. package/src/fonts/nanum-myeongjo.ts +34 -0
  166. package/src/fonts/neuton.ts +66 -0
  167. package/src/fonts/newsreader.ts +114 -0
  168. package/src/fonts/noticia-text.ts +114 -0
  169. package/src/fonts/noto-sans-arabic.ts +58 -0
  170. package/src/fonts/noto-sans-hk.ts +49 -0
  171. package/src/fonts/noto-sans-jp.ts +49 -0
  172. package/src/fonts/noto-sans-kr.ts +49 -0
  173. package/src/fonts/noto-sans-sc.ts +49 -0
  174. package/src/fonts/noto-sans-tc.ts +49 -0
  175. package/src/fonts/noto-sans.ts +140 -0
  176. package/src/fonts/noto-serif-jp.ts +80 -0
  177. package/src/fonts/noto-serif.ts +140 -0
  178. package/src/fonts/nunito-sans.ts +170 -0
  179. package/src/fonts/nunito.ts +170 -0
  180. package/src/fonts/old-standard-tt.ts +133 -0
  181. package/src/fonts/onest.ts +48 -0
  182. package/src/fonts/open-sans.ts +340 -0
  183. package/src/fonts/orbitron.ts +34 -0
  184. package/src/fonts/oswald.ts +94 -0
  185. package/src/fonts/outfit.ts +34 -0
  186. package/src/fonts/overpass.ts +94 -0
  187. package/src/fonts/oxygen.ts +50 -0
  188. package/src/fonts/pacifico.ts +56 -0
  189. package/src/fonts/passion-one.ts +50 -0
  190. package/src/fonts/pathway-gothic-one.ts +34 -0
  191. package/src/fonts/permanent-marker.ts +26 -0
  192. package/src/fonts/philosopher.ts +172 -0
  193. package/src/fonts/play.ts +109 -0
  194. package/src/fonts/playfair-display.ts +142 -0
  195. package/src/fonts/plus-jakarta-sans.ts +142 -0
  196. package/src/fonts/poppins.ts +114 -0
  197. package/src/fonts/prompt.ts +142 -0
  198. package/src/fonts/pt-sans-narrow.ts +78 -0
  199. package/src/fonts/pt-sans.ts +138 -0
  200. package/src/fonts/pt-serif.ts +138 -0
  201. package/src/fonts/public-sans.ts +66 -0
  202. package/src/fonts/questrial.ts +42 -0
  203. package/src/fonts/quicksand.ts +66 -0
  204. package/src/fonts/rajdhani.ts +66 -0
  205. package/src/fonts/raleway.ts +94 -0
  206. package/src/fonts/recursive.ts +80 -0
  207. package/src/fonts/red-hat-display.ts +82 -0
  208. package/src/fonts/red-rose.ts +66 -0
  209. package/src/fonts/registry.gen.ts +173 -0
  210. package/src/fonts/righteous.ts +34 -0
  211. package/src/fonts/roboto-condensed.ts +124 -0
  212. package/src/fonts/roboto-flex.ts +64 -0
  213. package/src/fonts/roboto-mono.ts +199 -0
  214. package/src/fonts/roboto-slab.ts +72 -0
  215. package/src/fonts/roboto.ts +156 -0
  216. package/src/fonts/rokkitt.ts +66 -0
  217. package/src/fonts/rubik.ts +199 -0
  218. package/src/fonts/sacramento.ts +34 -0
  219. package/src/fonts/saira.ts +66 -0
  220. package/src/fonts/sarabun.ts +142 -0
  221. package/src/fonts/satisfy.ts +26 -0
  222. package/src/fonts/sawarabi-mincho.ts +41 -0
  223. package/src/fonts/shadows-into-light.ts +34 -0
  224. package/src/fonts/signika-negative.ts +66 -0
  225. package/src/fonts/signika.ts +66 -0
  226. package/src/fonts/slabo-27px.ts +34 -0
  227. package/src/fonts/sora.ts +50 -0
  228. package/src/fonts/source-code-pro.ts +228 -0
  229. package/src/fonts/source-sans-3.ts +228 -0
  230. package/src/fonts/source-serif-4.ts +199 -0
  231. package/src/fonts/space-grotesk.ts +66 -0
  232. package/src/fonts/space-mono.ts +114 -0
  233. package/src/fonts/spectral.ts +170 -0
  234. package/src/fonts/tajawal.ts +50 -0
  235. package/src/fonts/teko.ts +66 -0
  236. package/src/fonts/tenor-sans.ts +41 -0
  237. package/src/fonts/tinos.ts +256 -0
  238. package/src/fonts/titillium-web.ts +82 -0
  239. package/src/fonts/types.ts +43 -0
  240. package/src/fonts/ubuntu-condensed.ts +63 -0
  241. package/src/fonts/ubuntu-mono.ts +195 -0
  242. package/src/fonts/ubuntu.ts +195 -0
  243. package/src/fonts/unbounded.ts +94 -0
  244. package/src/fonts/unicode-ranges.gen.ts +28 -0
  245. package/src/fonts/urbanist.ts +50 -0
  246. package/src/fonts/varela-round.ts +49 -0
  247. package/src/fonts/vollkorn.ts +199 -0
  248. package/src/fonts/work-sans.ts +66 -0
  249. package/src/fonts/yanone-kaffeesatz.ts +128 -0
  250. package/src/fonts/yantramanav.ts +66 -0
  251. package/src/fonts/yellowtail.ts +34 -0
  252. package/src/fonts/young-serif.ts +34 -0
  253. package/src/fonts/zen-kaku-gothic-new.ts +64 -0
  254. package/src/fonts/zilla-slab.ts +82 -0
  255. package/src/form/index.tsx +205 -0
  256. package/src/generators/index.ts +1589 -0
  257. package/src/head-client/index.ts +649 -0
  258. package/src/history/index.ts +210 -0
  259. package/src/hooks.ts +61 -0
  260. package/src/hydrate/index.tsx +381 -0
  261. package/src/hydration/index.ts +187 -0
  262. package/src/i18n/index.ts +250 -0
  263. package/src/i18n/jsx.tsx +42 -0
  264. package/src/i18n/message-format.ts +174 -0
  265. package/src/image/index.tsx +473 -0
  266. package/src/image.ts +12 -0
  267. package/src/intercept-outlet/index.tsx +18 -0
  268. package/src/intercept-outlet.ts +2 -0
  269. package/src/internal/index.ts +143 -0
  270. package/src/internal/once-idle.ts +24 -0
  271. package/src/internal.ts +12 -0
  272. package/src/lazy/index.tsx +205 -0
  273. package/src/link/index.tsx +367 -0
  274. package/src/loader-pipeline/index.ts +639 -0
  275. package/src/locale/index.tsx +137 -0
  276. package/src/locale.ts +3 -0
  277. package/src/logger.ts +22 -0
  278. package/src/middleware/builtins/api-proxy.ts +64 -0
  279. package/src/middleware/builtins/cdn-proxy.ts +58 -0
  280. package/src/middleware/builtins/i18n.ts +256 -0
  281. package/src/middleware/builtins/keepalive.ts +24 -0
  282. package/src/middleware/builtins/markdown-negotiation.ts +141 -0
  283. package/src/middleware/builtins/static-assets.ts +32 -0
  284. package/src/middleware/index.ts +223 -0
  285. package/src/module-graph/index.ts +181 -0
  286. package/src/mount/index.ts +146 -0
  287. package/src/navigate.ts +1 -0
  288. package/src/navigation/index.ts +1278 -0
  289. package/src/navigation/types.ts +59 -0
  290. package/src/navigation-progress/index.tsx +247 -0
  291. package/src/navigation-progress.ts +1 -0
  292. package/src/ndjson-client/index.ts +376 -0
  293. package/src/ndjson-server/index.ts +284 -0
  294. package/src/outlet/index.tsx +633 -0
  295. package/src/outlet/types.ts +243 -0
  296. package/src/plugins/css-transform.ts +70 -0
  297. package/src/plugins/dev-cdn-cache.ts +454 -0
  298. package/src/plugins/dev-dashboard/client.tsx +2603 -0
  299. package/src/plugins/dev-dashboard/match-route-tree.ts +159 -0
  300. package/src/plugins/dev-dashboard/plugin.ts +404 -0
  301. package/src/plugins/dev-prerender.ts +168 -0
  302. package/src/plugins/dev-server.ts +327 -0
  303. package/src/plugins/generate-watch.ts +32 -0
  304. package/src/plugins/image-plugin.ts +171 -0
  305. package/src/plugins/index.ts +445 -0
  306. package/src/plugins/options.ts +37 -0
  307. package/src/plugins/parse-utils.ts +59 -0
  308. package/src/plugins/prerender-plugin.ts +144 -0
  309. package/src/plugins/purge.ts +242 -0
  310. package/src/plugins/server-fn.ts +641 -0
  311. package/src/plugins/service-worker.ts +225 -0
  312. package/src/plugins/solid-jsx.d.ts +12 -0
  313. package/src/plugins/sx-ast/atomic.ts +166 -0
  314. package/src/plugins/sx-ast/evaluator.ts +235 -0
  315. package/src/plugins/sx-ast/index.ts +372 -0
  316. package/src/plugins/sx-ast/parser.ts +22 -0
  317. package/src/plugins/sx-ast/rewrite.ts +763 -0
  318. package/src/plugins/sx.d.ts +14 -0
  319. package/src/plugins/tw-compile.ts +240 -0
  320. package/src/plugins/types.ts +41 -0
  321. package/src/plugins/virtual.d.ts +110 -0
  322. package/src/plugins/virtual.ts +184 -0
  323. package/src/preload/index.ts +77 -0
  324. package/src/prerender/index.ts +431 -0
  325. package/src/query-client/index.tsx +168 -0
  326. package/src/registry/index.ts +88 -0
  327. package/src/reset-css.ts +1 -0
  328. package/src/revalidation/index.ts +84 -0
  329. package/src/rewrite/index.ts +83 -0
  330. package/src/route-builder/create-layout.ts +655 -0
  331. package/src/route-builder/create-page.ts +806 -0
  332. package/src/route-builder/create-path-segment.ts +34 -0
  333. package/src/route-builder/create-root-layout.ts +608 -0
  334. package/src/route-builder/index.ts +49 -0
  335. package/src/route-builder/register.ts +242 -0
  336. package/src/route-builder/types.ts +294 -0
  337. package/src/router-config/index.ts +91 -0
  338. package/src/router-primitives/index.ts +25 -0
  339. package/src/router-primitives/location.ts +29 -0
  340. package/src/router-primitives/match-id.ts +111 -0
  341. package/src/router-primitives/paths.ts +109 -0
  342. package/src/router-primitives/tree.ts +243 -0
  343. package/src/router-primitives/types.ts +74 -0
  344. package/src/router.ts +31 -0
  345. package/src/search-engine/bing.ts +32 -0
  346. package/src/search-engine/google-jwt.ts +95 -0
  347. package/src/search-engine/google.ts +139 -0
  348. package/src/search-engine/index-now.ts +64 -0
  349. package/src/search-engine/index.ts +12 -0
  350. package/src/security/index.ts +230 -0
  351. package/src/server/index.ts +286 -0
  352. package/src/server-context/browser.ts +113 -0
  353. package/src/server-context/index.ts +217 -0
  354. package/src/server-fn/index.ts +710 -0
  355. package/src/server-fn-query.ts +169 -0
  356. package/src/server-handler/etag.ts +51 -0
  357. package/src/server-handler/headers.ts +5 -0
  358. package/src/server-handler/index.ts +1664 -0
  359. package/src/server-handler/types.ts +24 -0
  360. package/src/server-handler/validate-static-params.ts +130 -0
  361. package/src/server.ts +2 -0
  362. package/src/service-worker/template.ts +153 -0
  363. package/src/service-worker-hook/index.ts +70 -0
  364. package/src/sitemap/index.ts +231 -0
  365. package/src/ssr/critical-css.ts +150 -0
  366. package/src/ssr/head-prefix.ts +65 -0
  367. package/src/ssr/head.ts +259 -0
  368. package/src/ssr/index.tsx +879 -0
  369. package/src/state-parser/index.ts +266 -0
  370. package/src/store/filesystem.ts +131 -0
  371. package/src/store/index.ts +29 -0
  372. package/src/styles/index.ts +1200 -0
  373. package/src/styles/sx-types.ts +11 -0
  374. package/src/testing/index.ts +251 -0
  375. package/src/testing.ts +2 -0
  376. package/src/theme/index.tsx +232 -0
  377. package/src/theme.ts +3 -0
  378. package/src/tracing/index.ts +4 -0
  379. package/src/tracing/noop.ts +13 -0
  380. package/src/tracing/otel.ts +37 -0
  381. package/src/tracing/timing.ts +34 -0
  382. package/src/tracing/types.ts +9 -0
  383. package/src/url/index.ts +140 -0
  384. package/src/use-router.ts +11 -0
  385. package/src/use-suspense-query.ts +2 -0
  386. package/src/validation/index.ts +57 -0
  387. package/src/view-transition-css.ts +1 -0
@@ -0,0 +1,879 @@
1
+ import type { JSX } from "solid-js";
2
+ import { createComponent, Hydration, renderToStream as solidRenderToStream } from "solid-js/web";
3
+ import type { GlobalBoundaries } from "../boundaries/index.ts";
4
+ import { BroadcastProvider } from "../broadcast/provider.tsx";
5
+ import { createMatchCache, createPrefetchCache } from "../caches/index.ts";
6
+ import { setSSRContext } from "../components/ssr-context.tsx";
7
+ import type { DeferContext, DeferredEntry } from "../defer/index.ts";
8
+ import { isDeferred } from "../defer/index.ts";
9
+ import { DirectionProvider } from "../direction.ts";
10
+ import { isNotFoundError, isUnauthenticatedError, isUnauthorizedError } from "../errors/index.ts";
11
+ import { applyResponseHeaders, isRenderFn } from "../internal.ts";
12
+ import type { PipelineMatch } from "../loader-pipeline/index.ts";
13
+ import { FlareProvider, Outlet, useRouter } from "../outlet/index.tsx";
14
+ import type {
15
+ ClientErrorRenderProps,
16
+ ClientMatch,
17
+ ClientNotFoundRenderProps,
18
+ ClientUnauthenticatedRenderProps,
19
+ ClientUnauthorizedRenderProps,
20
+ ProviderLocation,
21
+ RenderProps,
22
+ } from "../outlet/types.ts";
23
+ import { dk as getDynamicKeys } from "../registry/index.ts";
24
+ import type { HeadConfig } from "../route-builder/types.ts";
25
+ import { extractSerializable, type MarkedRouterConfig } from "../router-config/index.ts";
26
+ import { createTreeNode } from "../router-primitives/index.ts";
27
+ import type { ServerLogEntry } from "@lovrozagar/flare/server-context";
28
+ import { clearScopedStyles, getScopedStyles, RUNTIME_SHEET_ID } from "../styles/index.ts";
29
+ import { CRITICAL_SHEET_ID, injectCriticalAppend, type SxCssManifest } from "./critical-css.ts";
30
+ import { ThemeProvider } from "../theme.ts";
31
+ import { parseSearchParams, type SearchParams } from "../url/index.ts";
32
+ import { renderHeadToHtml } from "./head.ts";
33
+ import { buildHeadPrefix } from "./head-prefix.ts";
34
+
35
+ export { mergeHeadConfigs } from "../internal.ts";
36
+ export { renderHeadToHtml } from "./head.ts";
37
+ export { buildHeadPrefix } from "./head-prefix.ts";
38
+
39
+ export const MAX_STREAM_BUFFER_SIZE = 2 * 1024 * 1024;
40
+
41
+ export interface FlareState {
42
+ c: ContextState;
43
+ dk?: string[];
44
+ e?: DevError[];
45
+ g?: ServerLogEntry[];
46
+ m: FlareMatchState[];
47
+ p: string;
48
+ ph?: PerRouteHead[];
49
+ q?: QueryState[];
50
+ r: Record<string, string | string[]>;
51
+ s: SearchParams;
52
+ }
53
+
54
+ export interface FlareMatchState {
55
+ d: unknown;
56
+ h?: HeadConfig;
57
+ i: string;
58
+ p?: Record<string, unknown>;
59
+ v: string;
60
+ x?: string;
61
+ }
62
+
63
+ export interface ContextState {
64
+ dir?: string;
65
+ locale?: string;
66
+ router?: Record<string, unknown>;
67
+ theme?: string;
68
+ }
69
+
70
+ export interface DevError {
71
+ message: string;
72
+ name: string;
73
+ source: string;
74
+ stack?: string;
75
+ }
76
+
77
+ export interface PerRouteHead {
78
+ head: HeadConfig;
79
+ matchId: string;
80
+ }
81
+
82
+ export interface QueryState {
83
+ data: unknown;
84
+ key: unknown[];
85
+ staleTime?: number;
86
+ }
87
+
88
+ export interface ModulePreloads {
89
+ css: string[];
90
+ js: string[];
91
+ }
92
+
93
+ export interface SSRConfig {
94
+ auth: unknown | null;
95
+ cause: string;
96
+ deferContexts?: Map<string, DeferContext>;
97
+ entryScript?: string;
98
+ matches: PipelineMatch[];
99
+ modulePreloads?: ModulePreloads;
100
+ moduleScripts: string[];
101
+ nonce: string;
102
+ params?: Record<string, string | string[]>;
103
+ prefetch: boolean;
104
+ queryClient?: unknown;
105
+ queryClientGetter?: () => unknown;
106
+ queryClientProvider?: (props: { children: JSX.Element; client: unknown }) => JSX.Element;
107
+ resolvedHead: HeadConfig;
108
+ router?: MarkedRouterConfig;
109
+ serverLogs?: ServerLogEntry[];
110
+ /** Loaded from flare-sx-manifest.json at server startup. Enables critical-CSS injection. */
111
+ sxCssManifest?: SxCssManifest;
112
+ /** Module IDs in the rendered route's dependency graph. Used to union Show/Switch branch classes. */
113
+ sxRenderedModules?: string[];
114
+ url: URL;
115
+ }
116
+
117
+ export interface SSRResult {
118
+ body: ReadableStream<Uint8Array>;
119
+ headers: Headers;
120
+ status: number;
121
+ }
122
+
123
+ function escapeAttr(str: string): string {
124
+ return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
125
+ }
126
+
127
+ /**
128
+ * Walk value tree, strip `promise` from Deferred markers.
129
+ * Returns a JSON-safe clone.
130
+ * Uses ancestry-based cycle detection: tracks objects on the current
131
+ * recursion stack (add before recurse, delete after) so shared references
132
+ * (diamonds) are allowed but true cycles return null.
133
+ */
134
+ function stripDeferredPromises(value: unknown, ancestors?: WeakSet<object>): unknown {
135
+ if (value === null || value === undefined) return value;
136
+ if (typeof value !== "object") return value;
137
+
138
+ const a = ancestors ?? new WeakSet<object>();
139
+ if (a.has(value as object)) return null;
140
+ a.add(value as object);
141
+
142
+ let result: unknown;
143
+ if (Array.isArray(value)) {
144
+ result = value.map((v) => stripDeferredPromises(v, a));
145
+ } else {
146
+ const obj = value as Record<string, unknown>;
147
+ if (isDeferred(obj)) {
148
+ result = { __deferred: true, key: obj.key };
149
+ } else {
150
+ const out: Record<string, unknown> = {};
151
+ for (const key of Object.keys(obj)) {
152
+ out[key] = stripDeferredPromises(obj[key], a);
153
+ }
154
+ result = out;
155
+ }
156
+ }
157
+
158
+ a.delete(value as object);
159
+ return result;
160
+ }
161
+
162
+ export function serializeFlareState(state: FlareState): string {
163
+ const cleaned = stripDeferredPromises(state);
164
+ /* Strip error info entirely — never serialize to client (security) */
165
+ delete (cleaned as { e?: unknown }).e;
166
+ const json = JSON.stringify(cleaned);
167
+ return json.replace(/</g, "\\u003c");
168
+ }
169
+
170
+ const FLARE_STATE_MARKER = "data-flare-state";
171
+
172
+ export function buildFlareStateScript(state: FlareState, nonce: string): string {
173
+ return `<script ${FLARE_STATE_MARKER} nonce="${escapeAttr(nonce)}">self.flare=${serializeFlareState(state)};</script>`;
174
+ }
175
+
176
+ export { applyResponseHeaders, mergeResponseHeaders } from "../internal.ts";
177
+
178
+ /**
179
+ * Derive HTTP status from pipeline match errors.
180
+ * Priority: 401 > 403 > 404 > 500 > 200
181
+ */
182
+ export function deriveStatus(matches: Array<{ error?: Error }>): number {
183
+ let status = 200;
184
+
185
+ for (const match of matches) {
186
+ if (!match.error) continue;
187
+
188
+ if (isUnauthenticatedError(match.error)) return 401;
189
+
190
+ if (isUnauthorizedError(match.error)) {
191
+ if (status !== 401) status = 403;
192
+ } else if (isNotFoundError(match.error)) {
193
+ if (status !== 401 && status !== 403) status = 404;
194
+ } else {
195
+ if (status === 200) status = 500;
196
+ }
197
+ }
198
+
199
+ return status;
200
+ }
201
+
202
+ /* ── renderToStream ────────────────────────────────────────────────── */
203
+
204
+ function buildFlareState(config: SSRConfig): FlareState {
205
+ const matchStates: FlareMatchState[] = config.matches.map((m) => {
206
+ const state: FlareMatchState = {
207
+ d: stripDeferredPromises(m.loaderData),
208
+ i: m.matchId,
209
+ v: m.route.virtualPath,
210
+ };
211
+ if (m.preloaderContext) {
212
+ for (const k in m.preloaderContext) {
213
+ if (Object.hasOwn(m.preloaderContext, k)) {
214
+ state.p = m.preloaderContext;
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ if (m.headConfig) {
220
+ state.h = m.headConfig;
221
+ }
222
+ if (m.error) {
223
+ state.x = m.error.name;
224
+ }
225
+ return state;
226
+ });
227
+
228
+ const contextState: ContextState = {};
229
+ if (config.router) {
230
+ contextState.router = extractSerializable(config.router) as Record<string, unknown>;
231
+ }
232
+
233
+ const state: FlareState = {
234
+ c: contextState,
235
+ m: matchStates,
236
+ p: config.url.pathname,
237
+ r: config.params ?? {},
238
+ s: parseSearchParams(config.url.searchParams),
239
+ };
240
+
241
+ /* per-route heads for client initRouteHierarchy */
242
+ const perRouteHeads: PerRouteHead[] = [];
243
+ for (const m of config.matches) {
244
+ if (m.headConfig) {
245
+ perRouteHeads.push({ head: m.headConfig as HeadConfig, matchId: m.matchId });
246
+ }
247
+ }
248
+ if (perRouteHeads.length > 0) {
249
+ state.ph = perRouteHeads;
250
+ }
251
+
252
+ /* dynamic registry keys for client preloading (spec 18) */
253
+ const dynamicKeys = getDynamicKeys();
254
+ if (dynamicKeys.length > 0) {
255
+ state.dk = dynamicKeys;
256
+ }
257
+
258
+ /* dev-only SSR errors for client overlay (spec 37) */
259
+ const devErrors: DevError[] = config.matches
260
+ .filter((m) => m.error)
261
+ .map((m) => ({
262
+ message: m.error?.message ?? "Unknown error",
263
+ name: m.error?.name ?? "Error",
264
+ source: m.route.virtualPath,
265
+ stack: m.error?.stack,
266
+ }));
267
+ for (const m of config.matches) {
268
+ if (m.headError) {
269
+ devErrors.push({
270
+ message: m.headError.message,
271
+ name: `HeadError(${m.route.virtualPath})`,
272
+ source: m.route.virtualPath,
273
+ stack: m.headError.stack,
274
+ });
275
+ }
276
+ if (m.headersError) {
277
+ devErrors.push({
278
+ message: m.headersError.message,
279
+ name: `HeadersError(${m.route.virtualPath})`,
280
+ source: m.route.virtualPath,
281
+ stack: m.headersError.stack,
282
+ });
283
+ }
284
+ }
285
+ if (devErrors.length > 0) {
286
+ state.e = devErrors;
287
+ }
288
+
289
+ /* query client hydration handled post-render in renderToStream (spec 33) */
290
+
291
+ /* dev-only server logs for client console forwarding */
292
+ if (config.serverLogs && config.serverLogs.length > 0) {
293
+ state.g = config.serverLogs;
294
+ }
295
+
296
+ return state;
297
+ }
298
+
299
+ /**
300
+ * Build script tags injected before </body>.
301
+ *
302
+ * Entry and module scripts use inline `import()` rather than `src="..."` so
303
+ * the <script> tag itself has no network request. The actual JS is fetched
304
+ * via <link rel="modulepreload"> in <head> (injected by injectHeadContent),
305
+ * which Chrome can schedule at High priority with isLinkPreload=true.
306
+ * A <script type="module" src="..."> would create a second parser-driven
307
+ * fetch that competes with the modulepreload and appears in the critical chain.
308
+ */
309
+ function buildScriptTags(config: SSRConfig, flareState: FlareState): string {
310
+ const parts: string[] = [];
311
+ const escapedNonce = escapeAttr(config.nonce);
312
+
313
+ parts.push(buildFlareStateScript(flareState, config.nonce));
314
+
315
+ if (config.entryScript) {
316
+ parts.push(
317
+ `<script nonce="${escapedNonce}" type="module" async>import(${JSON.stringify(config.entryScript).replace(/</g, "\\u003c")})</script>`,
318
+ );
319
+ }
320
+
321
+ for (const src of config.moduleScripts) {
322
+ parts.push(
323
+ `<script nonce="${escapedNonce}" type="module" async>import(${JSON.stringify(src).replace(/</g, "\\u003c")})</script>`,
324
+ );
325
+ }
326
+
327
+ return parts.join("");
328
+ }
329
+
330
+ /**
331
+ * Convert PipelineMatch[] → ClientMatch[] for FlareProvider.
332
+ * Pipeline uses generic fn types (Record<string, unknown> => unknown),
333
+ * client uses JSX types. Wrappers bridge with single casts (not banned double-cast).
334
+ */
335
+ function pipelineMatchesToClientMatches(matches: PipelineMatch[]): ClientMatch[] {
336
+ return matches.map((m) => {
337
+ const er = m.route.errorRender;
338
+ const nfr = m.route.notFoundRender;
339
+ const uar = m.route.unauthenticatedRender;
340
+ const ur = m.route.unauthorizedRender;
341
+ const r = m.route.render;
342
+
343
+ return {
344
+ _type: m.route._type as "layout" | "render",
345
+ error: m.error,
346
+ errorRender: isRenderFn(er)
347
+ ? (((p: ClientErrorRenderProps) => er(p) as JSX.Element) as ClientMatch["errorRender"])
348
+ : undefined,
349
+ loaderData: m.loaderData,
350
+ notFoundRender: isRenderFn(nfr)
351
+ ? (((p: ClientNotFoundRenderProps) => nfr(p) as JSX.Element) as ClientMatch["notFoundRender"])
352
+ : undefined,
353
+ preloaderContext: m.preloaderContext,
354
+ render: isRenderFn(r)
355
+ ? (((p: RenderProps) => r(p) as JSX.Element) as ClientMatch["render"])
356
+ : ((() => null) as ClientMatch["render"]),
357
+ unauthenticatedRender: isRenderFn(uar)
358
+ ? (((p: ClientUnauthenticatedRenderProps) => uar(p) as JSX.Element) as ClientMatch["unauthenticatedRender"])
359
+ : undefined,
360
+ unauthorizedRender: isRenderFn(ur)
361
+ ? (((p: ClientUnauthorizedRenderProps) => ur(p) as JSX.Element) as ClientMatch["unauthorizedRender"])
362
+ : undefined,
363
+ variablePath: m.route.variablePath,
364
+ virtualPath: m.route.virtualPath,
365
+ };
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Extract root layout boundary renders as GlobalBoundaries.
371
+ * Root layout boundaries are passed separately to FlareProvider
372
+ * for global error/notFound/auth boundary resolution.
373
+ */
374
+ function extractRootBoundaries(rootMatch: PipelineMatch | undefined): GlobalBoundaries {
375
+ const boundaries: GlobalBoundaries = {};
376
+ if (rootMatch?.route.errorRender && isRenderFn(rootMatch.route.errorRender)) {
377
+ boundaries.error = rootMatch.route.errorRender as GlobalBoundaries["error"];
378
+ }
379
+ if (rootMatch?.route.notFoundRender && isRenderFn(rootMatch.route.notFoundRender)) {
380
+ boundaries.notFound = rootMatch.route.notFoundRender as GlobalBoundaries["notFound"];
381
+ }
382
+ if (rootMatch?.route.unauthenticatedRender && isRenderFn(rootMatch.route.unauthenticatedRender)) {
383
+ boundaries.unauthenticated = rootMatch.route.unauthenticatedRender as GlobalBoundaries["unauthenticated"];
384
+ }
385
+ if (rootMatch?.route.unauthorizedRender && isRenderFn(rootMatch.route.unauthorizedRender)) {
386
+ boundaries.unauthorized = rootMatch.route.unauthorizedRender as GlobalBoundaries["unauthorized"];
387
+ }
388
+ return boundaries;
389
+ }
390
+
391
+ function RootRenderer(props: {
392
+ data: unknown;
393
+ location: ProviderLocation;
394
+ preloaderContext: Record<string, unknown> | undefined;
395
+ renderFn: (p: RenderProps) => JSX.Element;
396
+ }): JSX.Element {
397
+ const router = useRouter();
398
+ return props.renderFn({
399
+ children: <Outlet />,
400
+ loaderData: props.data,
401
+ location: props.location,
402
+ preloaderContext: props.preloaderContext,
403
+ router,
404
+ });
405
+ }
406
+
407
+ /**
408
+ * Build component tree for SSR rendering.
409
+ *
410
+ * Full-document hydration: Hydration wraps entire tree including root layout.
411
+ * Client hydrates `document` (not just #app), so root layout render fn runs
412
+ * on both SSR and client — enabling reactive components (NavigationProgress)
413
+ * inside root layout.
414
+ *
415
+ * Head components (ThemeScript, DirectionScript, etc.) use their own
416
+ * <NoHydration> internally so their innerHTML-injected content stays inert.
417
+ */
418
+ function buildComponentTree(config: SSRConfig, flareStateScript: string): () => JSX.Element {
419
+ const matches = config.matches;
420
+
421
+ return () => {
422
+ setSSRContext({
423
+ direction: config.router?.direction,
424
+ entryScript: config.entryScript,
425
+ flareStateScript,
426
+ isServer: true,
427
+ nonce: config.nonce,
428
+ resolvedHead: config.resolvedHead,
429
+ theme: config.router?.theme,
430
+ });
431
+
432
+ const lastMatch = matches[matches.length - 1];
433
+ const location: ProviderLocation = {
434
+ hash: "",
435
+ params: config.params ?? ({} as Record<string, string | string[]>),
436
+ pathname: config.url.pathname,
437
+ search: parseSearchParams(config.url.searchParams),
438
+ url: config.url,
439
+ variablePath: lastMatch?.route.variablePath ?? "",
440
+ virtualPath: lastMatch?.route.virtualPath ?? "",
441
+ };
442
+
443
+ const rootMatch = matches.find((m) => m.route._type === "root-layout");
444
+ const nonRootMatches = matches.filter((m) => m.route._type !== "root-layout");
445
+
446
+ const clientMatches = pipelineMatchesToClientMatches(nonRootMatches);
447
+ const rootBoundaries = extractRootBoundaries(rootMatch);
448
+ const matchCache = createMatchCache();
449
+ const prefetchCache = createPrefetchCache();
450
+
451
+ /*
452
+ * Full-document hydration: providers wrap root layout so components
453
+ * inside the root layout (e.g. NavigationProgress) have RouterContext.
454
+ *
455
+ * SSR: Hydration > Theme > Direction > FlareProvider > rootRenderFn({children: Outlet})
456
+ * Client: Dummy > Theme > Direction > FlareProvider > rootRenderFn({children: Outlet})
457
+ *
458
+ * All JSX inside the Hydration boundary — no pre-evaluated constants outside.
459
+ */
460
+ let rootRenderFn: ((props: RenderProps) => JSX.Element) | undefined;
461
+ if (rootMatch?.error) {
462
+ rootRenderFn = rootMatch.route.errorRender
463
+ ? (props: RenderProps) =>
464
+ (rootMatch.route.errorRender as (p: Record<string, unknown>) => JSX.Element)({
465
+ ...props,
466
+ error: rootMatch.error,
467
+ reset: () => {},
468
+ })
469
+ : undefined;
470
+ } else {
471
+ rootRenderFn = rootMatch?.route.render as typeof rootRenderFn;
472
+ }
473
+
474
+ const QCP = config.queryClientProvider;
475
+ const qc = config.queryClient;
476
+
477
+ /*
478
+ * Inner tree factory — must be lazy so that when QCP wraps it,
479
+ * component functions (useSuspenseQuery → useQueryClient) execute
480
+ * inside the Provider's context scope during Solid SSR.
481
+ */
482
+ const renderInner = () => (
483
+ <ThemeProvider config={config.router?.theme}>
484
+ <DirectionProvider config={config.router?.direction}>
485
+ <BroadcastProvider>
486
+ <FlareProvider
487
+ boundaries={rootBoundaries}
488
+ caseSensitive={config.router?.caseSensitive}
489
+ initialLocation={location}
490
+ layouts={config.router?.layouts ?? {}}
491
+ localeConfig={config.router?.locale}
492
+ matchCache={matchCache}
493
+ matches={clientMatches}
494
+ params={location.params}
495
+ prefetchCache={prefetchCache}
496
+ resolvers={new Map()}
497
+ routeTree={config.router?.routeTree ?? createTreeNode()}
498
+ search={location.search}
499
+ >
500
+ {rootRenderFn ? (
501
+ <RootRenderer
502
+ data={rootMatch?.loaderData}
503
+ location={location}
504
+ preloaderContext={rootMatch?.preloaderContext}
505
+ renderFn={rootRenderFn}
506
+ />
507
+ ) : (
508
+ <Outlet />
509
+ )}
510
+ </FlareProvider>
511
+ </BroadcastProvider>
512
+ </DirectionProvider>
513
+ </ThemeProvider>
514
+ );
515
+
516
+ return (
517
+ <Hydration>
518
+ {QCP && qc
519
+ ? createComponent(QCP, {
520
+ get children() {
521
+ return renderInner();
522
+ },
523
+ client: qc,
524
+ })
525
+ : renderInner()}
526
+ </Hydration>
527
+ ) as JSX.Element;
528
+ };
529
+ }
530
+
531
+ /**
532
+ * Inject resolved head tags, CSP nonce meta, and scoped styles before </head>.
533
+ * Returns updated buffer with head content injected, or original buffer if </head> not found.
534
+ *
535
+ * Head structure (order matters for first paint + Chrome's preload scanner):
536
+ * 1. <meta name="csp-nonce"> with nonce attribute (browser hides it from DOM/CSS)
537
+ * 2. viewport (if the app omitted one)
538
+ * 3. theme / direction / locale blocking scripts — before CSS so first land is themed
539
+ * 4. <link rel="modulepreload"> + stylesheets
540
+ * 5. Solid's SSR output (layout ThemeScript is a duplicate, harmless)
541
+ * 6. Resolved <head> tags (title, meta, etc.)
542
+ * 7. Scoped styles
543
+ *
544
+ * Theme scripts must precede stylesheets. Modulepreloads stay after those
545
+ * tiny inline scripts so Chrome can still mark them isLinkPreload when CSP
546
+ * has no 'strict-dynamic' (crbug.com/702612).
547
+ */
548
+ function injectHeadContent(
549
+ buffer: string,
550
+ config: SSRConfig,
551
+ scopedStyles: string,
552
+ extraSuffix = "",
553
+ devSxCss = "",
554
+ devSxClasses: string[] = [],
555
+ ): string {
556
+ const escapedNonce = escapeAttr(config.nonce);
557
+ const headPrefix = buildHeadPrefix({
558
+ direction: config.router?.direction,
559
+ locale: config.router?.locale,
560
+ modulePreloads: config.modulePreloads,
561
+ nonce: config.nonce,
562
+ resolvedHead: config.resolvedHead,
563
+ theme: config.router?.theme,
564
+ });
565
+
566
+ let headSuffix = "";
567
+ const headHtml = renderHeadToHtml(config.resolvedHead, config.nonce);
568
+ if (headHtml) headSuffix += headHtml;
569
+ if (scopedStyles) {
570
+ const safeStyles = scopedStyles.replace(/<\/style\b/gi, "<\\/style");
571
+ headSuffix += `<style id="${RUNTIME_SHEET_ID}" nonce="${escapedNonce}">${safeStyles}</style>`;
572
+ }
573
+ /* Critical-CSS placeholder — body not known yet; late-inject populates it near </body>. */
574
+ if (config.sxCssManifest) {
575
+ const nonceAttr = config.nonce ? ` nonce="${escapedNonce}"` : "";
576
+ headSuffix += `<style id="${CRITICAL_SHEET_ID}"${nonceAttr}></style>`;
577
+ const href = config.sxCssManifest.bundleHref;
578
+ if (href) {
579
+ const escapedHref = href.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
580
+ /* Direct stylesheet — inline `onload="this.rel='stylesheet'"` preload trick violates
581
+ * CSP `script-src` (inline event handler). Critical CSS already ships in the head
582
+ * placeholder above, so the bundle link can resolve synchronously without the swap. */
583
+ headSuffix += `<link rel="stylesheet" href="${escapedHref}"${nonceAttr}/>`;
584
+ }
585
+ }
586
+ /*
587
+ * Dev-mode sx CSS inlined directly — CF Workers SSR bypasses transformIndexHtml so the
588
+ * placeholder <style id="flare-sx-dev"> is never in the template HTML. Inlining here
589
+ * ensures styles land in the SSR response before any JS runs.
590
+ */
591
+ if (devSxCss) {
592
+ const nonceAttr = config.nonce ? ` nonce="${escapedNonce}"` : "";
593
+ const safeCss = devSxCss.replace(/<\/style\b/gi, "<\\/style");
594
+ headSuffix += `<style id="flare-sx-dev"${nonceAttr}>${safeCss}</style>`;
595
+ if (devSxClasses && devSxClasses.length > 0) {
596
+ /* Seed window.__flare_sx_classes__ with the full SSR class-pool before any module
597
+ * runs. Per-module client inject snippets dedupe against this Set — stops duplicate
598
+ * @layer app rules from stacking and flipping source-order cascade (breaks combos
599
+ * like `hidden md:flex`). */
600
+ const seedJson = JSON.stringify(devSxClasses).replace(/</g, "\\u003c");
601
+ headSuffix += `<script${nonceAttr}>window.__flare_sx_classes__=new Set(${seedJson})</script>`;
602
+ }
603
+ }
604
+
605
+ /*
606
+ * CSP with nonce-based style-src blocks inline style="" attributes.
607
+ * Extract <body style="..."> and promote to a nonce'd <style> tag so
608
+ * critical body styles (font-family, background, color) apply on first paint.
609
+ */
610
+ const bodyStyleMatch = buffer.match(/<body\b[^>]*?\sstyle="([^"]*)"/);
611
+ if (bodyStyleMatch) {
612
+ const cssText = bodyStyleMatch[1]
613
+ .replace(/&quot;/g, '"')
614
+ .replace(/&amp;/g, "&")
615
+ .replace(/&lt;/g, "<")
616
+ .replace(/&gt;/g, ">");
617
+ headSuffix += `<style nonce="${escapedNonce}">body{${cssText}}</style>`;
618
+ }
619
+
620
+ const result = buffer.replace(/<head[^>]*>/, `$&${headPrefix}`);
621
+ return result.replace("</head>", `${headSuffix}${extraSuffix}</head>`);
622
+ }
623
+
624
+ /**
625
+ * Inject hydration scripts (flare state + entry) before </body>.
626
+ * Automatic — no component needed in root layout.
627
+ */
628
+ export function serializeQueryClientCache(qc: unknown, nonce: string): string {
629
+ const typed = qc as
630
+ | {
631
+ getQueryCache?: () => {
632
+ getAll?: () => Array<{
633
+ options?: { staleTime?: number };
634
+ queryKey: unknown[];
635
+ state: { data: unknown };
636
+ }>;
637
+ };
638
+ }
639
+ | undefined;
640
+ const queries = typed?.getQueryCache?.()?.getAll?.();
641
+ if (!queries || queries.length === 0) return "";
642
+ const entries: QueryState[] = queries.map((q) => {
643
+ const qs: QueryState = { data: q.state.data, key: q.queryKey };
644
+ if (q.options?.staleTime !== undefined) {
645
+ qs.staleTime = q.options.staleTime;
646
+ }
647
+ return qs;
648
+ });
649
+ const json = JSON.stringify(entries).replace(/</g, "\\u003c");
650
+ return `<script nonce="${escapeAttr(nonce)}">self.flare.q=${json};</script>`;
651
+ }
652
+
653
+ function injectBodyContent(buffer: string, config: SSRConfig, flareState: FlareState, qcScript: string): string {
654
+ /* Late-inject critical CSS computed from final rendered body + module manifest. */
655
+ let result = config.sxCssManifest
656
+ ? injectCriticalAppend(buffer, config.sxRenderedModules ?? [], config.sxCssManifest, config.nonce)
657
+ : buffer;
658
+
659
+ let scripts = buildScriptTags(config, flareState);
660
+ if (qcScript) scripts = scripts.replace(/<\/script>/, `</script>${qcScript}`);
661
+ if (scripts) result = result.replace("</body>", `${scripts}</body>`);
662
+ return result;
663
+ }
664
+
665
+ export function renderToStream(config: SSRConfig): SSRResult {
666
+ clearScopedStyles();
667
+
668
+ /* Resolve queryClient from getter if not provided directly */
669
+ if (!config.queryClient && config.queryClientGetter) {
670
+ config.queryClient = config.queryClientGetter();
671
+ }
672
+
673
+ const flareState = buildFlareState(config);
674
+ const flareStateScript = buildFlareStateScript(flareState, config.nonce);
675
+ const treeFactory = buildComponentTree(config, flareStateScript);
676
+
677
+ const solidStream = solidRenderToStream(treeFactory, { nonce: config.nonce });
678
+
679
+ /*
680
+ * Capture scoped styles immediately after sync render completes.
681
+ * Components register styles during solidRenderToStream (synchronous).
682
+ * Reading eagerly prevents a concurrent SSR render from clearing the
683
+ * registry before our stream transform runs injectHeadContent.
684
+ */
685
+ const capturedStyles = getScopedStyles();
686
+
687
+ const encoder = new TextEncoder();
688
+ const decoder = new TextDecoder();
689
+
690
+ /*
691
+ * Solid only injects _$HY when async work exists (Suspense, deferred data).
692
+ * For sync-only rendering, _$HY is never set, but client `hydrate()` always
693
+ * reads `globalThis._$HY.done`. Inject a minimal _$HY init unconditionally.
694
+ */
695
+ const hyInit = `<script nonce="${escapeAttr(config.nonce)}">window._$HY||(window._$HY={events:[],completed:new WeakSet,r:{},fe(){},done:false})</script>`;
696
+
697
+ let streamBuffer = "";
698
+ let streamDoctypeInjected = false;
699
+ let streamHeadInjected = false;
700
+ let streamBodyInjected = false;
701
+ let hyInjected = false;
702
+
703
+ /* Solid's pipeTo writes Uint8Array chunks despite the typing */
704
+ const { readable, writable } = new TransformStream();
705
+ /* Prevent unhandled rejection — Solid's pipeTo returns Promise at runtime despite void typing */
706
+ void Promise.resolve(solidStream.pipeTo(writable)).catch(() => {});
707
+
708
+ const reader = readable.getReader();
709
+
710
+ const body = new ReadableStream<Uint8Array>({
711
+ async start(controller) {
712
+ try {
713
+ /*
714
+ * Resolve dev sx CSS before the read loop so both injectHeadContent call sites
715
+ * (in-loop and flush) get the same snapshot. Dynamic import re-runs the virtual
716
+ * module's load hook each time — no caching — so HMR additions are reflected.
717
+ * Guarded by import.meta.env.DEV so prod builds tree-shake this entirely.
718
+ */
719
+ let resolvedDevSxCss = "";
720
+ let resolvedDevSxClasses: string[] = [];
721
+ if (import.meta.env.DEV) {
722
+ try {
723
+ const mod = await import("virtual:flare-sx-dev-css");
724
+ resolvedDevSxCss = mod.getDevSxCss();
725
+ resolvedDevSxClasses = mod.getDevSxClasses();
726
+ } catch {
727
+ /* virtual module absent (sx plugin not enabled) — silently skip */
728
+ }
729
+ }
730
+
731
+ while (true) {
732
+ const { done, value } = await reader.read();
733
+ if (done) break;
734
+ const chunk = value instanceof Uint8Array ? decoder.decode(value, { stream: true }) : String(value);
735
+ streamBuffer += chunk;
736
+
737
+ if (streamBuffer.length > MAX_STREAM_BUFFER_SIZE) {
738
+ controller.enqueue(encoder.encode("<!-- flare: stream buffer limit exceeded, aborting render -->"));
739
+ controller.close();
740
+ return;
741
+ }
742
+
743
+ if (!streamDoctypeInjected && streamBuffer.includes("<html")) {
744
+ streamDoctypeInjected = true;
745
+ streamBuffer = streamBuffer.replace("<html", "<!DOCTYPE html><html");
746
+ }
747
+
748
+ if (!streamHeadInjected && streamBuffer.includes("</head>")) {
749
+ streamHeadInjected = true;
750
+ /*
751
+ * Inject _$HY init at end of <head> so it runs before body content.
752
+ * Must NOT be after <body> tag — full-document hydration walks body.firstChild,
753
+ * and an injected script there would break the DOM traversal.
754
+ * Passed as extraSuffix to avoid scanning buffer for </head> twice.
755
+ */
756
+ const hySuffix = hyInjected ? "" : hyInit;
757
+ hyInjected = true;
758
+ streamBuffer = injectHeadContent(
759
+ streamBuffer,
760
+ config,
761
+ capturedStyles,
762
+ hySuffix,
763
+ resolvedDevSxCss,
764
+ resolvedDevSxClasses,
765
+ );
766
+ }
767
+
768
+ if (!streamBodyInjected && streamBuffer.includes("</body>")) {
769
+ streamBodyInjected = true;
770
+ /* QC serialization deferred to here — queries populate during stream render */
771
+ const qcScript = config.queryClient ? serializeQueryClientCache(config.queryClient, config.nonce) : "";
772
+ streamBuffer = injectBodyContent(streamBuffer, config, flareState, qcScript);
773
+ }
774
+
775
+ if (streamHeadInjected && streamBodyInjected) {
776
+ controller.enqueue(encoder.encode(streamBuffer));
777
+ streamBuffer = "";
778
+ } else {
779
+ const safeLen = streamBuffer.length - 20;
780
+ if (safeLen > 0) {
781
+ controller.enqueue(encoder.encode(streamBuffer.slice(0, safeLen)));
782
+ streamBuffer = streamBuffer.slice(safeLen);
783
+ }
784
+ }
785
+ }
786
+
787
+ /* Flush remaining — use same helpers as loop to avoid duplication */
788
+ if (!streamHeadInjected) {
789
+ streamBuffer = injectHeadContent(
790
+ streamBuffer,
791
+ config,
792
+ capturedStyles,
793
+ "",
794
+ resolvedDevSxCss,
795
+ resolvedDevSxClasses,
796
+ );
797
+ }
798
+ if (!streamBodyInjected) {
799
+ const qcFlush = config.queryClient ? serializeQueryClientCache(config.queryClient, config.nonce) : "";
800
+ streamBuffer = injectBodyContent(streamBuffer, config, flareState, qcFlush);
801
+ }
802
+ if (streamBuffer.length > 0) {
803
+ controller.enqueue(encoder.encode(streamBuffer));
804
+ }
805
+
806
+ /* Stream deferred resolution scripts after HTML */
807
+ if (config.deferContexts && config.deferContexts.size > 0) {
808
+ /* Install QC tracking so deferred callbacks' setQueryData calls are captured */
809
+ let trackedQC: { drain(): Array<{ data: unknown; key: unknown[]; staleTime?: number }> } | undefined;
810
+ if (config.queryClient) {
811
+ const { createTrackedQueryClient } = await import("../query-client");
812
+ const typed = config.queryClient as Parameters<typeof createTrackedQueryClient>[0];
813
+ trackedQC = createTrackedQueryClient(typed);
814
+ }
815
+
816
+ const entries: DeferredEntry[] = [];
817
+ for (const ctx of config.deferContexts.values()) {
818
+ for (const e of ctx.entries()) {
819
+ entries.push(e);
820
+ }
821
+ }
822
+ const escapedNonce = escapeAttr(config.nonce);
823
+ const esc = (s: string) => JSON.stringify(s).replace(/</g, "\\u003c");
824
+ await Promise.allSettled(
825
+ entries.map(async (entry) => {
826
+ const resolverKey = esc(`${entry.matchId}:${entry.key}`);
827
+ try {
828
+ const data = await entry.promise;
829
+ const json = JSON.stringify(data).replace(/</g, "\\u003c");
830
+ controller.enqueue(
831
+ encoder.encode(
832
+ `<script nonce="${escapedNonce}">(self.__flare_q=self.__flare_q||[]).push([${resolverKey},${json}])</script>`,
833
+ ),
834
+ );
835
+ } catch (err) {
836
+ const msg = err instanceof Error ? err.message : String(err);
837
+ const safeMsg = esc(msg);
838
+ controller.enqueue(
839
+ encoder.encode(
840
+ `<script nonce="${escapedNonce}">(self.__flare_q=self.__flare_q||[]).push([${resolverKey},${safeMsg},true])</script>`,
841
+ ),
842
+ );
843
+ }
844
+
845
+ /* Stream QC delta entries accumulated during this deferred resolve */
846
+ if (trackedQC) {
847
+ const qcEntries = trackedQC.drain();
848
+ for (const qcEntry of qcEntries) {
849
+ const qcJson = JSON.stringify(qcEntry).replace(/</g, "\\u003c");
850
+ controller.enqueue(
851
+ encoder.encode(
852
+ `<script nonce="${escapedNonce}">(self.__flare_qc=self.__flare_qc||[]).push([${qcJson}])</script>`,
853
+ ),
854
+ );
855
+ }
856
+ }
857
+ }),
858
+ );
859
+ }
860
+
861
+ controller.close();
862
+ } catch (e) {
863
+ controller.error(e);
864
+ } finally {
865
+ reader.releaseLock();
866
+ }
867
+ },
868
+ });
869
+
870
+ const status = deriveStatus(config.matches);
871
+
872
+ const lastMatch = config.matches[config.matches.length - 1];
873
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
874
+ if (lastMatch?.responseHeaders) {
875
+ applyResponseHeaders(headers, lastMatch.responseHeaders);
876
+ }
877
+
878
+ return { body, headers, status };
879
+ }