gemi 0.122.0 → 0.123.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 (297) hide show
  1. package/dist/ai/index.js +2 -2
  2. package/dist/ai/index.js.map +1 -1
  3. package/dist/app/App.d.ts +9 -0
  4. package/dist/app/App.d.ts.map +1 -1
  5. package/dist/app/index.js +1 -1
  6. package/dist/auth/AuthController.d.ts.map +1 -1
  7. package/dist/bin/gemi.js +4 -1
  8. package/dist/bin/gemi.js.map +3 -3
  9. package/dist/bun/plugin.js +1 -1
  10. package/dist/bun/preload.js +1 -1
  11. package/dist/{chunk-dwedx1jb.js → chunk-08fcqny0.js} +3 -3
  12. package/dist/{chunk-dwedx1jb.js.map → chunk-08fcqny0.js.map} +1 -1
  13. package/dist/{chunk-vx7hvyvq.js → chunk-0qmc7snf.js} +2 -2
  14. package/dist/{chunk-vx7hvyvq.js.map → chunk-0qmc7snf.js.map} +1 -1
  15. package/dist/{chunk-qy8c0gzr.js → chunk-0tc0j3pf.js} +2 -2
  16. package/dist/{chunk-qy8c0gzr.js.map → chunk-0tc0j3pf.js.map} +1 -1
  17. package/dist/{chunk-meg8rtr6.js → chunk-1f1sb3w7.js} +2 -2
  18. package/dist/{chunk-meg8rtr6.js.map → chunk-1f1sb3w7.js.map} +1 -1
  19. package/dist/{chunk-d5f1m3d2.js → chunk-1kdga1b2.js} +2 -2
  20. package/dist/{chunk-d5f1m3d2.js.map → chunk-1kdga1b2.js.map} +1 -1
  21. package/dist/{chunk-48gmyaxc.js → chunk-2amj1eyp.js} +2 -2
  22. package/dist/{chunk-48gmyaxc.js.map → chunk-2amj1eyp.js.map} +1 -1
  23. package/dist/chunk-2vc72qez.js +9 -0
  24. package/dist/chunk-2vc72qez.js.map +10 -0
  25. package/dist/{chunk-9xyxbxgk.js → chunk-30xxc6r7.js} +2 -2
  26. package/dist/{chunk-9xyxbxgk.js.map → chunk-30xxc6r7.js.map} +1 -1
  27. package/dist/chunk-39dx7xrz.js +10 -0
  28. package/dist/{chunk-dyp0wg9e.js.map → chunk-39dx7xrz.js.map} +8 -4
  29. package/dist/{chunk-dhbgrv58.js → chunk-4qpypa3h.js} +1 -1
  30. package/dist/{chunk-wqe47c0j.js → chunk-52xkw2bs.js} +2 -2
  31. package/dist/{chunk-wqe47c0j.js.map → chunk-52xkw2bs.js.map} +1 -1
  32. package/dist/{chunk-3gsv1vv1.js → chunk-53hc0ah1.js} +2 -2
  33. package/dist/{chunk-3gsv1vv1.js.map → chunk-53hc0ah1.js.map} +1 -1
  34. package/dist/{chunk-qsz9241c.js → chunk-58ffbwdc.js} +2 -2
  35. package/dist/{chunk-qsz9241c.js.map → chunk-58ffbwdc.js.map} +1 -1
  36. package/dist/{chunk-0ejjgzwc.js → chunk-5a4qbtez.js} +2 -2
  37. package/dist/{chunk-0ejjgzwc.js.map → chunk-5a4qbtez.js.map} +1 -1
  38. package/dist/{chunk-7mmw0jr2.js → chunk-675kzd7z.js} +2 -2
  39. package/dist/{chunk-7mmw0jr2.js.map → chunk-675kzd7z.js.map} +1 -1
  40. package/dist/{chunk-dedqr5wb.js → chunk-6gjvqtcp.js} +2 -2
  41. package/dist/{chunk-dedqr5wb.js.map → chunk-6gjvqtcp.js.map} +1 -1
  42. package/dist/{chunk-6yhpprzs.js → chunk-6pxavyh3.js} +2 -2
  43. package/dist/{chunk-6yhpprzs.js.map → chunk-6pxavyh3.js.map} +1 -1
  44. package/dist/{chunk-h43jp8ws.js → chunk-73v34hw8.js} +2 -2
  45. package/dist/{chunk-h43jp8ws.js.map → chunk-73v34hw8.js.map} +1 -1
  46. package/dist/{chunk-wjkh2gem.js → chunk-8x73yyfs.js} +1 -1
  47. package/dist/{chunk-yds7zwc3.js → chunk-96a1459v.js} +2 -2
  48. package/dist/{chunk-yds7zwc3.js.map → chunk-96a1459v.js.map} +1 -1
  49. package/dist/{chunk-d2c3947c.js → chunk-9a9ef0f1.js} +2 -2
  50. package/dist/{chunk-d2c3947c.js.map → chunk-9a9ef0f1.js.map} +1 -1
  51. package/dist/{chunk-ep0baa9g.js → chunk-9p7eq7bz.js} +2 -2
  52. package/dist/{chunk-ep0baa9g.js.map → chunk-9p7eq7bz.js.map} +1 -1
  53. package/dist/chunk-9t08wbkd.js +58 -0
  54. package/dist/{chunk-3rvn9a76.js.map → chunk-9t08wbkd.js.map} +3 -3
  55. package/dist/{chunk-pn2x59ge.js → chunk-aafanq8n.js} +3 -3
  56. package/dist/{chunk-pn2x59ge.js.map → chunk-aafanq8n.js.map} +1 -1
  57. package/dist/{chunk-tewhms9w.js → chunk-ay8nat73.js} +2 -2
  58. package/dist/{chunk-tewhms9w.js.map → chunk-ay8nat73.js.map} +1 -1
  59. package/dist/{chunk-shfe0b22.js → chunk-bk2aheea.js} +2 -2
  60. package/dist/{chunk-shfe0b22.js.map → chunk-bk2aheea.js.map} +1 -1
  61. package/dist/{chunk-mr94nfp1.js → chunk-d0wr9jk4.js} +3 -3
  62. package/dist/{chunk-mr94nfp1.js.map → chunk-d0wr9jk4.js.map} +1 -1
  63. package/dist/chunk-d3ywmeb7.js +4 -0
  64. package/dist/chunk-d3ywmeb7.js.map +10 -0
  65. package/dist/{chunk-zefs2b6h.js → chunk-de7hxe73.js} +2 -2
  66. package/dist/{chunk-zefs2b6h.js.map → chunk-de7hxe73.js.map} +1 -1
  67. package/dist/{chunk-er8acmch.js → chunk-dmj6ysym.js} +2 -2
  68. package/dist/{chunk-er8acmch.js.map → chunk-dmj6ysym.js.map} +1 -1
  69. package/dist/{chunk-rhek27sj.js → chunk-ebx5hnqn.js} +3 -3
  70. package/dist/{chunk-rhek27sj.js.map → chunk-ebx5hnqn.js.map} +1 -1
  71. package/dist/chunk-eevna943.js +5 -0
  72. package/dist/chunk-eevna943.js.map +10 -0
  73. package/dist/{chunk-9rr5h818.js → chunk-enta97jb.js} +3 -3
  74. package/dist/{chunk-9rr5h818.js.map → chunk-enta97jb.js.map} +1 -1
  75. package/dist/{chunk-s5k3yq6t.js → chunk-f1thqe6j.js} +2 -2
  76. package/dist/{chunk-s5k3yq6t.js.map → chunk-f1thqe6j.js.map} +1 -1
  77. package/dist/{chunk-zrcd9hxr.js → chunk-fsy344zb.js} +2 -2
  78. package/dist/{chunk-zrcd9hxr.js.map → chunk-fsy344zb.js.map} +1 -1
  79. package/dist/{chunk-gemzbw7f.js → chunk-fxxbwyce.js} +2 -2
  80. package/dist/{chunk-gemzbw7f.js.map → chunk-fxxbwyce.js.map} +1 -1
  81. package/dist/{chunk-1wnnkde0.js → chunk-fyk07w01.js} +3 -3
  82. package/dist/{chunk-1wnnkde0.js.map → chunk-fyk07w01.js.map} +1 -1
  83. package/dist/chunk-gvp6brdt.js +57 -0
  84. package/dist/{chunk-3s21p9n2.js.map → chunk-gvp6brdt.js.map} +19 -9
  85. package/dist/{chunk-7g649j8h.js → chunk-gwnmgkq5.js} +2 -2
  86. package/dist/{chunk-7g649j8h.js.map → chunk-gwnmgkq5.js.map} +1 -1
  87. package/dist/{chunk-51w5ktv2.js → chunk-h76mddt3.js} +2 -2
  88. package/dist/{chunk-51w5ktv2.js.map → chunk-h76mddt3.js.map} +1 -1
  89. package/dist/{chunk-nz8z8jr1.js → chunk-hnrzm5pf.js} +3 -3
  90. package/dist/{chunk-nz8z8jr1.js.map → chunk-hnrzm5pf.js.map} +1 -1
  91. package/dist/{chunk-3wnghdsx.js → chunk-hs88b8gt.js} +2 -2
  92. package/dist/{chunk-3wnghdsx.js.map → chunk-hs88b8gt.js.map} +1 -1
  93. package/dist/{chunk-0k02tza1.js → chunk-j0cnsgjq.js} +1 -1
  94. package/dist/{chunk-pemhmjzp.js → chunk-j8vc3bze.js} +3 -3
  95. package/dist/{chunk-pemhmjzp.js.map → chunk-j8vc3bze.js.map} +1 -1
  96. package/dist/{chunk-471htqmz.js → chunk-k17wx7gz.js} +2 -2
  97. package/dist/{chunk-471htqmz.js.map → chunk-k17wx7gz.js.map} +1 -1
  98. package/dist/{chunk-m83zm86m.js → chunk-kc924zjz.js} +2 -2
  99. package/dist/{chunk-m83zm86m.js.map → chunk-kc924zjz.js.map} +1 -1
  100. package/dist/{chunk-5nj94s66.js → chunk-kdjhgtp9.js} +4 -4
  101. package/dist/{chunk-5nj94s66.js.map → chunk-kdjhgtp9.js.map} +1 -1
  102. package/dist/{chunk-32n8t8x6.js → chunk-kvas36p6.js} +2 -2
  103. package/dist/{chunk-32n8t8x6.js.map → chunk-kvas36p6.js.map} +1 -1
  104. package/dist/{chunk-4fx8p02c.js → chunk-kzstewtm.js} +2 -2
  105. package/dist/{chunk-4fx8p02c.js.map → chunk-kzstewtm.js.map} +1 -1
  106. package/dist/{chunk-gc2dz11k.js → chunk-mxbtcaed.js} +2 -2
  107. package/dist/{chunk-gc2dz11k.js.map → chunk-mxbtcaed.js.map} +1 -1
  108. package/dist/{chunk-cca0grq3.js → chunk-n1a2zj5z.js} +2 -2
  109. package/dist/{chunk-cca0grq3.js.map → chunk-n1a2zj5z.js.map} +1 -1
  110. package/dist/{chunk-0yj0s1pn.js → chunk-nf608sb1.js} +2 -2
  111. package/dist/{chunk-0yj0s1pn.js.map → chunk-nf608sb1.js.map} +1 -1
  112. package/dist/{chunk-19dpzp4y.js → chunk-nvngb4ed.js} +3 -3
  113. package/dist/{chunk-19dpzp4y.js.map → chunk-nvngb4ed.js.map} +1 -1
  114. package/dist/{chunk-cskqet01.js → chunk-pfm9ngch.js} +2 -2
  115. package/dist/{chunk-cskqet01.js.map → chunk-pfm9ngch.js.map} +1 -1
  116. package/dist/{chunk-fenc5tpv.js → chunk-q1e3q01h.js} +2 -2
  117. package/dist/{chunk-fenc5tpv.js.map → chunk-q1e3q01h.js.map} +1 -1
  118. package/dist/{chunk-2c0s0561.js → chunk-qen6587t.js} +2 -2
  119. package/dist/{chunk-2c0s0561.js.map → chunk-qen6587t.js.map} +1 -1
  120. package/dist/{chunk-r01aqf0q.js → chunk-r5tadf14.js} +2 -2
  121. package/dist/{chunk-r01aqf0q.js.map → chunk-r5tadf14.js.map} +1 -1
  122. package/dist/{chunk-cvr5624r.js → chunk-rjm1kj8n.js} +1 -1
  123. package/dist/{chunk-jaa6xkeh.js → chunk-rjtmgbay.js} +2 -2
  124. package/dist/{chunk-jaa6xkeh.js.map → chunk-rjtmgbay.js.map} +1 -1
  125. package/dist/{chunk-rhvc1axc.js → chunk-rkgsvsrj.js} +2 -2
  126. package/dist/{chunk-rhvc1axc.js.map → chunk-rkgsvsrj.js.map} +1 -1
  127. package/dist/{chunk-m53fgf20.js → chunk-s5jxc63j.js} +3 -3
  128. package/dist/{chunk-m53fgf20.js.map → chunk-s5jxc63j.js.map} +1 -1
  129. package/dist/{chunk-5tdxbwzg.js → chunk-sxa70wgn.js} +3 -3
  130. package/dist/{chunk-5tdxbwzg.js.map → chunk-sxa70wgn.js.map} +1 -1
  131. package/dist/chunk-tazvfppr.js +5 -0
  132. package/dist/{chunk-htgmy3x7.js.map → chunk-tazvfppr.js.map} +1 -1
  133. package/dist/{chunk-gy81nd9a.js → chunk-vd6zn8c4.js} +2 -2
  134. package/dist/{chunk-gy81nd9a.js.map → chunk-vd6zn8c4.js.map} +1 -1
  135. package/dist/chunk-vpgbn494.js +24 -0
  136. package/dist/{chunk-2yw4yezc.js.map → chunk-vpgbn494.js.map} +3 -3
  137. package/dist/{chunk-7ftj43vp.js → chunk-vwnd4jb5.js} +2 -2
  138. package/dist/{chunk-7ftj43vp.js.map → chunk-vwnd4jb5.js.map} +1 -1
  139. package/dist/{chunk-c0v61fp5.js → chunk-w0jfqnw9.js} +2 -2
  140. package/dist/{chunk-c0v61fp5.js.map → chunk-w0jfqnw9.js.map} +1 -1
  141. package/dist/{chunk-65beps2q.js → chunk-w61sphn5.js} +3 -3
  142. package/dist/{chunk-65beps2q.js.map → chunk-w61sphn5.js.map} +1 -1
  143. package/dist/{chunk-zg6hmc48.js → chunk-wqharhrp.js} +1 -1
  144. package/dist/{chunk-sx340k85.js → chunk-x0jahbqj.js} +2 -2
  145. package/dist/{chunk-sx340k85.js.map → chunk-x0jahbqj.js.map} +1 -1
  146. package/dist/{chunk-y6wt5b6t.js → chunk-xhmtph39.js} +1 -1
  147. package/dist/{chunk-c7qs1xp3.js → chunk-y67evsdy.js} +2 -2
  148. package/dist/{chunk-c7qs1xp3.js.map → chunk-y67evsdy.js.map} +1 -1
  149. package/dist/{chunk-krk7mcgm.js → chunk-ycsrpf47.js} +3 -3
  150. package/dist/{chunk-krk7mcgm.js.map → chunk-ycsrpf47.js.map} +1 -1
  151. package/dist/{chunk-9d2d08qm.js → chunk-ywgvxx35.js} +2 -2
  152. package/dist/{chunk-9d2d08qm.js.map → chunk-ywgvxx35.js.map} +1 -1
  153. package/dist/{chunk-4acpw92g.js → chunk-z23h4s1a.js} +3 -3
  154. package/dist/{chunk-4acpw92g.js.map → chunk-z23h4s1a.js.map} +1 -1
  155. package/dist/{chunk-tk135gmj.js → chunk-zjej5vqh.js} +3 -3
  156. package/dist/{chunk-tk135gmj.js.map → chunk-zjej5vqh.js.map} +1 -1
  157. package/dist/chunks/{ThemeProvider-BLI4zpLu.js → ThemeProvider-CgK2mmVV.js} +576 -2
  158. package/dist/chunks/ThemeProvider-CgK2mmVV.js.map +1 -0
  159. package/dist/client/auth/useEmailCode.d.ts.map +1 -1
  160. package/dist/client/auth/useSignIn.d.ts.map +1 -1
  161. package/dist/client/auth/useSignOut.d.ts.map +1 -1
  162. package/dist/client/index.d.ts +5 -1
  163. package/dist/client/index.d.ts.map +1 -1
  164. package/dist/client/index.js +216 -8
  165. package/dist/client/index.js.map +1 -1
  166. package/dist/client/init.d.ts +8 -0
  167. package/dist/client/init.d.ts.map +1 -1
  168. package/dist/client/realtime/FakeSocket.d.ts +47 -0
  169. package/dist/client/realtime/FakeSocket.d.ts.map +1 -0
  170. package/dist/client/realtime/RealtimeClient.d.ts +162 -0
  171. package/dist/client/realtime/RealtimeClient.d.ts.map +1 -0
  172. package/dist/client/realtime/RealtimeContext.d.ts +8 -0
  173. package/dist/client/realtime/RealtimeContext.d.ts.map +1 -0
  174. package/dist/client/realtime/useChannelSubscription.d.ts +32 -0
  175. package/dist/client/realtime/useChannelSubscription.d.ts.map +1 -0
  176. package/dist/client/rpc.d.ts +11 -0
  177. package/dist/client/rpc.d.ts.map +1 -1
  178. package/dist/client/useChannel.d.ts +87 -0
  179. package/dist/client/useChannel.d.ts.map +1 -0
  180. package/dist/client/useMutation.d.ts.map +1 -1
  181. package/dist/client/useQuery.d.ts +13 -0
  182. package/dist/client/useQuery.d.ts.map +1 -1
  183. package/dist/config/index.js +1 -1
  184. package/dist/console/run.js +2 -2
  185. package/dist/console/run.js.map +1 -1
  186. package/dist/container/index.js +2 -2
  187. package/dist/container/index.js.map +1 -1
  188. package/dist/database/index.js +2 -2
  189. package/dist/database/index.js.map +1 -1
  190. package/dist/email/index.js +2 -2
  191. package/dist/email/index.js.map +1 -1
  192. package/dist/facades/Broadcast.d.ts +68 -0
  193. package/dist/facades/Broadcast.d.ts.map +1 -0
  194. package/dist/facades/index.d.ts +1 -0
  195. package/dist/facades/index.d.ts.map +1 -1
  196. package/dist/facades/index.js +2 -2
  197. package/dist/facades/index.js.map +1 -1
  198. package/dist/foundation/index.js +2 -2
  199. package/dist/foundation/index.js.map +1 -1
  200. package/dist/gemi.d.ts +19 -6
  201. package/dist/http/ChannelRouter.d.ts +171 -0
  202. package/dist/http/ChannelRouter.d.ts.map +1 -0
  203. package/dist/http/index.d.ts +1 -0
  204. package/dist/http/index.d.ts.map +1 -1
  205. package/dist/http/index.js +2 -2
  206. package/dist/http/index.js.map +1 -1
  207. package/dist/i18n/dictionaryRuntime.js +2 -2
  208. package/dist/i18n/dictionaryRuntime.js.map +1 -1
  209. package/dist/i18n/index.js +2 -2
  210. package/dist/i18n/index.js.map +1 -1
  211. package/dist/kernel/Kernel.d.ts +6 -0
  212. package/dist/kernel/Kernel.d.ts.map +1 -1
  213. package/dist/kernel/index.js +3 -3
  214. package/dist/kernel/index.js.map +5 -6
  215. package/dist/kernel/providers.d.ts +2 -2
  216. package/dist/kernel/providers.d.ts.map +1 -1
  217. package/dist/orm/context.d.ts.map +1 -1
  218. package/dist/orm/index.js +2 -2
  219. package/dist/orm/index.js.map +1 -1
  220. package/dist/server/Server.d.ts.map +1 -1
  221. package/dist/server/httpDev.d.ts +1 -2
  222. package/dist/server/httpDev.d.ts.map +1 -1
  223. package/dist/server/httpProd.d.ts +1 -1
  224. package/dist/server/httpProd.d.ts.map +1 -1
  225. package/dist/server/index.js +2 -2
  226. package/dist/server/index.js.map +3 -3
  227. package/dist/server/shutdown.d.ts +9 -0
  228. package/dist/server/shutdown.d.ts.map +1 -1
  229. package/dist/services/broadcast/BroadcastDriver.d.ts +73 -0
  230. package/dist/services/broadcast/BroadcastDriver.d.ts.map +1 -0
  231. package/dist/services/broadcast/BroadcastEvent.d.ts +67 -0
  232. package/dist/services/broadcast/BroadcastEvent.d.ts.map +1 -0
  233. package/dist/services/broadcast/BroadcastManager.d.ts +148 -0
  234. package/dist/services/broadcast/BroadcastManager.d.ts.map +1 -0
  235. package/dist/services/broadcast/BroadcastServiceProvider.d.ts +16 -0
  236. package/dist/services/broadcast/BroadcastServiceProvider.d.ts.map +1 -0
  237. package/dist/services/broadcast/FakeBroadcastManager.d.ts +45 -0
  238. package/dist/services/broadcast/FakeBroadcastManager.d.ts.map +1 -0
  239. package/dist/services/broadcast/MemoryBroadcastDriver.d.ts +23 -0
  240. package/dist/services/broadcast/MemoryBroadcastDriver.d.ts.map +1 -0
  241. package/dist/services/broadcast/SocketHub.d.ts +192 -0
  242. package/dist/services/broadcast/SocketHub.d.ts.map +1 -0
  243. package/dist/services/broadcast/brand.d.ts +16 -0
  244. package/dist/services/broadcast/brand.d.ts.map +1 -0
  245. package/dist/services/broadcast/channels.d.ts +77 -0
  246. package/dist/services/broadcast/channels.d.ts.map +1 -0
  247. package/dist/services/broadcast/config.d.ts +93 -0
  248. package/dist/services/broadcast/config.d.ts.map +1 -0
  249. package/dist/services/broadcast/protocol.d.ts +123 -0
  250. package/dist/services/broadcast/protocol.d.ts.map +1 -0
  251. package/dist/services/broadcast/socketTag.d.ts +17 -0
  252. package/dist/services/broadcast/socketTag.d.ts.map +1 -0
  253. package/dist/services/broadcast/types.d.ts +27 -0
  254. package/dist/services/broadcast/types.d.ts.map +1 -0
  255. package/dist/services/broadcast/wire.d.ts +35 -0
  256. package/dist/services/broadcast/wire.d.ts.map +1 -0
  257. package/dist/services/events/EventManager.d.ts +6 -0
  258. package/dist/services/events/EventManager.d.ts.map +1 -1
  259. package/dist/services/index.d.ts +9 -0
  260. package/dist/services/index.d.ts.map +1 -1
  261. package/dist/services/index.js +9 -9
  262. package/dist/services/index.js.map +4 -3
  263. package/dist/services/queue/work.js +2 -2
  264. package/dist/services/queue/work.js.map +1 -1
  265. package/dist/services/router/config.d.ts +7 -0
  266. package/dist/services/router/config.d.ts.map +1 -1
  267. package/dist/stats/route-table.js +1 -1
  268. package/dist/support/index.js +2 -2
  269. package/dist/support/index.js.map +1 -1
  270. package/dist/testing/Page.d.ts +8 -0
  271. package/dist/testing/Page.d.ts.map +1 -1
  272. package/dist/testing/index.d.ts +2 -0
  273. package/dist/testing/index.d.ts.map +1 -1
  274. package/dist/testing/index.js +174 -8
  275. package/dist/testing/index.js.map +1 -1
  276. package/dist/vitest/index.js +1 -1
  277. package/package.json +1 -1
  278. package/skills/gemi-react-best-practices/SKILL.md +3 -1
  279. package/skills/gemi-react-best-practices/rules/query-live-over-polling.md +56 -0
  280. package/dist/chunk-2n3pgfnb.js +0 -5
  281. package/dist/chunk-2n3pgfnb.js.map +0 -10
  282. package/dist/chunk-2yw4yezc.js +0 -24
  283. package/dist/chunk-3rvn9a76.js +0 -58
  284. package/dist/chunk-3s21p9n2.js +0 -57
  285. package/dist/chunk-dyp0wg9e.js +0 -10
  286. package/dist/chunk-htgmy3x7.js +0 -5
  287. package/dist/chunk-krtybgz1.js +0 -4
  288. package/dist/chunk-krtybgz1.js.map +0 -10
  289. package/dist/chunk-s07v65x6.js +0 -9
  290. package/dist/chunk-s07v65x6.js.map +0 -10
  291. package/dist/chunks/ThemeProvider-BLI4zpLu.js.map +0 -1
  292. /package/dist/{chunk-dhbgrv58.js.map → chunk-4qpypa3h.js.map} +0 -0
  293. /package/dist/{chunk-wjkh2gem.js.map → chunk-8x73yyfs.js.map} +0 -0
  294. /package/dist/{chunk-0k02tza1.js.map → chunk-j0cnsgjq.js.map} +0 -0
  295. /package/dist/{chunk-cvr5624r.js.map → chunk-rjm1kj8n.js.map} +0 -0
  296. /package/dist/{chunk-zg6hmc48.js.map → chunk-wqharhrp.js.map} +0 -0
  297. /package/dist/{chunk-y6wt5b6t.js.map → chunk-xhmtph39.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../testing/Page.tsx","../../internal/islandRuntime.ts","../../internal/staticNavigationRuntime.ts","../../testing/staticDocument.ts"],"sourcesContent":["import {\n Suspense,\n useContext,\n useMemo,\n useRef,\n useState,\n type ComponentType,\n type PropsWithChildren,\n type ReactNode,\n} from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\nimport { Action, createMemoryHistory } from \"history\";\n\nimport type { User } from \"../auth/types\";\nimport { ClientRouterContext } from \"../client/ClientRouterContext\";\nimport { I18nProvider } from \"../client/I18nContext\";\nimport { ProgressManager } from \"../client/ProgressManager\";\nimport {\n QueryManagerContext,\n QueryManagerProvider,\n type QueryConfig,\n} from \"../client/QueryManagerContext\";\nimport {\n RouteStateProvider,\n type PageData,\n type RouteState,\n} from \"../client/RouteStateContext\";\nimport { RouteTransitionProvider } from \"../client/RouteTransitionProvider\";\nimport {\n ServerDataContext,\n type ServerDataContextValue,\n} from \"../client/ServerDataProvider\";\nimport { ThemeProvider } from \"../client/ThemeProvider\";\nimport type { ClientFeatureKey } from \"../client/rpc\";\nimport type { Breadcrumb } from \"../client/useBreadcrumbs\";\nimport { applyParams } from \"../utils/applyParams\";\nimport { Subject } from \"../utils/Subject\";\nimport { toVariantKey } from \"../utils/variantKey\";\n\n/**\n * A dictionary as `Dictionary.create` produces it: a name and a\n * `{ key: { locale: string } }` map.\n *\n * Typed structurally rather than as `Dictionary<T>` for two reasons. It keeps\n * `gemi/testing` from importing `gemi/i18n` — a server entry, which reaches the\n * `Lang` facade and through it the container. And it means an object literal of\n * the same shape is accepted, so a test never *has* to import the app's\n * dictionaries at all.\n *\n * Only `name` and `dictionary` are read — both plain data. `Dictionary`'s\n * server-only methods (`render`, `reference`, which throw once `window` is\n * defined) are never called from here.\n */\nexport interface PageDictionary {\n name: string;\n dictionary: Record<string, Record<string, string>>;\n}\n\nexport interface PageProps {\n /**\n * The URL the view is mounted at. A template (`/app/:orgId/chat`) is resolved\n * against `params`, so the same string can be pasted from the router.\n */\n pathname?: string;\n /** Route params, as the router would have parsed them out of `pathname`. */\n params?: Record<string, string>;\n /** `\"?tab=recent\"`, `\"tab=recent\"`, or `{ tab: \"recent\" }`. */\n searchParams?: string | Record<string, string | number | boolean>;\n hash?: string;\n /** The locale `useTranslator` and `useLocale` report. */\n locale?: string;\n /**\n * The app's default locale. Links and `useNavigate` omit the locale segment\n * for it, so leaving this equal to `locale` (the default) keeps hrefs\n * unprefixed — set it to seed a page being viewed in a non-default locale.\n */\n defaultLocale?: string;\n supportedLocales?: string[];\n /** The zone `useFormatter` formats dates in. Defaults to `UTC`, like an app with no `translation.timeZone`. */\n timeZone?: string;\n /**\n * Dictionaries the components under test translate against — the app's own,\n * or literals of the same shape.\n *\n * Note what importing the app's costs: `app/i18n/index.ts` imports\n * `gemi/i18n`, a server entry, so the test file pulls the container and\n * `node:async_hooks` into its module graph. Nothing there runs (and nothing\n * here calls it), so it is fine under any runner that has Node builtins —\n * vitest, `bun test` — and not under one that renders in a real browser. Use\n * `translations` there, which is the shape the client actually receives.\n */\n dictionaries?: PageDictionary[];\n /**\n * Translations for the current locale, already resolved: dictionary name →\n * key → string. This is the shape the server serializes onto the page, so it\n * is what the client reads at runtime — and it imports nothing.\n *\n * ```tsx\n * <Page translations={{ Chat: { greeting: \"Hello {{name}}\" } }}>\n * ```\n *\n * Merged over `dictionaries`, so the two compose: seed from the app's real\n * dictionary and override the one key a test is about.\n */\n translations?: Record<string, Record<string, string>>;\n /**\n * Data `useQuery` finds already cached, keyed by API path — the path passed\n * to `useQuery`, without the `/api` prefix the client adds when it fetches.\n * A key may carry a query string (`\"/lists?page=2\"`) to seed one search\n * variant.\n *\n * The cache is keyed by the *resolved* path, so a key's `:params` are\n * resolved against the page's `params` — and a key naming one the page does\n * not carry is an error, naming the key. That case is common enough to be\n * worth the loud failure: a query often takes its params from the call site\n * rather than the route (`useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state), and the right seed for it is the resolved\n * path, `\"/orgs/abc/lists\"`.\n *\n * Seeded at mount only: a component that mutates or refetches its data owns\n * the cache from then on.\n */\n queryData?: Record<string, unknown>;\n /** App-wide `useQuery` defaults, as `createRoot` threads them. */\n queryConfig?: QueryConfig;\n /**\n * What `useUser()` reports. Defaults to `null` — an anonymous visitor — and\n * either way the hook resolves from here without issuing a request.\n */\n user?: Partial<User> | null;\n /** What `useBreadcrumbs()` returns, in order. */\n breadcrumbs?: Breadcrumb[];\n /**\n * The `route.domains` group the page is served under, as `useDomain()`\n * reports it. Left out, `useDomain()` reads empty — the shape an app without\n * `route.domains` sees — and `useDomain().url()` throws, exactly as it does\n * there.\n *\n * `root` and `origin` default off the host, so the common case is one field:\n * `domain={{ host: \"acme.example.com\", params: { tenant: \"acme\" } }}`.\n */\n domain?: {\n host: string;\n group?: string;\n params?: Record<string, string>;\n custom?: boolean;\n root?: string;\n origin?: string;\n } | null;\n /**\n * What `useFeature()` reports, keyed by feature key. Anything left out reads\n * as off, exactly as an undeclared key does in a real request.\n *\n * A test seeds the *outcome*, not the declaration: `app/features` owns the\n * targeting and the rollout, and re-deriving those here would mean building a\n * request context — a user, a `session_id`, a bot verdict — to assert a\n * branch that only cares which way the feature came out. Cover the targeting\n * where it lives, with a server test.\n */\n features?: Partial<Record<ClientFeatureKey, boolean>>;\n /**\n * The theme `useTheme()` starts on. The app reads a visitor's stored choice\n * out of `localStorage`; a test has no session to have made one in, so this\n * seeds it without writing to storage. `setTheme` works from either.\n */\n theme?: \"light\" | \"dark\" | \"system\";\n /**\n * Fallback for the `Suspense` boundary wrapped around the children — the\n * stand-in for the view's own `Loading` export, which the real router\n * supplies. Defaults to `null`.\n */\n fallback?: ReactNode;\n /**\n * Renders in place of the children when one throws, mirroring a view's\n * `Error` export. Without it a thrown render (an errored suspense query,\n * say) propagates out of `render()` and fails the test, which is usually\n * what you want.\n */\n errorFallback?: ComponentType<FallbackProps>;\n /**\n * Called when something navigates — a `Link` click, `useNavigate().push`.\n * The navigation is reported, not performed: `<Page>` renders one route and\n * does not re-resolve the tree, so `useLocation()` keeps reporting the\n * pathname it was given.\n */\n onNavigate?: (href: string, action: \"push\" | \"replace\") => void;\n}\n\n/** `\"?a=b\"` (or `\"\"`) from any of the shapes `searchParams` accepts. */\nfunction toSearchString(search: PageProps[\"searchParams\"]): string {\n if (!search) return \"\";\n const query =\n typeof search === \"string\"\n ? search.replace(/^\\?/, \"\")\n : new URLSearchParams(\n Object.entries(search).map(([key, value]) => [key, String(value)]),\n ).toString();\n return query ? `?${query}` : \"\";\n}\n\n/**\n * The `:params` a seed key needs supplied. Wildcards (`:rest*`) are excluded —\n * `applyParams` drops those when missing, which is a resolution rather than a\n * failure.\n *\n * Optional params (`:id?`) cannot reach here at all: a key is split at its\n * first `?` to separate the search string, so `\"/lists/:folderId?\"` arrives as\n * the path `/lists/:folderId`. Seed the resolved path for those.\n */\nfunction requiredParamsOf(path: string): string[] {\n return Array.from(path.matchAll(/:([^/]+)/g), ([, name]) => name).filter(\n (name) => !name.endsWith(\"*\"),\n );\n}\n\n/**\n * Fails a `queryData` key whose `:params` the page does not carry, saying so\n * in terms of the key.\n *\n * Without this the two documented runners disagree, and neither is legible.\n * `applyParams` throws `Missing parameter: orgId` under `import.meta.env.DEV`\n * (vitest) — a router-utility stack trace out of `<Page>`'s render, before\n * anything mounts, naming nothing that appears in the test. Under `bun test`\n * that flag is falsy, so it logs and seeds the literal `/orgs/undefined/lists`\n * instead: the seed misses, the query goes to the network, and the assertion\n * runs against a loading state.\n *\n * The case is common because a query's params often come from the call site\n * rather than the route — `useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state. The cache is keyed by the *resolved* path,\n * so the way out is to seed that path directly.\n */\nfunction resolveSeedPath(\n key: string,\n rawPath: string,\n params: Record<string, string>,\n): string {\n const missing = requiredParamsOf(rawPath).filter(\n (name) => params[name] === undefined,\n );\n if (missing.length > 0) {\n throw new Error(\n `[gemi] <Page> cannot resolve the queryData key \"${key}\": no value for ` +\n `${missing.map((name) => `:${name}`).join(\", \")}. The query cache is ` +\n `keyed by the resolved path, so either add ${missing\n .map((name) => `\\`${name}\\``)\n .join(\", \")} to the page's \\`params\\`, or seed the resolved path ` +\n `(e.g. \"${applyParams(\n rawPath,\n Object.fromEntries(missing.map((name) => [name, \"123\"])),\n )}\") — which is what a query taking its params from component state ` +\n `reads under.`,\n );\n }\n return applyParams(rawPath, params);\n}\n\n/**\n * `queryData` in the shape the query cache seeds from: `{ [path]: { [variant]:\n * data } }`, the same payload `Query.prefetch` puts on a server-rendered page.\n */\nfunction toPrefetchedData(\n queryData: Record<string, unknown>,\n params: Record<string, string>,\n warned: Set<string>,\n) {\n const prefetchedData: Record<string, Record<string, unknown>> = {};\n for (const [key, data] of Object.entries(queryData)) {\n const [rawPath, rawSearch = \"\"] = key.split(\"?\");\n // Once per key per page, not once per render: this runs again on every\n // re-render, and a duplicated warning reads as a second mistake.\n if (rawPath.startsWith(\"/api/\") && !warned.has(key)) {\n warned.add(key);\n console.warn(\n `[gemi] queryData key \"${key}\" starts with /api. useQuery is keyed by ` +\n `the path you pass it — the /api prefix is added when it fetches — ` +\n `so this entry seeds a path no query reads.`,\n );\n }\n const path = resolveSeedPath(key, rawPath, params);\n prefetchedData[path] = {\n ...prefetchedData[path],\n [toVariantKey(rawSearch)]: data,\n };\n }\n return prefetchedData;\n}\n\n/**\n * `{ [locale]: { [dictionary]: { [key]: string } } }` — the payload\n * `Translator` serializes onto the page, which is the only form the client\n * ever sees. `dictionaries` are transposed into it (they are keyed the other\n * way round, by key then locale); `translations` is already in it and lands\n * under `locale`, last, so it wins.\n */\nfunction toClientDictionary(\n dictionaries: PageDictionary[],\n locales: string[],\n locale: string,\n translations: Record<string, Record<string, string>>,\n): Record<string, Record<string, Record<string, string>>> {\n const clientDictionary: Record<\n string,\n Record<string, Record<string, string>>\n > = {};\n for (const supported of locales) {\n clientDictionary[supported] ??= {};\n }\n for (const { name, dictionary } of dictionaries) {\n for (const [key, byLocale] of Object.entries(dictionary ?? {})) {\n for (const [dictLocale, translation] of Object.entries(byLocale ?? {})) {\n clientDictionary[dictLocale] ??= {};\n clientDictionary[dictLocale][name] ??= {};\n clientDictionary[dictLocale][name][key] = translation;\n }\n }\n }\n for (const [name, keys] of Object.entries(translations)) {\n clientDictionary[locale] ??= {};\n clientDictionary[locale][name] = {\n ...clientDictionary[locale][name],\n ...keys,\n };\n }\n return clientDictionary;\n}\n\n/**\n * Mounts a component with the inputs a view normally arrives with — route\n * params, the current locale and its dictionaries, prefetched query data, the\n * signed-in user — so a unit test can assert what it renders rather than only\n * its empty state.\n *\n * ```tsx\n * import { render, screen } from \"@testing-library/react\";\n * import { Page } from \"gemi/testing\";\n *\n * render(\n * <Page\n * pathname=\"/app/:orgId/chat\"\n * params={{ orgId: \"abc\" }}\n * queryData={{ \"/organizations/:orgId/messages\": [{ id: 1, body: \"hi\" }] }}\n * dictionaries={[ChatDictionary]}\n * >\n * <OrgChat />\n * </Page>,\n * );\n *\n * expect(screen.getByText(\"hi\")).toBeDefined();\n * ```\n *\n * It is a plain component, so it composes with any renderer — Testing\n * Library's `render`, `react-test-renderer`, or `renderToString`.\n *\n * Two things it does not do.\n *\n * It does not route: there is no view tree and no route manifest behind it, so\n * a navigation is reported through `onNavigate` rather than resolved. To\n * assert the page *after* one, render a second `<Page>`.\n *\n * And under `renderToString` it renders the server's own no-data branch for an\n * *unseeded* query rather than suspending — deliberately, because suspending\n * there is the streaming server's job: `createRoot` threads the request's\n * `ServerQueryStore` through `ServerQueryContext`, and that store is what runs\n * the route handler. A harness has no request and no handler to run, so there\n * is nothing to suspend on. Seeded queries render their data on the server\n * exactly as they do in the browser; unseeded ones behave as a route with no\n * `Query.prefetch` does, framework warning included.\n */\nexport const Page = (props: PropsWithChildren<PageProps>) => {\n const {\n children,\n pathname: routePath = \"/\",\n params = {},\n searchParams,\n hash = \"\",\n locale = \"en-US\",\n defaultLocale = locale,\n timeZone = \"UTC\",\n dictionaries = [],\n translations = {},\n queryData = {},\n queryConfig,\n user = null,\n breadcrumbs = [],\n domain = null,\n features = {},\n theme,\n fallback = null,\n errorFallback,\n onNavigate,\n } = props;\n\n // The caller's list, in the caller's order — a language switcher maps over\n // it, so hoisting the page's own locale to the front would reorder the\n // rendered options. The defaults are appended, only to guarantee presence.\n const supportedLocales = useMemo(\n () =>\n Array.from(\n new Set([...(props.supportedLocales ?? []), defaultLocale, locale]),\n ),\n [props.supportedLocales, defaultLocale, locale],\n );\n const pathname = applyParams(routePath, params) || \"/\";\n // `root` defaults to the host minus its first label, so a tenant subdomain\n // needs only its host; `origin` to https on that host. Either can be given\n // outright when the test is about the other.\n const domainValue = useMemo(() => {\n if (!domain) {\n return null;\n }\n const labels = domain.host.split(\".\");\n return {\n host: domain.host,\n group: domain.group ?? \"\",\n params: domain.params ?? {},\n custom: domain.custom ?? false,\n root:\n domain.root ?? (labels.length > 2 ? labels.slice(1).join(\".\") : domain.host),\n origin: domain.origin ?? `https://${domain.host}`,\n };\n }, [domain]);\n const search = toSearchString(searchParams);\n // The URL's locale segment, which the default locale does not get.\n const urlLocaleSegment = locale === defaultLocale ? null : locale;\n\n const dictionary = useMemo(\n () =>\n toClientDictionary(dictionaries, supportedLocales, locale, translations),\n [dictionaries, supportedLocales, locale, translations],\n );\n\n // Keys that have already drawn a warning, so a re-render does not repeat it.\n const warnedRef = useRef<Set<string>>(new Set());\n const prefetchedData = useMemo(() => {\n const seeded = toPrefetchedData(queryData, params, warnedRef.current);\n // `useUser` reads `/auth/me` like any other query. Seeding it — with `null`\n // for the anonymous default — is what keeps a component test off the\n // network for a hook nothing in the test asked for.\n seeded[\"/auth/me\"] ??= { \"\": user ?? null };\n return seeded;\n }, [queryData, params, user]);\n\n // Keyed by `${view}:${pathname}` in the router's cache; the view names are\n // ours to choose here, and only their order reaches `useBreadcrumbs`.\n const { breadcrumbViews, breadcrumbsCache, breadcrumbsRecord } =\n useMemo(() => {\n const views = breadcrumbs.map((_, index) => `breadcrumb-${index}`);\n const cache = new Map<string, Breadcrumb>(\n breadcrumbs.map((breadcrumb, index) => [\n `${views[index]}:${pathname}`,\n breadcrumb,\n ]),\n );\n return {\n breadcrumbViews: views,\n breadcrumbsCache: cache,\n breadcrumbsRecord: Object.fromEntries(cache),\n };\n }, [breadcrumbs, pathname]);\n\n const [isNavigatingSubject] = useState(() => new Subject<boolean>(false));\n const [progressManager] = useState(\n () => new ProgressManager(isNavigatingSubject),\n );\n const [history] = useState(() =>\n createMemoryHistory({ initialEntries: [`${pathname}${search}${hash}`] }),\n );\n\n const routeState: RouteState & PageData = useMemo(\n () => ({\n views: [],\n params,\n search,\n state: {},\n pathname,\n hash,\n action: null,\n routePath,\n locale: urlLocaleSegment,\n data: {},\n i18n: {\n currentLocale: locale,\n dictionary,\n supportedLocales,\n defaultLocale,\n timeZone,\n },\n prefetchedData,\n breadcrumbs: breadcrumbsRecord,\n features: features as Record<string, boolean>,\n appId: \"test\",\n }),\n [\n params,\n search,\n pathname,\n hash,\n routePath,\n urlLocaleSegment,\n locale,\n dictionary,\n supportedLocales,\n defaultLocale,\n timeZone,\n prefetchedData,\n breadcrumbsRecord,\n features,\n ],\n );\n\n const [routerSubject] = useState(() => new Subject<RouteState>(routeState));\n\n const onNavigateRef = useRef(onNavigate);\n onNavigateRef.current = onNavigate;\n // Subscribed during render, not in an effect, because React flushes child\n // effects before parent ones — and mount-time navigation is a real pattern:\n // `<Redirect>` pushes from its own mount effect, as does any auth guard. A\n // listener registered in `<Page>`'s effect is not there yet when those fire,\n // so `onNavigate` would silently miss exactly the redirects a test most\n // wants to assert. Ref-guarded, so React's double-invoke registers one.\n //\n // Nothing unsubscribes: this history is created per `<Page>` and reachable\n // from nowhere else, so it is collected with the page it belongs to.\n const listeningRef = useRef(false);\n if (!listeningRef.current) {\n listeningRef.current = true;\n history.listen(({ location, action }) => {\n const href = `${location.pathname}${location.search}${location.hash}`;\n onNavigateRef.current?.(\n href,\n action === Action.Replace ? \"replace\" : \"push\",\n );\n });\n }\n\n const serverData: ServerDataContextValue = useMemo(\n () => ({\n routeManifest: {},\n pageData: {},\n breadcrumbs: breadcrumbsRecord,\n prefetchedData,\n router: {\n pathname,\n params,\n currentPath: routePath,\n is404: false,\n searchParams: search,\n urlLocaleSegment,\n domain: domainValue,\n },\n i18n: {\n dictionary,\n currentLocale: locale,\n supportedLocales,\n defaultLocale,\n timeZone,\n },\n componentTree: [],\n auth: { user: user as User },\n features: features as Record<string, boolean>,\n __csrf: \"test-csrf-token\",\n cssManifest: {},\n modulePreloadManifest: {},\n meta: {},\n appId: \"test\",\n }),\n [\n breadcrumbsRecord,\n prefetchedData,\n pathname,\n params,\n routePath,\n search,\n urlLocaleSegment,\n dictionary,\n locale,\n supportedLocales,\n defaultLocale,\n timeZone,\n user,\n features,\n ],\n );\n\n // Everything the router hooks read, inert: nothing here fetches, preloads or\n // resolves a route, but every hook finds the shape it expects rather than an\n // empty context to crash on.\n const [viewEntriesSubject] = useState(() => new Subject<string[]>([]));\n const routerContext = useMemo(\n () => ({\n viewEntriesSubject,\n history,\n updatePageData: () => {},\n getPageData: () => ({}),\n getScrollPosition: () => 0,\n getViewPathsFromPathname: () => breadcrumbViews,\n getRoutePathnameFromHref: (href: string) => href,\n isNavigatingSubject,\n setNavigationAbortController: () => {},\n progressManager,\n fetchRouteCSS: async () => {},\n preloadRouteModules: () => {},\n prefetchRoute: async () => {},\n takePrefetched: () => null,\n clearPrefetchCache: () => {},\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n }),\n [\n viewEntriesSubject,\n history,\n breadcrumbViews,\n isNavigatingSubject,\n progressManager,\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n ],\n );\n\n return (\n <ThemeProvider theme={theme}>\n <ServerDataContext.Provider value={serverData}>\n <I18nProvider>\n <QueryManagerProvider queryConfig={queryConfig}>\n <ClientRouterContext.Provider value={routerContext}>\n <RouteTransitionProvider\n isPending={false}\n isFetching={false}\n transitionPath={[\"\", pathname]}\n >\n <RouteStateProvider state={routeState}>\n <Boundary\n errorFallback={errorFallback}\n fallback={fallback}\n resetKey={pathname}\n >\n {children}\n </Boundary>\n </RouteStateProvider>\n </RouteTransitionProvider>\n </ClientRouterContext.Provider>\n </QueryManagerProvider>\n </I18nProvider>\n </ServerDataContext.Provider>\n </ThemeProvider>\n );\n};\n\n/**\n * The `Suspense` boundary every view renders inside, and — when the test\n * supplies a fallback for it — the error boundary too.\n *\n * `onReset={clearErrors}` is the part that has to be here rather than inline\n * above: it needs `QueryManagerContext`, and it is what makes a view's real\n * `Error` export testable. A fallback whose \"Try again\" calls\n * `resetErrorBoundary()` re-renders the child, and `useQuery` reads the\n * failure still stored on the `QueryResource` and throws it straight back —\n * so without this the fallback never goes away and the retry path that works\n * in the app cannot be exercised. `ClientRouter`'s own boundary wires exactly\n * this pair.\n */\nconst Boundary = (\n props: PropsWithChildren<{\n errorFallback?: ComponentType<FallbackProps>;\n fallback: ReactNode;\n resetKey: string;\n }>,\n) => {\n const { errorFallback: ErrorFallback, fallback, resetKey, children } = props;\n const { clearErrors } = useContext(QueryManagerContext);\n const content = <Suspense fallback={fallback}>{children}</Suspense>;\n\n if (!ErrorFallback) {\n return content;\n }\n\n return (\n <ErrorBoundary\n FallbackComponent={ErrorFallback}\n resetKeys={[resetKey]}\n onReset={clearErrors}\n >\n {content}\n </ErrorBoundary>\n );\n};\n","/**\n * The browser side of islands, shared by the Vite plugin (which builds it), the\n * servers (which point the loader at it) and the tests.\n *\n * No imports: the Vite plugin is built on its own and the servers load it at\n * boot, so this file stays plain strings.\n */\n\n/**\n * The module that hydrates islands: React, `react-dom/client` and the small\n * `h(marker, Component)` below. A virtual module of gemi's Vite plugin that\n * every island entry imports, so the islands on a page share one copy of\n * React, in one chunk.\n */\nexport const ISLAND_RUNTIME_ID = \"virtual:gemi-island-runtime\";\n\n/** What `resolveId` turns `ISLAND_RUNTIME_ID` into (Vite's virtual-module convention). */\nexport const RESOLVED_ISLAND_RUNTIME_ID = `\\0${ISLAND_RUNTIME_ID}`;\n\n/**\n * The query that turns a module into its island entry: `Counter.tsx?gemi-island`\n * is a module of the plugin's that re-exports `Counter.tsx` as `m` next to the\n * runtime's `h`. It is what a static page imports for an island, and so it\n * is what the client manifest lists, keyed `app/…/Counter.tsx?gemi-island`,\n * with React and `react-dom/client` among its imports.\n */\nexport const ISLAND_ENTRY_QUERY = \"?gemi-island\";\n\n/** The island entry module for `file` (an absolute path or a root-relative URL). */\nexport function islandEntrySource(file: string) {\n return `import * as m from ${JSON.stringify(file)};\nexport { m };\nexport { h } from ${JSON.stringify(ISLAND_RUNTIME_ID)};\n`;\n}\n\n/**\n * `h(marker, Component)`: hydrates one `<gemi-island>` marker with the island's\n * component and the props the server serialised into `data-props`.\n *\n * The island's static children were rendered on the server into a\n * `<gemi-slot>` (the first one that belongs to this marker, not to an island\n * nested in it). They are handed back to the component as that same element\n * with the server's HTML, so hydration matches and React leaves them alone.\n *\n * `data-uid` is the `identifierPrefix` the server rendered the island with, so\n * `useId` agrees on both sides and two islands never share an id.\n *\n * Written as a function body with `createElement` and `hydrateRoot` free, so a\n * test can run it against real React without a bundler.\n */\nexport const ISLAND_HYDRATE_SOURCE = `function h(marker, Component) {\n if (typeof Component !== \"function\" && (typeof Component !== \"object\" || Component === null)) {\n throw new Error(\"gemi: island module has no component export for \" + marker.outerHTML.slice(0, 80));\n }\n const json = marker.getAttribute(\"data-props\");\n const props = json ? JSON.parse(json) : {};\n let slot = null;\n for (const candidate of marker.querySelectorAll(\"gemi-slot\")) {\n if (candidate.closest(\"gemi-island\") === marker) {\n slot = candidate;\n break;\n }\n }\n const children = slot\n ? [createElement(\"gemi-slot\", {\n style: { display: \"contents\" },\n suppressHydrationWarning: true,\n dangerouslySetInnerHTML: { __html: slot.innerHTML },\n })]\n : [];\n return hydrateRoot(marker, createElement(Component, props, ...children), {\n identifierPrefix: marker.getAttribute(\"data-uid\") || undefined,\n });\n}`;\n\n/** The runtime module's source, as the plugin serves and builds it. */\nexport const ISLAND_RUNTIME_SOURCE = `import { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nexport ${ISLAND_HYDRATE_SOURCE}\n`;\n\n/**\n * The island loader, inlined into a static document that uses islands.\n *\n * Kept byte-for-byte constant (the per-page island table travels in a\n * separate `<script type=\"application/json\">`, which a Content Security Policy\n * does not treat as script), so a strict policy can allow it by hash:\n * `script-src 'sha256-…'` with `ISLAND_LOADER_CSP_HASH`. Everything it loads\n * is a plain `import()` of a same-origin (or asset-base) file.\n *\n * The table is `{ i: [{ s: entry URL, e: export, l: load }] }` and each\n * marker's `data-island` indexes `i`. Per marker, on its schedule: import the\n * island's entry (the module as `m`, the runtime's `h`), then\n * `h(marker, m[export])`, which calls `hydrateRoot`. A failure is logged and\n * leaves the server-rendered markup in place.\n */\nexport const ISLAND_LOADER_SOURCE =\n 'const d=document,c=JSON.parse(d.getElementById(\"gemi-islands\").textContent),' +\n \"w=f=>(window.requestIdleCallback||setTimeout)(f),\" +\n \"m=(e,o)=>{if(!e.g){e.g=1;import(o.s).then(x=>x.h(e,x.m[o.e])).catch(r=>console.error(r))}};\" +\n 'for(const e of d.querySelectorAll(\"gemi-island\")){const o=c.i[e.dataset.island];if(!o)continue;' +\n 'if(o.l==\"eager\")m(e,o);else if(o.l==\"idle\"||!window.IntersectionObserver||!e.children.length)w(()=>m(e,o));' +\n \"else{const v=new IntersectionObserver(n=>{if(n.some(x=>x.isIntersecting)){v.disconnect();m(e,o)}});for(const k of e.children)v.observe(k)}}\";\n","// Generated by `bun scripts/build-static-navigation.ts` from\n// `internal/staticNavigation/runtime.js`. Do not edit; edit the source and regenerate.\n// No imports: `gemi/testing` (a browser bundle) and `gemi stats` read it too.\n\n/** The sha-256 of the source this was built from, so a test can tell it is stale. */\nexport const STATIC_NAVIGATION_SOURCE_HASH = \"38d6cb0484aa686a8b39adf41de1992d084fe0e504fbc0a1da2f066187451bce\";\n\n/**\n * The navigation runtime (with the island loader) a static view with\n * `.static({ navigation })` inlines. Byte-for-byte constant.\n */\nexport const STATIC_NAVIGATION_SOURCE =\n \"var r=document,i=window,a=history;i.__gemi_nav?.d();var L=new AbortController,h=(t,e,n,s)=>t.addEventListener(e,n,{signal:L.signal,...s}),q=i.__gemi_nav={d:()=>L.abort()},v=(t,e)=>{if(!t.g)t.g=1,import(e.s).then((n)=>{if(t.isConnected)t.r=n.h(t,n.m[e.e])}).catch((n)=>console.error(n))},D=(t)=>{let e=t.getElementById(\\\"gemi-islands\\\");return e?JSON.parse(e.textContent):{i:[]}},R=(t,e)=>{for(let n of t.querySelectorAll(\\\"gemi-island\\\")){let s=e.i[n.dataset.island];if(!s||n.o)continue;if(s.l==\\\"eager\\\")v(n,s);else if(s.l==\\\"idle\\\"||!i.IntersectionObserver||!n.children.length)(i.requestIdleCallback||setTimeout)(()=>v(n,s));else{let l=new IntersectionObserver((m)=>{if(m.some((d)=>d.isIntersecting))l.disconnect(),v(n,s)});for(let m of n.children)l.observe(m)}}},O='meta[name=\\\"gemi-static\\\"]',A=\\\"style:not([data-gemi-fonts])\\\",T=A+\\\",link[rel=stylesheet]\\\",K=`head>${A},body>${A},link[rel=stylesheet]`,I='meta[name]:not([name=viewport]),meta[property],link[rel~=canonical],link[rel~=alternate],link[rel~=icon],link[rel=preload][as=font],style[data-gemi-fonts],script[type=\\\"application/ld+json\\\"]',w=(t)=>t.querySelector(O)?.content,C=(t)=>[...t.querySelectorAll(K)].map((e)=>e.outerHTML).join(),c=(t)=>t.href.split(\\\"#\\\")[0],p=r.createElement(\\\"div\\\");p.setAttribute(\\\"aria-live\\\",\\\"polite\\\");p.style.cssText=\\\"position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap\\\";r.body.append(p);var _=(t)=>{q.p={url:location.href,title:r.title,initial:t},r.dispatchEvent(new CustomEvent(\\\"gemi:page-load\\\",{detail:q.p}))},f=new Map,k=(t)=>{let e=f.get(t);if(!e||Date.now()-e[0]>30000){let n=fetch(t,{headers:{accept:\\\"text/html\\\",\\\"x-gemi-navigate\\\":\\\"1\\\"},signal:AbortSignal.timeout(1e4)}).then((s)=>{if(!s.ok||!/^text\\\\/html/.test(s.headers.get(\\\"content-type\\\"))||new URL(s.url||t).origin!=location.origin)throw s;return s.text().then((l)=>[s.url||t,l])});e=[Date.now(),n],n.catch(()=>f.get(t)==e&&f.delete(t))}if(f.delete(t),f.set(t,e),f.size>10)f.delete(f.keys().next().value);return e[1]},M=(t)=>{let e=t.target.closest?.(\\\"a[href]\\\"),n=e?.getAttribute(\\\"target\\\");if(!e||n&&n!=\\\"_self\\\"||e.hasAttribute(\\\"download\\\")||/(^|\\\\s)external(\\\\s|$)/i.test(e.getAttribute(\\\"rel\\\"))||e.closest(\\\"[data-gemi-reload]\\\"))return;let s=new URL(e.getAttribute(\\\"href\\\"),r.baseURI);if(s.origin==location.origin&&!(s.href.includes(\\\"#\\\")&&c(s)==c(location)))return s},y={},U=()=>Math.random().toString(36).slice(2),E=()=>a.state?.g||a.replaceState({...a.state,g:U()},\\\"\\\"),P=c(location),b=0,N=(t)=>{if(!t.hasAttribute(\\\"tabindex\\\"))t.setAttribute(\\\"tabindex\\\",\\\"-1\\\"),t.addEventListener(\\\"blur\\\",()=>t.removeAttribute(\\\"tabindex\\\"),{once:!0});t.focus({preventScroll:!0})},Y=(t)=>{let e=r.body,n=t.body,s=D(t),l=r.documentElement;r.title=t.title;for(let o of[\\\"lang\\\",\\\"dir\\\"]){let u=t.documentElement.getAttribute(o);if(u==null)l.removeAttribute(o);else l.setAttribute(o,u)}for(let o of r.head.querySelectorAll(I))o.remove();let m=new Set([...r.querySelectorAll(\\\"link[rel=modulepreload]\\\")].map((o)=>o.getAttribute(\\\"href\\\")));r.head.append(...t.head.querySelectorAll(I),...[...t.head.querySelectorAll(\\\"link[rel=modulepreload]\\\")].filter((o)=>!m.has(o.getAttribute(\\\"href\\\"))));let d=new Map([...e.querySelectorAll(\\\"[data-gemi-persist]\\\")].map((o)=>[o.dataset.gemiPersist,o])),g=[...n.querySelectorAll(\\\"[data-gemi-persist]\\\")].map((o)=>[o,d.get(o.dataset.gemiPersist)]).filter((o)=>o[1]);for(let o of[...e.querySelectorAll(\\\"gemi-island\\\")].reverse())if(o.o=1,!g.some((u)=>u[1].contains(o)))try{o.r?.unmount()}catch(u){console.error(u)}for(let[o,u]of g)o.replaceWith(u);for(let o of[...e.childNodes])if(!o.matches?.(T))o.remove();for(let o of[...n.children])if(o.matches(T))o.remove();for(let o of[...e.attributes])e.removeAttribute(o.name);for(let o of n.attributes)e.setAttribute(o.name,o.value);e.append(...n.childNodes,p),R(e,s)},H=async(t,e)=>{let n=++b,s=!e&&y[a.state?.g];a.scrollRestoration=\\\"manual\\\";try{let[l,m]=await k(c(t));if(n!=b)return;let d=new DOMParser().parseFromString(m,\\\"text/html\\\");if(w(d)==null||w(d)!=w(r)||C(d)!=C(r))throw 0;if(e){let o=new URL(l);o.hash=t.hash,a.pushState({g:U()},\\\"\\\",o.href)}P=c(location),Y(d);let g=e&&t.hash&&r.getElementById(decodeURIComponent(t.hash.slice(1)));if(g)g.scrollIntoView({behavior:\\\"instant\\\"});else i.scrollTo({left:s?.[0]||0,top:s?.[1]||0,behavior:\\\"instant\\\"});if(e)N(g||r.querySelector(\\\"h1\\\")||r.querySelector(\\\"main\\\")||r.body);p.textContent=r.title,_(!1)}catch{if(n!=b)return;if(e)location.assign(t.href);else location.reload()}};h(r,\\\"click\\\",(t)=>{if(t.defaultPrevented||t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;let e=M(t);if(e&&r.dispatchEvent(new CustomEvent(\\\"gemi:before-navigate\\\",{cancelable:!0,detail:{url:e.href}})))t.preventDefault(),E(),y[a.state.g]=[i.scrollX,i.scrollY],H(e,1)});h(i,\\\"popstate\\\",()=>{if(b++,c(location)==P){let t=y[a.state?.g];if(t)i.scrollTo({left:t[0],top:t[1],behavior:\\\"instant\\\"})}else if(a.state?.g)H(new URL(location.href),0);else location.reload()});h(i,\\\"hashchange\\\",E);h(i,\\\"scroll\\\",()=>a.state?.g&&(y[a.state.g]=[i.scrollX,i.scrollY]),{passive:!0});var S,x=(t)=>{if(r.querySelector(O)?.dataset.prefetch==\\\"none\\\"||navigator.connection?.saveData)return;let e=M(t);if(e&&c(e)!=c(location))k(c(e))};h(r,\\\"pointerover\\\",(t)=>{clearTimeout(S),S=setTimeout(()=>x(t),65)});h(r,\\\"pointerout\\\",()=>clearTimeout(S));h(r,\\\"focusin\\\",x);h(r,\\\"touchstart\\\",x,{passive:!0});E();R(r,D(r));_(!0);\";\n","import { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ISLAND_HYDRATE_SOURCE } from \"../internal/islandRuntime\";\nimport { STATIC_NAVIGATION_SOURCE } from \"../internal/staticNavigationRuntime\";\n\n/**\n * One page `renderStaticDocument` serves to the navigation runtime: its HTML, or\n * a response of another kind to see the runtime fall back to a full load.\n */\nexport interface StaticTestPage {\n html?: string;\n /** Default 200. */\n status?: number;\n /** Default `text/html; charset=utf-8`. */\n contentType?: string;\n /** The URL the response ends at, as after a redirect. Default: the one asked for. */\n url?: string;\n}\n\nexport interface StaticDocumentOptions {\n /** The document's URL, resolved against the test window's origin. Default `/`. */\n url?: string;\n /**\n * What the runtime's `fetch` gets, by path (`/about`, `/about?x=1`) or full\n * URL: HTML, a `StaticTestPage`, or a function for anything else. A URL\n * not listed is a 404.\n */\n pages?:\n | Record<string, string | StaticTestPage>\n | ((url: URL) => string | StaticTestPage | Promise<string | StaticTestPage>);\n /**\n * The island modules, by the entry URL the page's island table names (`s`):\n * each a module whose exports include the islands' components. Hydrated\n * with React, as `<module>?gemi-island` would.\n */\n islands?: Record<string, Record<string, unknown>>;\n}\n\n/** How a navigation ended. */\nexport type StaticNavigationResult =\n /** The runtime swapped the fetched page in. */\n | { kind: \"swap\"; url: string }\n /** The runtime gave up on a swap and asked for a full page load of `url`. */\n | { kind: \"full-load\"; url: string }\n /** The runtime left the click alone: the browser would follow the link itself. */\n | { kind: \"native\" };\n\nexport interface StaticDocumentHandle {\n /**\n * Clicks the page's first `<a>` whose `href` attribute is `href` and waits\n * for the outcome. Throws when there is no such link.\n */\n navigate(href: string, init?: MouseEventInit): Promise<StaticNavigationResult>;\n /** `history.back()`, and waits for the runtime to show that entry. */\n back(): Promise<StaticNavigationResult>;\n /** Every URL the runtime fetched, prefetches included, in order. */\n requests: string[];\n /** Every full page load the runtime asked for. */\n fullLoads: string[];\n /** Stops the runtime and restores what the helper stubbed. */\n dispose(): void;\n}\n\nconst hydrate = new Function(\n \"createElement\",\n \"hydrateRoot\",\n `${ISLAND_HYDRATE_SOURCE};return h;`,\n)(createElement, hydrateRoot) as (marker: Element, component: unknown) => unknown;\n\n/**\n * Puts a static document (`.static({ navigation })`) into the test's DOM and\n * starts its navigation runtime against stubbed pages, so a test can check that\n * a site's links are followed client-side, and what the next page looks like.\n * Needs a DOM (`// @vitest-environment jsdom`).\n *\n * ```ts\n * const html = await (await render(\"/\")).text(); // the page, from your app\n * const page = renderStaticDocument(html, { pages: { \"/about\": aboutHtml } });\n * expect(await page.navigate(\"/about\")).toEqual({ kind: \"swap\", url: \"http://localhost:3000/about\" });\n * expect(document.querySelector(\"h1\")!.textContent).toBe(\"About\");\n * page.dispose();\n * ```\n *\n * The document's own scripts do not run (the helper runs the runtime itself),\n * and a link the runtime leaves alone is not followed. Islands hydrate\n * asynchronously, as in a browser: wrap what follows in `act`, or wait for it.\n */\nexport function renderStaticDocument(\n html: string,\n options: StaticDocumentOptions = {},\n): StaticDocumentHandle {\n const requests: string[] = [];\n const fullLoads: string[] = [];\n let settle: ((result: StaticNavigationResult) => void) | undefined;\n const outcome = () =>\n new Promise<StaticNavigationResult>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"the navigation did not finish in 5 s\")), 5000);\n settle = (result) => {\n clearTimeout(timer);\n settle = undefined;\n resolve(result);\n };\n });\n\n history.replaceState(null, \"\", options.url ?? \"/\");\n const parsed = new DOMParser().parseFromString(html, \"text/html\");\n for (const script of Array.from(parsed.querySelectorAll(\"script\"))) {\n if (!/json/.test(script.type)) script.remove();\n }\n document.replaceChild(document.importNode(parsed.documentElement, true), document.documentElement);\n\n const fullLoad = (url: string) => {\n fullLoads.push(url);\n settle?.({ kind: \"full-load\", url });\n };\n const location = {\n get href() {\n return window.location.href;\n },\n get origin() {\n return window.location.origin;\n },\n assign: (url: string) => fullLoad(new URL(url, window.location.href).href),\n reload: () => fullLoad(window.location.href),\n };\n\n const pages = options.pages ?? {};\n const fetch = async (input: string) => {\n const url = new URL(input, window.location.href);\n requests.push(url.href);\n const page =\n typeof pages === \"function\"\n ? await pages(url)\n : (pages[url.href] ?? pages[url.pathname + url.search]);\n const spec: StaticTestPage =\n page === undefined\n ? { status: 404, html: \"not found\", contentType: \"text/plain\" }\n : typeof page === \"string\"\n ? { html: page }\n : page;\n const response = new Response(spec.html ?? \"\", {\n status: spec.status ?? 200,\n headers: { \"content-type\": spec.contentType ?? \"text/html; charset=utf-8\" },\n });\n Object.defineProperty(response, \"url\", { value: new URL(spec.url ?? url.href, url).href });\n return response;\n };\n\n const importIsland = async (src: string) => {\n const module = options.islands?.[src];\n if (!module) throw new Error(`renderStaticDocument: no island module for ${src} in options.islands`);\n return { m: module, h: hydrate };\n };\n\n // The runtime announces a click it takes with `gemi:before-navigate`; one\n // it does not take (or one a listener cancelled) is the browser's, which\n // jsdom would try (and fail) to follow, so it is stopped here, on `window`,\n // after every listener on the way.\n let announced: Event | undefined;\n const onBeforeNavigate = (event: Event) => {\n announced = event;\n };\n document.addEventListener(\"gemi:before-navigate\", onBeforeNavigate);\n const onClick = (event: Event) => {\n const taken = announced !== undefined && !announced.defaultPrevented;\n announced = undefined;\n if (taken || !(event.target as Element).closest?.(\"a[href]\")) return;\n event.preventDefault();\n settle?.({ kind: \"native\" });\n };\n window.addEventListener(\"click\", onClick);\n const onPageLoad = (event: Event) => {\n const detail = (event as CustomEvent<{ url: string; initial: boolean }>).detail;\n if (!detail.initial) settle?.({ kind: \"swap\", url: detail.url });\n };\n document.addEventListener(\"gemi:page-load\", onPageLoad);\n\n // Not implemented by jsdom.\n const scrollTo = window.scrollTo;\n window.scrollTo = () => {};\n const scrollIntoView = Element.prototype.scrollIntoView;\n Element.prototype.scrollIntoView = function () {};\n\n const source = STATIC_NAVIGATION_SOURCE.replace(\"import(\", \"__import(\");\n new Function(\"__import\", \"location\", \"fetch\", source)(importIsland, location, fetch);\n\n return {\n requests,\n fullLoads,\n async navigate(href, init) {\n const link = Array.from(document.querySelectorAll(\"a[href]\")).find(\n (a) => a.getAttribute(\"href\") === href,\n );\n if (!link) throw new Error(`renderStaticDocument: no <a href=\"${href}\"> on the page`);\n const done = outcome();\n link.dispatchEvent(new MouseEvent(\"click\", { bubbles: true, cancelable: true, ...init }));\n return await done;\n },\n async back() {\n const done = outcome();\n history.back();\n return await done;\n },\n dispose() {\n (window as unknown as { __gemi_nav?: { d: () => void } }).__gemi_nav?.d();\n window.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"gemi:before-navigate\", onBeforeNavigate);\n document.removeEventListener(\"gemi:page-load\", onPageLoad);\n window.scrollTo = scrollTo;\n Element.prototype.scrollIntoView = scrollIntoView;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../services/broadcast/channels.ts","../../client/realtime/FakeSocket.ts","../../testing/Page.tsx","../../internal/islandRuntime.ts","../../internal/staticNavigationRuntime.ts","../../testing/staticDocument.ts"],"sourcesContent":["/**\n * Channel names: patterns, the topics they build, and the checks both go\n * through. Shared by the `Broadcast` facade (which builds the topic an emit\n * goes to), `ChannelRouter` (which builds the topic a socket may join) and the\n * fake (which matches a pattern against recorded topics), so the three can\n * never disagree on what `site.:siteId` with `{ siteId: \"abc\" }` is called.\n *\n * A pattern is dot-separated segments, each either a literal (`site`) or a\n * param (`:siteId`). A topic is the same with every param filled in:\n * `site.abc123`. Param values are restricted to `[A-Za-z0-9_-]`, so a value\n * can never contain a dot and add a segment, and a topic can always be read\n * back as exactly one pattern's.\n */\n\n/** The longest topic, in characters. */\nexport const MAX_TOPIC_LENGTH = 255;\n\n/** The longest event name, in characters. */\nexport const MAX_EVENT_NAME_LENGTH = 64;\n\nconst LITERAL_SEGMENT = /^[A-Za-z0-9_-]+$/;\nconst PARAM_SEGMENT = /^:[A-Za-z_][A-Za-z0-9_]*$/;\nconst PARAM_VALUE = /^[A-Za-z0-9_-]{1,128}$/;\nconst EVENT_NAME = /^[A-Za-z0-9_.:-]+$/;\n\n/** The names of a pattern's params: `\"site.:siteId\"` → `\"siteId\"`. */\nexport type ChannelParamNames<P extends string> = P extends `${infer Head}.${infer Rest}`\n ? SegmentParam<Head> | ChannelParamNames<Rest>\n : SegmentParam<P>;\n\ntype SegmentParam<S extends string> = S extends `:${infer Name}` ? Name : never;\n\n/** The params a pattern takes: `\"site.:siteId\"` → `{ siteId: string | number }`. */\nexport type ChannelParams<P extends string> = {\n [K in ChannelParamNames<P>]: string | number;\n};\n\n/**\n * `[]` for a pattern without params (or a concrete topic), `[params]` for one\n * with them, so `Broadcast.to(\"status\")` and `Broadcast.to(\"site.:siteId\",\n * { siteId })` both type-check and `Broadcast.to(\"site.:siteId\")` does not.\n */\nexport type ChannelParamsArgs<P extends string> = [ChannelParamNames<P>] extends [never]\n ? [params?: Record<string, never>]\n : [params: ChannelParams<P>];\n\n/**\n * A resolved channel: the topic sockets subscribe to, and the pattern it came\n * from when it was built from one. What `BroadcastEvent.channel()` returns and\n * `Broadcast.to()` accepts.\n */\nexport interface ChannelTarget {\n readonly topic: string;\n readonly pattern?: string;\n}\n\n/** A pattern, topic or param was refused. The message says which and why. */\nexport class InvalidChannelError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidChannelError\";\n }\n}\n\n/** Whether `value` is a `ChannelTarget` rather than a pattern string. */\nexport function isChannelTarget(value: unknown): value is ChannelTarget {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { topic?: unknown }).topic === \"string\"\n );\n}\n\n/** The segments of a pattern, checked. Throws `InvalidChannelError`. */\nexport function parsePattern(pattern: string): string[] {\n if (typeof pattern !== \"string\" || pattern.length === 0) {\n throw new InvalidChannelError(\"A channel pattern must be a non-empty string.\");\n }\n if (pattern.length > MAX_TOPIC_LENGTH) {\n throw new InvalidChannelError(\n `The channel pattern \"${pattern.slice(0, 40)}…\" is longer than ${MAX_TOPIC_LENGTH} characters.`,\n );\n }\n const segments = pattern.split(\".\");\n const seen = new Set<string>();\n for (const segment of segments) {\n if (PARAM_SEGMENT.test(segment)) {\n const name = segment.slice(1);\n if (seen.has(name)) {\n throw new InvalidChannelError(\n `The channel pattern \"${pattern}\" names the param \"${name}\" twice.`,\n );\n }\n seen.add(name);\n continue;\n }\n if (segment.startsWith(\"__\")) {\n throw new InvalidChannelError(\n `The channel \"${pattern}\" has the segment \"${segment}\". Segments starting ` +\n `with \"__\" are reserved for gemi (driver control messages).`,\n );\n }\n if (!LITERAL_SEGMENT.test(segment)) {\n throw new InvalidChannelError(\n `The channel pattern \"${pattern}\" has an invalid segment \"${segment}\". ` +\n `Segments are separated by dots and are either letters, digits, \"_\" ` +\n `and \"-\", or a param such as \":siteId\".`,\n );\n }\n }\n return segments;\n}\n\n/** The param names of a pattern, in order. */\nexport function patternParams(pattern: string): string[] {\n return parsePattern(pattern)\n .filter((segment) => segment.startsWith(\":\"))\n .map((segment) => segment.slice(1));\n}\n\n/**\n * The topic `pattern` names with `params` filled in. Every param must be\n * given, and nothing else: a missing one, an extra one, or a value outside\n * `[A-Za-z0-9_-]` throws `InvalidChannelError`, so a client can never steer a\n * subscription onto a topic its pattern does not describe.\n */\nexport function buildTopic(pattern: string, params: Record<string, unknown> = {}): string {\n const segments = parsePattern(pattern);\n const expected = new Set<string>();\n const topic = segments\n .map((segment) => {\n if (!segment.startsWith(\":\")) return segment;\n const name = segment.slice(1);\n expected.add(name);\n const value = params[name];\n if (value === undefined || value === null) {\n throw new InvalidChannelError(`The channel \"${pattern}\" needs the param \"${name}\".`);\n }\n const text = typeof value === \"number\" && Number.isFinite(value) ? String(value) : value;\n if (typeof text !== \"string\" || !PARAM_VALUE.test(text)) {\n throw new InvalidChannelError(\n `The param \"${name}\" of the channel \"${pattern}\" must be 1 to 128 ` +\n `letters, digits, \"_\" or \"-\". Use a public id in a channel name.`,\n );\n }\n return text;\n })\n .join(\".\");\n\n for (const key of Object.keys(params)) {\n if (!expected.has(key)) {\n throw new InvalidChannelError(`The channel \"${pattern}\" has no param \"${key}\".`);\n }\n }\n if (topic.length > MAX_TOPIC_LENGTH) {\n throw new InvalidChannelError(\n `The topic built from \"${pattern}\" is longer than ${MAX_TOPIC_LENGTH} characters.`,\n );\n }\n return topic;\n}\n\n/** Checks a concrete topic (no params). Throws `InvalidChannelError`. */\nexport function assertTopic(topic: string): string {\n const segments = parsePattern(topic);\n if (segments.some((segment) => segment.startsWith(\":\"))) {\n throw new InvalidChannelError(\n `\"${topic}\" is a pattern, not a topic: pass its params, as in ` +\n `Broadcast.to(\"${topic}\", { ... }).`,\n );\n }\n return topic;\n}\n\n/** Checks an event name. Throws `InvalidChannelError`. */\nexport function assertEventName(event: string): string {\n if (\n typeof event !== \"string\" ||\n event.length === 0 ||\n event.length > MAX_EVENT_NAME_LENGTH ||\n !EVENT_NAME.test(event)\n ) {\n throw new InvalidChannelError(\n `The event name \"${String(event).slice(0, 40)}\" must be 1 to ` +\n `${MAX_EVENT_NAME_LENGTH} letters, digits, \"_\", \"-\", \".\" or \":\".`,\n );\n }\n return event;\n}\n\n/**\n * Whether `topic` is one `pattern` builds. A pattern without params matches\n * only itself, except `\"user\"`, which also matches every `user.<id>`: that is\n * the topic the router's `user` channel resolves to.\n */\nexport function topicMatches(pattern: string, topic: string): boolean {\n if (pattern === topic) return true;\n if (pattern === USER_CHANNEL)\n return topic.startsWith(`${USER_CHANNEL}.`) && topic.split(\".\").length === 2;\n const patternSegments = pattern.split(\".\");\n const topicSegments = topic.split(\".\");\n if (patternSegments.length !== topicSegments.length) return false;\n return patternSegments.every(\n (segment, i) =>\n segment === topicSegments[i] ||\n (segment.startsWith(\":\") && PARAM_VALUE.test(topicSegments[i])),\n );\n}\n\n/**\n * The channel `ChannelRouter` resolves from the session rather than from\n * params: a socket that subscribes to `\"user\"` joins `user.<its user's id>`.\n */\nexport const USER_CHANNEL = \"user\";\n\n/** The topic of a user's own channel. */\nexport function userTopic(user: { id: unknown } | string | number): string {\n const id = typeof user === \"object\" && user !== null ? user.id : user;\n return buildTopic(`${USER_CHANNEL}.:id`, { id: id as string | number });\n}\n","import { buildTopic, topicMatches } from \"../../services/broadcast/channels\";\nimport type { DeniedCode } from \"../../services/broadcast/protocol\";\nimport {\n channelKey,\n type ChannelListener,\n type ChannelParamValues,\n type ChannelStatus,\n type RealtimeSocket,\n} from \"./RealtimeClient\";\n\nexport interface FakeSubscription {\n pattern: string;\n params: ChannelParamValues;\n status: ChannelStatus;\n code: DeniedCode | null;\n}\n\ninterface FakeEntry extends FakeSubscription {\n key: string;\n listeners: Set<ChannelListener>;\n}\n\n/**\n * A socket for component tests: what `<Page socket={...}>` hands the\n * channel hooks instead of a WebSocket. Every subscription is `open` at\n * once; the test pushes events and resyncs by hand.\n *\n * ```tsx\n * const socket = fakeSocket();\n * render(<Page socket={socket}><SiteEditor /></Page>);\n * act(() => socket.emit(\"site.:siteId\", \"changed\", { pages: [\"/about\"] }));\n * act(() => socket.resync());\n * ```\n *\n * No resync is run on subscribe, unlike the real socket's ack: a test says\n * when one happens.\n */\nexport class FakeSocket implements RealtimeSocket {\n private entries = new Map<string, FakeEntry>();\n private status: ChannelStatus = \"open\";\n\n subscribe(pattern: string, params: ChannelParamValues, listener: ChannelListener): () => void {\n const key = channelKey(pattern, params);\n let entry = this.entries.get(key);\n if (!entry) {\n entry = {\n key,\n pattern,\n params: { ...params },\n status: this.status,\n code: null,\n listeners: new Set(),\n };\n this.entries.set(key, entry);\n }\n entry.listeners.add(listener);\n listener.onStatus?.(entry.status, entry.code);\n return () => {\n entry!.listeners.delete(listener);\n if (entry!.listeners.size === 0) this.entries.delete(key);\n };\n }\n\n /** The channels subscribed to right now. */\n get subscriptions(): FakeSubscription[] {\n return Array.from(this.entries.values()).map(({ pattern, params, status, code }) => ({\n pattern,\n params,\n status,\n code,\n }));\n }\n\n /**\n * Delivers `event` to the open subscriptions `channel` names: a pattern\n * (`\"site.:siteId\"`, every subscription to it, or only those with `params`\n * when given), or a concrete topic (`\"site.abc\"`).\n */\n emit(channel: string, event: string, data?: unknown, params?: ChannelParamValues): void {\n for (const entry of this.matching(channel, params)) {\n if (entry.status !== \"open\") continue;\n for (const listener of Array.from(entry.listeners)) listener.onEvent?.(event, data);\n }\n }\n\n /** Runs every matching subscription's resync (all when `channel` is left out). */\n resync(channel?: string, params?: ChannelParamValues): void {\n for (const entry of channel ? this.matching(channel, params) : this.entries.values()) {\n if (entry.status !== \"open\") continue;\n for (const listener of Array.from(entry.listeners)) listener.onResync?.();\n }\n }\n\n /** Sets every subscription's status (and the status new ones start in). */\n setStatus(status: ChannelStatus): void {\n this.status = status;\n for (const entry of this.entries.values()) this.update(entry, status, null);\n }\n\n /** Refuses the matching subscriptions with `code`. */\n deny(channel: string, code: DeniedCode = \"denied\", params?: ChannelParamValues): void {\n for (const entry of this.matching(channel, params)) this.update(entry, \"denied\", code);\n }\n\n private update(entry: FakeEntry, status: ChannelStatus, code: DeniedCode | null) {\n entry.status = status;\n entry.code = code;\n for (const listener of Array.from(entry.listeners)) listener.onStatus?.(status, code);\n }\n\n private matching(channel: string, params?: ChannelParamValues): FakeEntry[] {\n return Array.from(this.entries.values()).filter((entry) => {\n if (entry.pattern === channel) {\n return !params || channelKey(channel, params) === entry.key;\n }\n try {\n const topic =\n Object.keys(entry.params).length > 0\n ? buildTopic(entry.pattern, entry.params)\n : entry.pattern;\n // `\"user\"` is resolved to `user.<id>` by the server; any id matches.\n return topic === channel || (entry.pattern === \"user\" && topicMatches(\"user\", channel));\n } catch {\n return false;\n }\n });\n }\n}\n\n/** A `FakeSocket` for `<Page socket={...}>`. */\nexport function fakeSocket(): FakeSocket {\n return new FakeSocket();\n}\n","import {\n Suspense,\n useContext,\n useMemo,\n useRef,\n useState,\n type ComponentType,\n type PropsWithChildren,\n type ReactNode,\n} from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\nimport { Action, createMemoryHistory } from \"history\";\n\nimport type { User } from \"../auth/types\";\nimport { ClientRouterContext } from \"../client/ClientRouterContext\";\nimport { I18nProvider } from \"../client/I18nContext\";\nimport { ProgressManager } from \"../client/ProgressManager\";\nimport {\n QueryManagerContext,\n QueryManagerProvider,\n type QueryConfig,\n} from \"../client/QueryManagerContext\";\nimport {\n RouteStateProvider,\n type PageData,\n type RouteState,\n} from \"../client/RouteStateContext\";\nimport { RouteTransitionProvider } from \"../client/RouteTransitionProvider\";\nimport {\n ServerDataContext,\n type ServerDataContextValue,\n} from \"../client/ServerDataProvider\";\nimport { ThemeProvider } from \"../client/ThemeProvider\";\nimport type { ClientFeatureKey } from \"../client/rpc\";\nimport { FakeSocket } from \"../client/realtime/FakeSocket\";\nimport { RealtimeContext } from \"../client/realtime/RealtimeContext\";\nimport type { Breadcrumb } from \"../client/useBreadcrumbs\";\nimport { applyParams } from \"../utils/applyParams\";\nimport { Subject } from \"../utils/Subject\";\nimport { toVariantKey } from \"../utils/variantKey\";\n\n/**\n * A dictionary as `Dictionary.create` produces it: a name and a\n * `{ key: { locale: string } }` map.\n *\n * Typed structurally rather than as `Dictionary<T>` for two reasons. It keeps\n * `gemi/testing` from importing `gemi/i18n` — a server entry, which reaches the\n * `Lang` facade and through it the container. And it means an object literal of\n * the same shape is accepted, so a test never *has* to import the app's\n * dictionaries at all.\n *\n * Only `name` and `dictionary` are read — both plain data. `Dictionary`'s\n * server-only methods (`render`, `reference`, which throw once `window` is\n * defined) are never called from here.\n */\nexport interface PageDictionary {\n name: string;\n dictionary: Record<string, Record<string, string>>;\n}\n\nexport interface PageProps {\n /**\n * The URL the view is mounted at. A template (`/app/:orgId/chat`) is resolved\n * against `params`, so the same string can be pasted from the router.\n */\n pathname?: string;\n /** Route params, as the router would have parsed them out of `pathname`. */\n params?: Record<string, string>;\n /** `\"?tab=recent\"`, `\"tab=recent\"`, or `{ tab: \"recent\" }`. */\n searchParams?: string | Record<string, string | number | boolean>;\n hash?: string;\n /** The locale `useTranslator` and `useLocale` report. */\n locale?: string;\n /**\n * The app's default locale. Links and `useNavigate` omit the locale segment\n * for it, so leaving this equal to `locale` (the default) keeps hrefs\n * unprefixed — set it to seed a page being viewed in a non-default locale.\n */\n defaultLocale?: string;\n supportedLocales?: string[];\n /** The zone `useFormatter` formats dates in. Defaults to `UTC`, like an app with no `translation.timeZone`. */\n timeZone?: string;\n /**\n * Dictionaries the components under test translate against — the app's own,\n * or literals of the same shape.\n *\n * Note what importing the app's costs: `app/i18n/index.ts` imports\n * `gemi/i18n`, a server entry, so the test file pulls the container and\n * `node:async_hooks` into its module graph. Nothing there runs (and nothing\n * here calls it), so it is fine under any runner that has Node builtins —\n * vitest, `bun test` — and not under one that renders in a real browser. Use\n * `translations` there, which is the shape the client actually receives.\n */\n dictionaries?: PageDictionary[];\n /**\n * Translations for the current locale, already resolved: dictionary name →\n * key → string. This is the shape the server serializes onto the page, so it\n * is what the client reads at runtime — and it imports nothing.\n *\n * ```tsx\n * <Page translations={{ Chat: { greeting: \"Hello {{name}}\" } }}>\n * ```\n *\n * Merged over `dictionaries`, so the two compose: seed from the app's real\n * dictionary and override the one key a test is about.\n */\n translations?: Record<string, Record<string, string>>;\n /**\n * Data `useQuery` finds already cached, keyed by API path — the path passed\n * to `useQuery`, without the `/api` prefix the client adds when it fetches.\n * A key may carry a query string (`\"/lists?page=2\"`) to seed one search\n * variant.\n *\n * The cache is keyed by the *resolved* path, so a key's `:params` are\n * resolved against the page's `params` — and a key naming one the page does\n * not carry is an error, naming the key. That case is common enough to be\n * worth the loud failure: a query often takes its params from the call site\n * rather than the route (`useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state), and the right seed for it is the resolved\n * path, `\"/orgs/abc/lists\"`.\n *\n * Seeded at mount only: a component that mutates or refetches its data owns\n * the cache from then on.\n */\n queryData?: Record<string, unknown>;\n /** App-wide `useQuery` defaults, as `createRoot` threads them. */\n queryConfig?: QueryConfig;\n /**\n * What `useUser()` reports. Defaults to `null` — an anonymous visitor — and\n * either way the hook resolves from here without issuing a request.\n */\n user?: Partial<User> | null;\n /** What `useBreadcrumbs()` returns, in order. */\n breadcrumbs?: Breadcrumb[];\n /**\n * The `route.domains` group the page is served under, as `useDomain()`\n * reports it. Left out, `useDomain()` reads empty — the shape an app without\n * `route.domains` sees — and `useDomain().url()` throws, exactly as it does\n * there.\n *\n * `root` and `origin` default off the host, so the common case is one field:\n * `domain={{ host: \"acme.example.com\", params: { tenant: \"acme\" } }}`.\n */\n domain?: {\n host: string;\n group?: string;\n params?: Record<string, string>;\n custom?: boolean;\n root?: string;\n origin?: string;\n } | null;\n /**\n * What `useFeature()` reports, keyed by feature key. Anything left out reads\n * as off, exactly as an undeclared key does in a real request.\n *\n * A test seeds the *outcome*, not the declaration: `app/features` owns the\n * targeting and the rollout, and re-deriving those here would mean building a\n * request context — a user, a `session_id`, a bot verdict — to assert a\n * branch that only cares which way the feature came out. Cover the targeting\n * where it lives, with a server test.\n */\n features?: Partial<Record<ClientFeatureKey, boolean>>;\n /**\n * The theme `useTheme()` starts on. The app reads a visitor's stored choice\n * out of `localStorage`; a test has no session to have made one in, so this\n * seeds it without writing to storage. `setTheme` works from either.\n */\n theme?: \"light\" | \"dark\" | \"system\";\n /**\n * Fallback for the `Suspense` boundary wrapped around the children — the\n * stand-in for the view's own `Loading` export, which the real router\n * supplies. Defaults to `null`.\n */\n fallback?: ReactNode;\n /**\n * Renders in place of the children when one throws, mirroring a view's\n * `Error` export. Without it a thrown render (an errored suspense query,\n * say) propagates out of `render()` and fails the test, which is usually\n * what you want.\n */\n errorFallback?: ComponentType<FallbackProps>;\n /**\n * Called when something navigates — a `Link` click, `useNavigate().push`.\n * The navigation is reported, not performed: `<Page>` renders one route and\n * does not re-resolve the tree, so `useLocation()` keeps reporting the\n * pathname it was given.\n */\n onNavigate?: (href: string, action: \"push\" | \"replace\") => void;\n /**\n * The socket `useChannel`, `useChannelInvalidate` and `useQuery({ live })`\n * subscribe through: a `fakeSocket()` the test pushes events and resyncs\n * into. Left out, a fake of the page's own is used, so a component test\n * never opens a WebSocket.\n */\n socket?: FakeSocket;\n}\n\n/** `\"?a=b\"` (or `\"\"`) from any of the shapes `searchParams` accepts. */\nfunction toSearchString(search: PageProps[\"searchParams\"]): string {\n if (!search) return \"\";\n const query =\n typeof search === \"string\"\n ? search.replace(/^\\?/, \"\")\n : new URLSearchParams(\n Object.entries(search).map(([key, value]) => [key, String(value)]),\n ).toString();\n return query ? `?${query}` : \"\";\n}\n\n/**\n * The `:params` a seed key needs supplied. Wildcards (`:rest*`) are excluded —\n * `applyParams` drops those when missing, which is a resolution rather than a\n * failure.\n *\n * Optional params (`:id?`) cannot reach here at all: a key is split at its\n * first `?` to separate the search string, so `\"/lists/:folderId?\"` arrives as\n * the path `/lists/:folderId`. Seed the resolved path for those.\n */\nfunction requiredParamsOf(path: string): string[] {\n return Array.from(path.matchAll(/:([^/]+)/g), ([, name]) => name).filter(\n (name) => !name.endsWith(\"*\"),\n );\n}\n\n/**\n * Fails a `queryData` key whose `:params` the page does not carry, saying so\n * in terms of the key.\n *\n * Without this the two documented runners disagree, and neither is legible.\n * `applyParams` throws `Missing parameter: orgId` under `import.meta.env.DEV`\n * (vitest) — a router-utility stack trace out of `<Page>`'s render, before\n * anything mounts, naming nothing that appears in the test. Under `bun test`\n * that flag is falsy, so it logs and seeds the literal `/orgs/undefined/lists`\n * instead: the seed misses, the query goes to the network, and the assertion\n * runs against a loading state.\n *\n * The case is common because a query's params often come from the call site\n * rather than the route — `useQuery(\"/orgs/:orgId/lists\", { params: { orgId } })`\n * with `orgId` in component state. The cache is keyed by the *resolved* path,\n * so the way out is to seed that path directly.\n */\nfunction resolveSeedPath(\n key: string,\n rawPath: string,\n params: Record<string, string>,\n): string {\n const missing = requiredParamsOf(rawPath).filter(\n (name) => params[name] === undefined,\n );\n if (missing.length > 0) {\n throw new Error(\n `[gemi] <Page> cannot resolve the queryData key \"${key}\": no value for ` +\n `${missing.map((name) => `:${name}`).join(\", \")}. The query cache is ` +\n `keyed by the resolved path, so either add ${missing\n .map((name) => `\\`${name}\\``)\n .join(\", \")} to the page's \\`params\\`, or seed the resolved path ` +\n `(e.g. \"${applyParams(\n rawPath,\n Object.fromEntries(missing.map((name) => [name, \"123\"])),\n )}\") — which is what a query taking its params from component state ` +\n `reads under.`,\n );\n }\n return applyParams(rawPath, params);\n}\n\n/**\n * `queryData` in the shape the query cache seeds from: `{ [path]: { [variant]:\n * data } }`, the same payload `Query.prefetch` puts on a server-rendered page.\n */\nfunction toPrefetchedData(\n queryData: Record<string, unknown>,\n params: Record<string, string>,\n warned: Set<string>,\n) {\n const prefetchedData: Record<string, Record<string, unknown>> = {};\n for (const [key, data] of Object.entries(queryData)) {\n const [rawPath, rawSearch = \"\"] = key.split(\"?\");\n // Once per key per page, not once per render: this runs again on every\n // re-render, and a duplicated warning reads as a second mistake.\n if (rawPath.startsWith(\"/api/\") && !warned.has(key)) {\n warned.add(key);\n console.warn(\n `[gemi] queryData key \"${key}\" starts with /api. useQuery is keyed by ` +\n `the path you pass it — the /api prefix is added when it fetches — ` +\n `so this entry seeds a path no query reads.`,\n );\n }\n const path = resolveSeedPath(key, rawPath, params);\n prefetchedData[path] = {\n ...prefetchedData[path],\n [toVariantKey(rawSearch)]: data,\n };\n }\n return prefetchedData;\n}\n\n/**\n * `{ [locale]: { [dictionary]: { [key]: string } } }` — the payload\n * `Translator` serializes onto the page, which is the only form the client\n * ever sees. `dictionaries` are transposed into it (they are keyed the other\n * way round, by key then locale); `translations` is already in it and lands\n * under `locale`, last, so it wins.\n */\nfunction toClientDictionary(\n dictionaries: PageDictionary[],\n locales: string[],\n locale: string,\n translations: Record<string, Record<string, string>>,\n): Record<string, Record<string, Record<string, string>>> {\n const clientDictionary: Record<\n string,\n Record<string, Record<string, string>>\n > = {};\n for (const supported of locales) {\n clientDictionary[supported] ??= {};\n }\n for (const { name, dictionary } of dictionaries) {\n for (const [key, byLocale] of Object.entries(dictionary ?? {})) {\n for (const [dictLocale, translation] of Object.entries(byLocale ?? {})) {\n clientDictionary[dictLocale] ??= {};\n clientDictionary[dictLocale][name] ??= {};\n clientDictionary[dictLocale][name][key] = translation;\n }\n }\n }\n for (const [name, keys] of Object.entries(translations)) {\n clientDictionary[locale] ??= {};\n clientDictionary[locale][name] = {\n ...clientDictionary[locale][name],\n ...keys,\n };\n }\n return clientDictionary;\n}\n\n/**\n * Mounts a component with the inputs a view normally arrives with — route\n * params, the current locale and its dictionaries, prefetched query data, the\n * signed-in user — so a unit test can assert what it renders rather than only\n * its empty state.\n *\n * ```tsx\n * import { render, screen } from \"@testing-library/react\";\n * import { Page } from \"gemi/testing\";\n *\n * render(\n * <Page\n * pathname=\"/app/:orgId/chat\"\n * params={{ orgId: \"abc\" }}\n * queryData={{ \"/organizations/:orgId/messages\": [{ id: 1, body: \"hi\" }] }}\n * dictionaries={[ChatDictionary]}\n * >\n * <OrgChat />\n * </Page>,\n * );\n *\n * expect(screen.getByText(\"hi\")).toBeDefined();\n * ```\n *\n * It is a plain component, so it composes with any renderer — Testing\n * Library's `render`, `react-test-renderer`, or `renderToString`.\n *\n * Two things it does not do.\n *\n * It does not route: there is no view tree and no route manifest behind it, so\n * a navigation is reported through `onNavigate` rather than resolved. To\n * assert the page *after* one, render a second `<Page>`.\n *\n * And under `renderToString` it renders the server's own no-data branch for an\n * *unseeded* query rather than suspending — deliberately, because suspending\n * there is the streaming server's job: `createRoot` threads the request's\n * `ServerQueryStore` through `ServerQueryContext`, and that store is what runs\n * the route handler. A harness has no request and no handler to run, so there\n * is nothing to suspend on. Seeded queries render their data on the server\n * exactly as they do in the browser; unseeded ones behave as a route with no\n * `Query.prefetch` does, framework warning included.\n */\nexport const Page = (props: PropsWithChildren<PageProps>) => {\n const {\n children,\n pathname: routePath = \"/\",\n params = {},\n searchParams,\n hash = \"\",\n locale = \"en-US\",\n defaultLocale = locale,\n timeZone = \"UTC\",\n dictionaries = [],\n translations = {},\n queryData = {},\n queryConfig,\n user = null,\n breadcrumbs = [],\n domain = null,\n features = {},\n theme,\n fallback = null,\n errorFallback,\n onNavigate,\n socket,\n } = props;\n const [ownSocket] = useState(() => new FakeSocket());\n\n // The caller's list, in the caller's order — a language switcher maps over\n // it, so hoisting the page's own locale to the front would reorder the\n // rendered options. The defaults are appended, only to guarantee presence.\n const supportedLocales = useMemo(\n () =>\n Array.from(\n new Set([...(props.supportedLocales ?? []), defaultLocale, locale]),\n ),\n [props.supportedLocales, defaultLocale, locale],\n );\n const pathname = applyParams(routePath, params) || \"/\";\n // `root` defaults to the host minus its first label, so a tenant subdomain\n // needs only its host; `origin` to https on that host. Either can be given\n // outright when the test is about the other.\n const domainValue = useMemo(() => {\n if (!domain) {\n return null;\n }\n const labels = domain.host.split(\".\");\n return {\n host: domain.host,\n group: domain.group ?? \"\",\n params: domain.params ?? {},\n custom: domain.custom ?? false,\n root:\n domain.root ?? (labels.length > 2 ? labels.slice(1).join(\".\") : domain.host),\n origin: domain.origin ?? `https://${domain.host}`,\n };\n }, [domain]);\n const search = toSearchString(searchParams);\n // The URL's locale segment, which the default locale does not get.\n const urlLocaleSegment = locale === defaultLocale ? null : locale;\n\n const dictionary = useMemo(\n () =>\n toClientDictionary(dictionaries, supportedLocales, locale, translations),\n [dictionaries, supportedLocales, locale, translations],\n );\n\n // Keys that have already drawn a warning, so a re-render does not repeat it.\n const warnedRef = useRef<Set<string>>(new Set());\n const prefetchedData = useMemo(() => {\n const seeded = toPrefetchedData(queryData, params, warnedRef.current);\n // `useUser` reads `/auth/me` like any other query. Seeding it — with `null`\n // for the anonymous default — is what keeps a component test off the\n // network for a hook nothing in the test asked for.\n seeded[\"/auth/me\"] ??= { \"\": user ?? null };\n return seeded;\n }, [queryData, params, user]);\n\n // Keyed by `${view}:${pathname}` in the router's cache; the view names are\n // ours to choose here, and only their order reaches `useBreadcrumbs`.\n const { breadcrumbViews, breadcrumbsCache, breadcrumbsRecord } =\n useMemo(() => {\n const views = breadcrumbs.map((_, index) => `breadcrumb-${index}`);\n const cache = new Map<string, Breadcrumb>(\n breadcrumbs.map((breadcrumb, index) => [\n `${views[index]}:${pathname}`,\n breadcrumb,\n ]),\n );\n return {\n breadcrumbViews: views,\n breadcrumbsCache: cache,\n breadcrumbsRecord: Object.fromEntries(cache),\n };\n }, [breadcrumbs, pathname]);\n\n const [isNavigatingSubject] = useState(() => new Subject<boolean>(false));\n const [progressManager] = useState(\n () => new ProgressManager(isNavigatingSubject),\n );\n const [history] = useState(() =>\n createMemoryHistory({ initialEntries: [`${pathname}${search}${hash}`] }),\n );\n\n const routeState: RouteState & PageData = useMemo(\n () => ({\n views: [],\n params,\n search,\n state: {},\n pathname,\n hash,\n action: null,\n routePath,\n locale: urlLocaleSegment,\n data: {},\n i18n: {\n currentLocale: locale,\n dictionary,\n supportedLocales,\n defaultLocale,\n timeZone,\n },\n prefetchedData,\n breadcrumbs: breadcrumbsRecord,\n features: features as Record<string, boolean>,\n appId: \"test\",\n }),\n [\n params,\n search,\n pathname,\n hash,\n routePath,\n urlLocaleSegment,\n locale,\n dictionary,\n supportedLocales,\n defaultLocale,\n timeZone,\n prefetchedData,\n breadcrumbsRecord,\n features,\n ],\n );\n\n const [routerSubject] = useState(() => new Subject<RouteState>(routeState));\n\n const onNavigateRef = useRef(onNavigate);\n onNavigateRef.current = onNavigate;\n // Subscribed during render, not in an effect, because React flushes child\n // effects before parent ones — and mount-time navigation is a real pattern:\n // `<Redirect>` pushes from its own mount effect, as does any auth guard. A\n // listener registered in `<Page>`'s effect is not there yet when those fire,\n // so `onNavigate` would silently miss exactly the redirects a test most\n // wants to assert. Ref-guarded, so React's double-invoke registers one.\n //\n // Nothing unsubscribes: this history is created per `<Page>` and reachable\n // from nowhere else, so it is collected with the page it belongs to.\n const listeningRef = useRef(false);\n if (!listeningRef.current) {\n listeningRef.current = true;\n history.listen(({ location, action }) => {\n const href = `${location.pathname}${location.search}${location.hash}`;\n onNavigateRef.current?.(\n href,\n action === Action.Replace ? \"replace\" : \"push\",\n );\n });\n }\n\n const serverData: ServerDataContextValue = useMemo(\n () => ({\n routeManifest: {},\n pageData: {},\n breadcrumbs: breadcrumbsRecord,\n prefetchedData,\n router: {\n pathname,\n params,\n currentPath: routePath,\n is404: false,\n searchParams: search,\n urlLocaleSegment,\n domain: domainValue,\n },\n i18n: {\n dictionary,\n currentLocale: locale,\n supportedLocales,\n defaultLocale,\n timeZone,\n },\n componentTree: [],\n auth: { user: user as User },\n features: features as Record<string, boolean>,\n __csrf: \"test-csrf-token\",\n cssManifest: {},\n modulePreloadManifest: {},\n meta: {},\n appId: \"test\",\n }),\n [\n breadcrumbsRecord,\n prefetchedData,\n pathname,\n params,\n routePath,\n search,\n urlLocaleSegment,\n dictionary,\n locale,\n supportedLocales,\n defaultLocale,\n timeZone,\n user,\n features,\n ],\n );\n\n // Everything the router hooks read, inert: nothing here fetches, preloads or\n // resolves a route, but every hook finds the shape it expects rather than an\n // empty context to crash on.\n const [viewEntriesSubject] = useState(() => new Subject<string[]>([]));\n const routerContext = useMemo(\n () => ({\n viewEntriesSubject,\n history,\n updatePageData: () => {},\n getPageData: () => ({}),\n getScrollPosition: () => 0,\n getViewPathsFromPathname: () => breadcrumbViews,\n getRoutePathnameFromHref: (href: string) => href,\n isNavigatingSubject,\n setNavigationAbortController: () => {},\n progressManager,\n fetchRouteCSS: async () => {},\n preloadRouteModules: () => {},\n prefetchRoute: async () => {},\n takePrefetched: () => null,\n clearPrefetchCache: () => {},\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n }),\n [\n viewEntriesSubject,\n history,\n breadcrumbViews,\n isNavigatingSubject,\n progressManager,\n breadcrumbsCache,\n routerSubject,\n urlLocaleSegment,\n ],\n );\n\n return (\n <ThemeProvider theme={theme}>\n <ServerDataContext.Provider value={serverData}>\n <I18nProvider>\n <QueryManagerProvider queryConfig={queryConfig}>\n <ClientRouterContext.Provider value={routerContext}>\n <RouteTransitionProvider\n isPending={false}\n isFetching={false}\n transitionPath={[\"\", pathname]}\n >\n <RouteStateProvider state={routeState}>\n <RealtimeContext.Provider value={socket ?? ownSocket}>\n <Boundary\n errorFallback={errorFallback}\n fallback={fallback}\n resetKey={pathname}\n >\n {children}\n </Boundary>\n </RealtimeContext.Provider>\n </RouteStateProvider>\n </RouteTransitionProvider>\n </ClientRouterContext.Provider>\n </QueryManagerProvider>\n </I18nProvider>\n </ServerDataContext.Provider>\n </ThemeProvider>\n );\n};\n\n/**\n * The `Suspense` boundary every view renders inside, and — when the test\n * supplies a fallback for it — the error boundary too.\n *\n * `onReset={clearErrors}` is the part that has to be here rather than inline\n * above: it needs `QueryManagerContext`, and it is what makes a view's real\n * `Error` export testable. A fallback whose \"Try again\" calls\n * `resetErrorBoundary()` re-renders the child, and `useQuery` reads the\n * failure still stored on the `QueryResource` and throws it straight back —\n * so without this the fallback never goes away and the retry path that works\n * in the app cannot be exercised. `ClientRouter`'s own boundary wires exactly\n * this pair.\n */\nconst Boundary = (\n props: PropsWithChildren<{\n errorFallback?: ComponentType<FallbackProps>;\n fallback: ReactNode;\n resetKey: string;\n }>,\n) => {\n const { errorFallback: ErrorFallback, fallback, resetKey, children } = props;\n const { clearErrors } = useContext(QueryManagerContext);\n const content = <Suspense fallback={fallback}>{children}</Suspense>;\n\n if (!ErrorFallback) {\n return content;\n }\n\n return (\n <ErrorBoundary\n FallbackComponent={ErrorFallback}\n resetKeys={[resetKey]}\n onReset={clearErrors}\n >\n {content}\n </ErrorBoundary>\n );\n};\n","/**\n * The browser side of islands, shared by the Vite plugin (which builds it), the\n * servers (which point the loader at it) and the tests.\n *\n * No imports: the Vite plugin is built on its own and the servers load it at\n * boot, so this file stays plain strings.\n */\n\n/**\n * The module that hydrates islands: React, `react-dom/client` and the small\n * `h(marker, Component)` below. A virtual module of gemi's Vite plugin that\n * every island entry imports, so the islands on a page share one copy of\n * React, in one chunk.\n */\nexport const ISLAND_RUNTIME_ID = \"virtual:gemi-island-runtime\";\n\n/** What `resolveId` turns `ISLAND_RUNTIME_ID` into (Vite's virtual-module convention). */\nexport const RESOLVED_ISLAND_RUNTIME_ID = `\\0${ISLAND_RUNTIME_ID}`;\n\n/**\n * The query that turns a module into its island entry: `Counter.tsx?gemi-island`\n * is a module of the plugin's that re-exports `Counter.tsx` as `m` next to the\n * runtime's `h`. It is what a static page imports for an island, and so it\n * is what the client manifest lists, keyed `app/…/Counter.tsx?gemi-island`,\n * with React and `react-dom/client` among its imports.\n */\nexport const ISLAND_ENTRY_QUERY = \"?gemi-island\";\n\n/** The island entry module for `file` (an absolute path or a root-relative URL). */\nexport function islandEntrySource(file: string) {\n return `import * as m from ${JSON.stringify(file)};\nexport { m };\nexport { h } from ${JSON.stringify(ISLAND_RUNTIME_ID)};\n`;\n}\n\n/**\n * `h(marker, Component)`: hydrates one `<gemi-island>` marker with the island's\n * component and the props the server serialised into `data-props`.\n *\n * The island's static children were rendered on the server into a\n * `<gemi-slot>` (the first one that belongs to this marker, not to an island\n * nested in it). They are handed back to the component as that same element\n * with the server's HTML, so hydration matches and React leaves them alone.\n *\n * `data-uid` is the `identifierPrefix` the server rendered the island with, so\n * `useId` agrees on both sides and two islands never share an id.\n *\n * Written as a function body with `createElement` and `hydrateRoot` free, so a\n * test can run it against real React without a bundler.\n */\nexport const ISLAND_HYDRATE_SOURCE = `function h(marker, Component) {\n if (typeof Component !== \"function\" && (typeof Component !== \"object\" || Component === null)) {\n throw new Error(\"gemi: island module has no component export for \" + marker.outerHTML.slice(0, 80));\n }\n const json = marker.getAttribute(\"data-props\");\n const props = json ? JSON.parse(json) : {};\n let slot = null;\n for (const candidate of marker.querySelectorAll(\"gemi-slot\")) {\n if (candidate.closest(\"gemi-island\") === marker) {\n slot = candidate;\n break;\n }\n }\n const children = slot\n ? [createElement(\"gemi-slot\", {\n style: { display: \"contents\" },\n suppressHydrationWarning: true,\n dangerouslySetInnerHTML: { __html: slot.innerHTML },\n })]\n : [];\n return hydrateRoot(marker, createElement(Component, props, ...children), {\n identifierPrefix: marker.getAttribute(\"data-uid\") || undefined,\n });\n}`;\n\n/** The runtime module's source, as the plugin serves and builds it. */\nexport const ISLAND_RUNTIME_SOURCE = `import { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nexport ${ISLAND_HYDRATE_SOURCE}\n`;\n\n/**\n * The island loader, inlined into a static document that uses islands.\n *\n * Kept byte-for-byte constant (the per-page island table travels in a\n * separate `<script type=\"application/json\">`, which a Content Security Policy\n * does not treat as script), so a strict policy can allow it by hash:\n * `script-src 'sha256-…'` with `ISLAND_LOADER_CSP_HASH`. Everything it loads\n * is a plain `import()` of a same-origin (or asset-base) file.\n *\n * The table is `{ i: [{ s: entry URL, e: export, l: load }] }` and each\n * marker's `data-island` indexes `i`. Per marker, on its schedule: import the\n * island's entry (the module as `m`, the runtime's `h`), then\n * `h(marker, m[export])`, which calls `hydrateRoot`. A failure is logged and\n * leaves the server-rendered markup in place.\n */\nexport const ISLAND_LOADER_SOURCE =\n 'const d=document,c=JSON.parse(d.getElementById(\"gemi-islands\").textContent),' +\n \"w=f=>(window.requestIdleCallback||setTimeout)(f),\" +\n \"m=(e,o)=>{if(!e.g){e.g=1;import(o.s).then(x=>x.h(e,x.m[o.e])).catch(r=>console.error(r))}};\" +\n 'for(const e of d.querySelectorAll(\"gemi-island\")){const o=c.i[e.dataset.island];if(!o)continue;' +\n 'if(o.l==\"eager\")m(e,o);else if(o.l==\"idle\"||!window.IntersectionObserver||!e.children.length)w(()=>m(e,o));' +\n \"else{const v=new IntersectionObserver(n=>{if(n.some(x=>x.isIntersecting)){v.disconnect();m(e,o)}});for(const k of e.children)v.observe(k)}}\";\n","// Generated by `bun scripts/build-static-navigation.ts` from\n// `internal/staticNavigation/runtime.js`. Do not edit; edit the source and regenerate.\n// No imports: `gemi/testing` (a browser bundle) and `gemi stats` read it too.\n\n/** The sha-256 of the source this was built from, so a test can tell it is stale. */\nexport const STATIC_NAVIGATION_SOURCE_HASH = \"38d6cb0484aa686a8b39adf41de1992d084fe0e504fbc0a1da2f066187451bce\";\n\n/**\n * The navigation runtime (with the island loader) a static view with\n * `.static({ navigation })` inlines. Byte-for-byte constant.\n */\nexport const STATIC_NAVIGATION_SOURCE =\n \"var r=document,i=window,a=history;i.__gemi_nav?.d();var L=new AbortController,h=(t,e,n,s)=>t.addEventListener(e,n,{signal:L.signal,...s}),q=i.__gemi_nav={d:()=>L.abort()},v=(t,e)=>{if(!t.g)t.g=1,import(e.s).then((n)=>{if(t.isConnected)t.r=n.h(t,n.m[e.e])}).catch((n)=>console.error(n))},D=(t)=>{let e=t.getElementById(\\\"gemi-islands\\\");return e?JSON.parse(e.textContent):{i:[]}},R=(t,e)=>{for(let n of t.querySelectorAll(\\\"gemi-island\\\")){let s=e.i[n.dataset.island];if(!s||n.o)continue;if(s.l==\\\"eager\\\")v(n,s);else if(s.l==\\\"idle\\\"||!i.IntersectionObserver||!n.children.length)(i.requestIdleCallback||setTimeout)(()=>v(n,s));else{let l=new IntersectionObserver((m)=>{if(m.some((d)=>d.isIntersecting))l.disconnect(),v(n,s)});for(let m of n.children)l.observe(m)}}},O='meta[name=\\\"gemi-static\\\"]',A=\\\"style:not([data-gemi-fonts])\\\",T=A+\\\",link[rel=stylesheet]\\\",K=`head>${A},body>${A},link[rel=stylesheet]`,I='meta[name]:not([name=viewport]),meta[property],link[rel~=canonical],link[rel~=alternate],link[rel~=icon],link[rel=preload][as=font],style[data-gemi-fonts],script[type=\\\"application/ld+json\\\"]',w=(t)=>t.querySelector(O)?.content,C=(t)=>[...t.querySelectorAll(K)].map((e)=>e.outerHTML).join(),c=(t)=>t.href.split(\\\"#\\\")[0],p=r.createElement(\\\"div\\\");p.setAttribute(\\\"aria-live\\\",\\\"polite\\\");p.style.cssText=\\\"position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap\\\";r.body.append(p);var _=(t)=>{q.p={url:location.href,title:r.title,initial:t},r.dispatchEvent(new CustomEvent(\\\"gemi:page-load\\\",{detail:q.p}))},f=new Map,k=(t)=>{let e=f.get(t);if(!e||Date.now()-e[0]>30000){let n=fetch(t,{headers:{accept:\\\"text/html\\\",\\\"x-gemi-navigate\\\":\\\"1\\\"},signal:AbortSignal.timeout(1e4)}).then((s)=>{if(!s.ok||!/^text\\\\/html/.test(s.headers.get(\\\"content-type\\\"))||new URL(s.url||t).origin!=location.origin)throw s;return s.text().then((l)=>[s.url||t,l])});e=[Date.now(),n],n.catch(()=>f.get(t)==e&&f.delete(t))}if(f.delete(t),f.set(t,e),f.size>10)f.delete(f.keys().next().value);return e[1]},M=(t)=>{let e=t.target.closest?.(\\\"a[href]\\\"),n=e?.getAttribute(\\\"target\\\");if(!e||n&&n!=\\\"_self\\\"||e.hasAttribute(\\\"download\\\")||/(^|\\\\s)external(\\\\s|$)/i.test(e.getAttribute(\\\"rel\\\"))||e.closest(\\\"[data-gemi-reload]\\\"))return;let s=new URL(e.getAttribute(\\\"href\\\"),r.baseURI);if(s.origin==location.origin&&!(s.href.includes(\\\"#\\\")&&c(s)==c(location)))return s},y={},U=()=>Math.random().toString(36).slice(2),E=()=>a.state?.g||a.replaceState({...a.state,g:U()},\\\"\\\"),P=c(location),b=0,N=(t)=>{if(!t.hasAttribute(\\\"tabindex\\\"))t.setAttribute(\\\"tabindex\\\",\\\"-1\\\"),t.addEventListener(\\\"blur\\\",()=>t.removeAttribute(\\\"tabindex\\\"),{once:!0});t.focus({preventScroll:!0})},Y=(t)=>{let e=r.body,n=t.body,s=D(t),l=r.documentElement;r.title=t.title;for(let o of[\\\"lang\\\",\\\"dir\\\"]){let u=t.documentElement.getAttribute(o);if(u==null)l.removeAttribute(o);else l.setAttribute(o,u)}for(let o of r.head.querySelectorAll(I))o.remove();let m=new Set([...r.querySelectorAll(\\\"link[rel=modulepreload]\\\")].map((o)=>o.getAttribute(\\\"href\\\")));r.head.append(...t.head.querySelectorAll(I),...[...t.head.querySelectorAll(\\\"link[rel=modulepreload]\\\")].filter((o)=>!m.has(o.getAttribute(\\\"href\\\"))));let d=new Map([...e.querySelectorAll(\\\"[data-gemi-persist]\\\")].map((o)=>[o.dataset.gemiPersist,o])),g=[...n.querySelectorAll(\\\"[data-gemi-persist]\\\")].map((o)=>[o,d.get(o.dataset.gemiPersist)]).filter((o)=>o[1]);for(let o of[...e.querySelectorAll(\\\"gemi-island\\\")].reverse())if(o.o=1,!g.some((u)=>u[1].contains(o)))try{o.r?.unmount()}catch(u){console.error(u)}for(let[o,u]of g)o.replaceWith(u);for(let o of[...e.childNodes])if(!o.matches?.(T))o.remove();for(let o of[...n.children])if(o.matches(T))o.remove();for(let o of[...e.attributes])e.removeAttribute(o.name);for(let o of n.attributes)e.setAttribute(o.name,o.value);e.append(...n.childNodes,p),R(e,s)},H=async(t,e)=>{let n=++b,s=!e&&y[a.state?.g];a.scrollRestoration=\\\"manual\\\";try{let[l,m]=await k(c(t));if(n!=b)return;let d=new DOMParser().parseFromString(m,\\\"text/html\\\");if(w(d)==null||w(d)!=w(r)||C(d)!=C(r))throw 0;if(e){let o=new URL(l);o.hash=t.hash,a.pushState({g:U()},\\\"\\\",o.href)}P=c(location),Y(d);let g=e&&t.hash&&r.getElementById(decodeURIComponent(t.hash.slice(1)));if(g)g.scrollIntoView({behavior:\\\"instant\\\"});else i.scrollTo({left:s?.[0]||0,top:s?.[1]||0,behavior:\\\"instant\\\"});if(e)N(g||r.querySelector(\\\"h1\\\")||r.querySelector(\\\"main\\\")||r.body);p.textContent=r.title,_(!1)}catch{if(n!=b)return;if(e)location.assign(t.href);else location.reload()}};h(r,\\\"click\\\",(t)=>{if(t.defaultPrevented||t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)return;let e=M(t);if(e&&r.dispatchEvent(new CustomEvent(\\\"gemi:before-navigate\\\",{cancelable:!0,detail:{url:e.href}})))t.preventDefault(),E(),y[a.state.g]=[i.scrollX,i.scrollY],H(e,1)});h(i,\\\"popstate\\\",()=>{if(b++,c(location)==P){let t=y[a.state?.g];if(t)i.scrollTo({left:t[0],top:t[1],behavior:\\\"instant\\\"})}else if(a.state?.g)H(new URL(location.href),0);else location.reload()});h(i,\\\"hashchange\\\",E);h(i,\\\"scroll\\\",()=>a.state?.g&&(y[a.state.g]=[i.scrollX,i.scrollY]),{passive:!0});var S,x=(t)=>{if(r.querySelector(O)?.dataset.prefetch==\\\"none\\\"||navigator.connection?.saveData)return;let e=M(t);if(e&&c(e)!=c(location))k(c(e))};h(r,\\\"pointerover\\\",(t)=>{clearTimeout(S),S=setTimeout(()=>x(t),65)});h(r,\\\"pointerout\\\",()=>clearTimeout(S));h(r,\\\"focusin\\\",x);h(r,\\\"touchstart\\\",x,{passive:!0});E();R(r,D(r));_(!0);\";\n","import { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ISLAND_HYDRATE_SOURCE } from \"../internal/islandRuntime\";\nimport { STATIC_NAVIGATION_SOURCE } from \"../internal/staticNavigationRuntime\";\n\n/**\n * One page `renderStaticDocument` serves to the navigation runtime: its HTML, or\n * a response of another kind to see the runtime fall back to a full load.\n */\nexport interface StaticTestPage {\n html?: string;\n /** Default 200. */\n status?: number;\n /** Default `text/html; charset=utf-8`. */\n contentType?: string;\n /** The URL the response ends at, as after a redirect. Default: the one asked for. */\n url?: string;\n}\n\nexport interface StaticDocumentOptions {\n /** The document's URL, resolved against the test window's origin. Default `/`. */\n url?: string;\n /**\n * What the runtime's `fetch` gets, by path (`/about`, `/about?x=1`) or full\n * URL: HTML, a `StaticTestPage`, or a function for anything else. A URL\n * not listed is a 404.\n */\n pages?:\n | Record<string, string | StaticTestPage>\n | ((url: URL) => string | StaticTestPage | Promise<string | StaticTestPage>);\n /**\n * The island modules, by the entry URL the page's island table names (`s`):\n * each a module whose exports include the islands' components. Hydrated\n * with React, as `<module>?gemi-island` would.\n */\n islands?: Record<string, Record<string, unknown>>;\n}\n\n/** How a navigation ended. */\nexport type StaticNavigationResult =\n /** The runtime swapped the fetched page in. */\n | { kind: \"swap\"; url: string }\n /** The runtime gave up on a swap and asked for a full page load of `url`. */\n | { kind: \"full-load\"; url: string }\n /** The runtime left the click alone: the browser would follow the link itself. */\n | { kind: \"native\" };\n\nexport interface StaticDocumentHandle {\n /**\n * Clicks the page's first `<a>` whose `href` attribute is `href` and waits\n * for the outcome. Throws when there is no such link.\n */\n navigate(href: string, init?: MouseEventInit): Promise<StaticNavigationResult>;\n /** `history.back()`, and waits for the runtime to show that entry. */\n back(): Promise<StaticNavigationResult>;\n /** Every URL the runtime fetched, prefetches included, in order. */\n requests: string[];\n /** Every full page load the runtime asked for. */\n fullLoads: string[];\n /** Stops the runtime and restores what the helper stubbed. */\n dispose(): void;\n}\n\nconst hydrate = new Function(\n \"createElement\",\n \"hydrateRoot\",\n `${ISLAND_HYDRATE_SOURCE};return h;`,\n)(createElement, hydrateRoot) as (marker: Element, component: unknown) => unknown;\n\n/**\n * Puts a static document (`.static({ navigation })`) into the test's DOM and\n * starts its navigation runtime against stubbed pages, so a test can check that\n * a site's links are followed client-side, and what the next page looks like.\n * Needs a DOM (`// @vitest-environment jsdom`).\n *\n * ```ts\n * const html = await (await render(\"/\")).text(); // the page, from your app\n * const page = renderStaticDocument(html, { pages: { \"/about\": aboutHtml } });\n * expect(await page.navigate(\"/about\")).toEqual({ kind: \"swap\", url: \"http://localhost:3000/about\" });\n * expect(document.querySelector(\"h1\")!.textContent).toBe(\"About\");\n * page.dispose();\n * ```\n *\n * The document's own scripts do not run (the helper runs the runtime itself),\n * and a link the runtime leaves alone is not followed. Islands hydrate\n * asynchronously, as in a browser: wrap what follows in `act`, or wait for it.\n */\nexport function renderStaticDocument(\n html: string,\n options: StaticDocumentOptions = {},\n): StaticDocumentHandle {\n const requests: string[] = [];\n const fullLoads: string[] = [];\n let settle: ((result: StaticNavigationResult) => void) | undefined;\n const outcome = () =>\n new Promise<StaticNavigationResult>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(\"the navigation did not finish in 5 s\")), 5000);\n settle = (result) => {\n clearTimeout(timer);\n settle = undefined;\n resolve(result);\n };\n });\n\n history.replaceState(null, \"\", options.url ?? \"/\");\n const parsed = new DOMParser().parseFromString(html, \"text/html\");\n for (const script of Array.from(parsed.querySelectorAll(\"script\"))) {\n if (!/json/.test(script.type)) script.remove();\n }\n document.replaceChild(document.importNode(parsed.documentElement, true), document.documentElement);\n\n const fullLoad = (url: string) => {\n fullLoads.push(url);\n settle?.({ kind: \"full-load\", url });\n };\n const location = {\n get href() {\n return window.location.href;\n },\n get origin() {\n return window.location.origin;\n },\n assign: (url: string) => fullLoad(new URL(url, window.location.href).href),\n reload: () => fullLoad(window.location.href),\n };\n\n const pages = options.pages ?? {};\n const fetch = async (input: string) => {\n const url = new URL(input, window.location.href);\n requests.push(url.href);\n const page =\n typeof pages === \"function\"\n ? await pages(url)\n : (pages[url.href] ?? pages[url.pathname + url.search]);\n const spec: StaticTestPage =\n page === undefined\n ? { status: 404, html: \"not found\", contentType: \"text/plain\" }\n : typeof page === \"string\"\n ? { html: page }\n : page;\n const response = new Response(spec.html ?? \"\", {\n status: spec.status ?? 200,\n headers: { \"content-type\": spec.contentType ?? \"text/html; charset=utf-8\" },\n });\n Object.defineProperty(response, \"url\", { value: new URL(spec.url ?? url.href, url).href });\n return response;\n };\n\n const importIsland = async (src: string) => {\n const module = options.islands?.[src];\n if (!module) throw new Error(`renderStaticDocument: no island module for ${src} in options.islands`);\n return { m: module, h: hydrate };\n };\n\n // The runtime announces a click it takes with `gemi:before-navigate`; one\n // it does not take (or one a listener cancelled) is the browser's, which\n // jsdom would try (and fail) to follow, so it is stopped here, on `window`,\n // after every listener on the way.\n let announced: Event | undefined;\n const onBeforeNavigate = (event: Event) => {\n announced = event;\n };\n document.addEventListener(\"gemi:before-navigate\", onBeforeNavigate);\n const onClick = (event: Event) => {\n const taken = announced !== undefined && !announced.defaultPrevented;\n announced = undefined;\n if (taken || !(event.target as Element).closest?.(\"a[href]\")) return;\n event.preventDefault();\n settle?.({ kind: \"native\" });\n };\n window.addEventListener(\"click\", onClick);\n const onPageLoad = (event: Event) => {\n const detail = (event as CustomEvent<{ url: string; initial: boolean }>).detail;\n if (!detail.initial) settle?.({ kind: \"swap\", url: detail.url });\n };\n document.addEventListener(\"gemi:page-load\", onPageLoad);\n\n // Not implemented by jsdom.\n const scrollTo = window.scrollTo;\n window.scrollTo = () => {};\n const scrollIntoView = Element.prototype.scrollIntoView;\n Element.prototype.scrollIntoView = function () {};\n\n const source = STATIC_NAVIGATION_SOURCE.replace(\"import(\", \"__import(\");\n new Function(\"__import\", \"location\", \"fetch\", source)(importIsland, location, fetch);\n\n return {\n requests,\n fullLoads,\n async navigate(href, init) {\n const link = Array.from(document.querySelectorAll(\"a[href]\")).find(\n (a) => a.getAttribute(\"href\") === href,\n );\n if (!link) throw new Error(`renderStaticDocument: no <a href=\"${href}\"> on the page`);\n const done = outcome();\n link.dispatchEvent(new MouseEvent(\"click\", { bubbles: true, cancelable: true, ...init }));\n return await done;\n },\n async back() {\n const done = outcome();\n history.back();\n return await done;\n },\n dispose() {\n (window as unknown as { __gemi_nav?: { d: () => void } }).__gemi_nav?.d();\n window.removeEventListener(\"click\", onClick);\n document.removeEventListener(\"gemi:before-navigate\", onBeforeNavigate);\n document.removeEventListener(\"gemi:page-load\", onPageLoad);\n window.scrollTo = scrollTo;\n Element.prototype.scrollIntoView = scrollIntoView;\n },\n };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import{fa as B,ga as D}from"../chunk-pemhmjzp.js";import"../chunk-qy8c0gzr.js";import"../chunk-ep0baa9g.js";import{Aa as J}from"../chunk-c0v61fp5.js";import"../chunk-er8acmch.js";import"../chunk-tewhms9w.js";function N(){return{name:"gemi-custom-request",enforce:"pre",async transform(G,H){let j=H.split("?",1)[0];if(j.includes("/node_modules/")||!B.test(j))return null;return{code:await D(G),map:null}}}}export{N as gemiRequestPlugin,J as gemiIslandPlugin};
2
+ import{fa as B,ga as D}from"../chunk-j8vc3bze.js";import"../chunk-0tc0j3pf.js";import"../chunk-9p7eq7bz.js";import{Aa as J}from"../chunk-w0jfqnw9.js";import"../chunk-dmj6ysym.js";import"../chunk-ay8nat73.js";function N(){return{name:"gemi-custom-request",enforce:"pre",async transform(G,H){let j=H.split("?",1)[0];if(j.includes("/node_modules/")||!B.test(j))return null;return{code:await D(G),map:null}}}}export{N as gemiRequestPlugin,J as gemiIslandPlugin};
3
3
 
4
4
  //# debugId=C11E8611E15123D864756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemi",
3
- "version": "0.122.0",
3
+ "version": "0.123.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Enes Tufekci <enes@gemijs.dev>",
@@ -6,7 +6,7 @@ description: Best practices for the gemi framework (Bun + Vite + React 19 SSR fu
6
6
  # gemi Best Practices
7
7
 
8
8
  Rules for building on **gemi**, the full-stack TypeScript framework this app runs on
9
- (Bun + Vite, React 19 SSR). 45 rules across 11 categories, ordered by impact.
9
+ (Bun + Vite, React 19 SSR). 47 rules across 11 categories, ordered by impact.
10
10
 
11
11
  Every rule is derived from the gemi documentation and from patterns that recur across
12
12
  gemi codebases. This skill ships with the `gemi` package, so it describes the
@@ -144,6 +144,7 @@ carried for free.
144
144
  - `query-mutate-over-refetch` - Write the cache with `mutate` / `useMutate` instead of refetching
145
145
  - `query-debounce-search-variant` - Debounce a value before it becomes a query variant
146
146
  - `query-revalidate-on-focus` - Opt in only for cross-tab-mutable data; let `staleTime` gate it
147
+ - `query-live-over-polling` - Prefer `live` / `useChannelInvalidate` over a bare `refetchUntil` poll
147
148
 
148
149
  ### 3. Data Access — ORM (HIGH)
149
150
 
@@ -209,6 +210,7 @@ Counter-intuitive behaviours that produce silent failures rather than errors:
209
210
  | A `defineDictionary` behind a helper passes tests and fails the **build** | `i18n-define-dictionary-inline` |
210
211
  | `Promise.all` inside a transaction shares one reserved connection | `orm-transaction-sequential` |
211
212
  | A lazy query never refetches when its variant changes | `query-lazy-vs-mount-gate` |
213
+ | Broadcasts are not replayed: `useChannel` with `on` and no `onResync` goes stale after a reconnect | `query-live-over-polling` |
212
214
  | A prefetch whose `search` differs primes a slot nothing reads | `payload-prefetch-mirrors-usequery` |
213
215
  | A class name is minified in prod — jobs and services need a static string | `service-static-token-and-name` |
214
216
  | `paginate()` silently caps `perPage` at 100 | `orm-paginate-helper` |
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: Prefer a Channel Over a Bare refetchUntil Poll
3
+ impact: HIGH
4
+ impactDescription: removes a request per second per open tab, and shows changes in milliseconds
5
+ tags: query, broadcasting, polling, realtime
6
+ ---
7
+
8
+ ## Prefer a Channel Over a Bare refetchUntil Poll
9
+
10
+ When the server knows the moment something changes (a job finished, a webhook landed,
11
+ another user saved), emit a broadcast there and let the view refetch on it, instead of
12
+ polling every second or two. Keep the poll as the fallback: `live` pauses it only while
13
+ the channel is open.
14
+
15
+ Broadcasts are hints, not data. Never render from an event's payload alone: refetch
16
+ over HTTP, which `useChannelInvalidate` and `live` do for you, including after every
17
+ reconnect, when events may have been missed.
18
+
19
+ **Incorrect (polls every 2 s for as long as the build runs, in every open tab):**
20
+
21
+ ```tsx
22
+ const { data } = useQuery("/posts/:postId/build", { params: { postId } }, {
23
+ refetchUntil: (d) => (d.status === "building" ? 2_000 : 0),
24
+ });
25
+ ```
26
+
27
+ **Correct (the server emits; the poll only runs while the socket is down):**
28
+
29
+ ```tsx
30
+ // Server, where the status changes:
31
+ Broadcast.to("post.:postId", { postId }).emit("build");
32
+
33
+ // Client:
34
+ const { data } = useQuery("/posts/:postId/build", { params: { postId } }, {
35
+ live: ["post.:postId", { postId }],
36
+ refetchUntil: (d) => (d.status === "building" ? 2_000 : 0),
37
+ });
38
+ ```
39
+
40
+ **Also correct (several queries refresh on one channel):**
41
+
42
+ ```tsx
43
+ import { useChannelInvalidate } from "gemi/client";
44
+
45
+ useChannelInvalidate("page.:pageId", { pageId }, ["/pages/:pageId", "/pages/:pageId/pictures"]);
46
+ ```
47
+
48
+ **Incorrect (`on` without `onResync` goes stale silently after a reconnect):**
49
+
50
+ ```tsx
51
+ useChannel("site.:siteId", { params: { siteId } }, {
52
+ on: { changed: () => mutate({ path: "/sites/:siteId", params: { siteId } }) },
53
+ });
54
+ ```
55
+
56
+ Add `onResync` with the same refetch, or use `useChannelInvalidate`.
@@ -1,5 +0,0 @@
1
- // @bun
2
- import{constants as X}from"os";var U=Symbol.for("gemi.server.shutdown");function P(){let j=globalThis;return j[U]??={shuttingDown:!1,drained:!1},j[U]}function Y(){return P().shuttingDown}function Z(){P().shuttingDown=!0}function $(){P().drained=!0}function y(j=process.env){let z={timeoutMs:Q(j.GEMI_SHUTDOWN_TIMEOUT,20),delayMs:Q(j.GEMI_SHUTDOWN_DELAY,0),providerTimeoutMs:Q(j.GEMI_SHUTDOWN_PROVIDER_TIMEOUT,5)};if(z.delayMs>0&&z.delayMs>=z.timeoutMs)console.warn(`[gemi] GEMI_SHUTDOWN_DELAY (${z.delayMs/1000}s) leaves no time to drain within GEMI_SHUTDOWN_TIMEOUT (${z.timeoutMs/1000}s); in-flight requests will be abandoned on every shutdown. Raise the timeout.`);return z}function Q(j,z){let B=j===void 0||j.trim()===""?NaN:Number(j);if(Number.isFinite(B)&&B>=0)return B*1000;if(j!==void 0)console.warn(`[gemi] Ignoring shutdown setting "${j}": expected a number of seconds.`);return z*1000}async function G(j){let{server:z,settings:B}=j,H=Date.now()+B.timeoutMs;Z(),console.log(`[gemi] Shutting down: draining in-flight requests (up to ${B.timeoutMs/1000}s).`);let F=!0;if(z){if(B.delayMs>0)await M(Math.min(B.delayMs,H-Date.now()));let O=z.stop(),R=H-Date.now();if(F=R>0?await C(O,R):z.pendingRequests===0,!F)console.error(`[gemi] Shutdown grace period elapsed with ${z.pendingRequests} request(s) still in flight; abandoning them.`),z.stop(!0).catch(()=>{})}$();let K=await j.shutdownProviders({timeoutMs:B.providerTimeoutMs}),J=F&&K.failed.length===0&&K.timedOut.length===0;return console.log(`[gemi] Shutdown ${J?"complete":"finished with errors"}.`),J?0:1}async function C(j,z){if(z<=0)return!1;let B,H=await Promise.race([j.then(()=>!0,()=>!0),new Promise((F)=>{B=setTimeout(()=>F(!1),z)})]);return clearTimeout(B),H}function M(j){return new Promise((z)=>setTimeout(z,Math.max(0,j)))}function f(j){if(!Y())return j;try{return j.headers.set("Connection","close"),j}catch{let z=new Response(j.body,j);return z.headers.set("Connection","close"),z}}function _(j){return async(z,B)=>{if(P().drained)return new Response("Service Unavailable",{status:503,headers:{Connection:"close"}});return f(await j(z,B))}}var V=Symbol.for("gemi.server.signalsInstalled"),q=1000;function w(j,z={}){let B=globalThis;if(B[V])return;B[V]=!0;let H=z.repeatWindowMs??q,F,K=(J)=>{if(F!==void 0){if(Date.now()-F<H)return;console.error(`[gemi] ${J} received again; exiting without waiting for the drain.`),process.exit(128+X.signals[J])}F=Date.now(),console.log(`[gemi] ${J} received.`),j().then((O)=>process.exit(O),(O)=>{console.error("[gemi] Shutdown failed:",O),process.exit(1)})};process.on("SIGTERM",K),process.on("SIGINT",K)}
3
- export{Y as Yi,Z as Zi,y as _i,G as $i,_ as aj,w as bj};
4
-
5
- //# debugId=5354B4538A37316B64756E2164756E21
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../server/shutdown.ts"],
4
- "sourcesContent": [
5
- "import { constants } from \"node:os\";\nimport type { ShutdownReport } from \"../foundation/Application\";\nimport type { Instrumentation } from \"./types\";\n\n// On `globalThis` under a registry symbol rather than in a module variable, so\n// the flag an app's health route reads is the one the server set even when two\n// copies of this module are loaded — the built CLI bundles its own gemi (see\n// `app/App.ts`), and `bun --hot` re-evaluates every module on a reload.\nconst STATE = Symbol.for(\"gemi.server.shutdown\");\n\ntype ShutdownState = { shuttingDown: boolean; drained: boolean };\n\nfunction state(): ShutdownState {\n const global = globalThis as { [STATE]?: ShutdownState };\n global[STATE] ??= { shuttingDown: false, drained: false };\n return global[STATE];\n}\n\n/**\n * True from the moment this server process has been told to stop — before it\n * stops accepting connections — until it exits.\n *\n * gemi has no health endpoint of its own, so taking an instance out of a load\n * balancer is the app's health route's job: answer it with a `503` while this\n * is true, and the platform's probe pulls the instance while the in-flight\n * requests are still draining. Every response sent in that window also carries\n * `Connection: close`, so a client that honours it opens its next request on a\n * new connection — one the load balancer can route elsewhere — instead of\n * reusing this one.\n */\nexport function isShuttingDown(): boolean {\n return state().shuttingDown;\n}\n\nexport function markShuttingDown() {\n state().shuttingDown = true;\n}\n\n/**\n * The request drain is over and the providers' `shutdown()` may be running:\n * from here on a request must not reach the app. See `serveForShutdown`.\n */\nexport function markDrained() {\n state().drained = true;\n}\n\n/** Test seam: a fresh process starts false, and so should each test. */\nexport function resetShuttingDown() {\n state().shuttingDown = false;\n state().drained = false;\n}\n\nexport type ShutdownSettings = {\n /**\n * From the signal to the end of the request drain, in milliseconds — the\n * delay below included. Requests still in flight when it runs out are\n * abandoned. `GEMI_SHUTDOWN_TIMEOUT`, in seconds; default 20. `0` means\n * \"close the listener and do not wait\": a shutdown with nothing in flight is\n * still a clean one.\n */\n timeoutMs: number;\n /**\n * How long to keep accepting new requests after `isShuttingDown()` turns\n * true, in milliseconds, so a health probe can see the `503` and the load\n * balancer stop routing here before the listener closes.\n * `GEMI_SHUTDOWN_DELAY`, in seconds; default 0.\n */\n delayMs: number;\n /**\n * The shared deadline for every provider's `shutdown()`, in milliseconds,\n * after the drain. `GEMI_SHUTDOWN_PROVIDER_TIMEOUT`, in seconds; default 5.\n * `0` skips the hooks — deliberately, so it is not reported as a failure.\n */\n providerTimeoutMs: number;\n};\n\n/**\n * 20s to drain plus 5s for providers is 25s: under the 30s that Kubernetes,\n * Azure Container Apps and ECS give by default between `SIGTERM` and `SIGKILL`,\n * with 5s left for the `gemi start` relay, the process exit and clock skew\n * between the platform's timer and ours. A platform with a shorter window\n * (Cloud Run's 10s, Fly's 5s) needs these lowered, not the other way round —\n * a drain that outlives the grace period ends in the same `SIGKILL` as no drain.\n *\n * The delay defaults to 0 because it only helps where a probe is watching, and\n * otherwise every shutdown would pay it for nothing.\n */\nexport function shutdownSettings(\n env: Record<string, string | undefined> = process.env,\n): ShutdownSettings {\n const settings = {\n timeoutMs: seconds(env.GEMI_SHUTDOWN_TIMEOUT, 20),\n delayMs: seconds(env.GEMI_SHUTDOWN_DELAY, 0),\n providerTimeoutMs: seconds(env.GEMI_SHUTDOWN_PROVIDER_TIMEOUT, 5),\n };\n // Warned, not clamped: the delay is sized to a probe, the timeout to the\n // platform's grace period, and which one is wrong only the operator knows.\n // Left alone, every shutdown spends the whole timeout in the delay and then\n // abandons whatever is in flight without draining it.\n if (settings.delayMs > 0 && settings.delayMs >= settings.timeoutMs) {\n console.warn(\n `[gemi] GEMI_SHUTDOWN_DELAY (${settings.delayMs / 1000}s) leaves no time to drain within GEMI_SHUTDOWN_TIMEOUT (${settings.timeoutMs / 1000}s); in-flight requests will be abandoned on every shutdown. Raise the timeout.`,\n );\n }\n return settings;\n}\n\nfunction seconds(value: string | undefined, fallback: number): number {\n const parsed = value === undefined || value.trim() === \"\" ? NaN : Number(value);\n if (Number.isFinite(parsed) && parsed >= 0) return parsed * 1000;\n if (value !== undefined) {\n console.warn(`[gemi] Ignoring shutdown setting \"${value}\": expected a number of seconds.`);\n }\n return fallback * 1000;\n}\n\n/** The part of `Bun.Server` a drain needs. */\nexport type Stoppable = {\n stop(closeActiveConnections?: boolean): Promise<void>;\n readonly pendingRequests: number;\n};\n\n/**\n * The shutdown sequence, in its order:\n *\n * 1. `isShuttingDown()` turns true — first, so the health route answers `503`\n * and responses carry `Connection: close` while the listener is still open.\n * 2. After `delayMs`, the listener closes (`server.stop()` without force):\n * new connections are refused, while every request in flight — a streamed\n * response included, to its last chunk — runs to completion.\n * 3. That completion is awaited until `timeoutMs` from the start.\n * 4. From here a request is refused with a `503` before it reaches the app\n * (`serveForShutdown`), and every provider's `shutdown()` runs, reverse\n * order, `providerTimeoutMs`.\n *\n * Resolves with the exit code: 0 when every step finished in time, 1 when the\n * drain was cut short or a provider failed or overran.\n *\n * Two things `stop()` does not do, both measured on Bun 1.3.14, and both why\n * the process exit that follows is what actually ends the server:\n *\n * - It does not close a kept-alive connection. One that is idle when the\n * listener closes stays open, and a request sent down it afterwards is\n * served. That is what the `Connection: close` on every response in the\n * window is for: a client (or a proxy in front) that honours it opens its\n * next request on a new connection, which the closed listener refuses. A\n * client that does not honour it is what step 4's `503` is for — Bun does\n * not close the socket after a `Connection: close` response either (#566).\n * - `stop(true)` issued after a graceful `stop()` does not cut the remaining\n * requests off; it waits for the same ones. It is still called when the\n * drain times out, for a Bun that does, but nothing here depends on it. Nor\n * does it close an idle kept-alive connection. Only a `stop(true)` issued\n * *first* does that, and it cuts off the requests in flight with it, which\n * is the drain given up — so the idle ones are refused, not closed.\n */\nexport async function drain(params: {\n server: Stoppable | undefined;\n shutdownProviders: (options: { timeoutMs: number }) => Promise<ShutdownReport>;\n settings: ShutdownSettings;\n}): Promise<number> {\n const { server, settings } = params;\n const deadline = Date.now() + settings.timeoutMs;\n\n markShuttingDown();\n console.log(\n `[gemi] Shutting down: draining in-flight requests (up to ${settings.timeoutMs / 1000}s).`,\n );\n\n let drained = true;\n if (server) {\n if (settings.delayMs > 0) {\n await sleep(Math.min(settings.delayMs, deadline - Date.now()));\n }\n const stopped = server.stop();\n // No time left to wait — a `GEMI_SHUTDOWN_TIMEOUT` of 0, or a delay that\n // spent all of it — is \"do not wait for what is in flight\", and that is\n // only a failure when something *is* in flight. Reading it as one made\n // every clean shutdown log an abandonment and exit 1 for the operator who\n // zeroed the budget to fit a 5s grace period, with nothing to abandon.\n const remaining = deadline - Date.now();\n drained =\n remaining > 0 ? await settlesWithin(stopped, remaining) : server.pendingRequests === 0;\n if (!drained) {\n console.error(\n `[gemi] Shutdown grace period elapsed with ${server.pendingRequests} request(s) still in flight; abandoning them.`,\n );\n void server.stop(true).catch(() => {});\n }\n }\n\n // Whether or not the drain finished: a request that arrives now, down a\n // kept-alive connection the listener's close left open, would run against\n // providers that are being shut down underneath it.\n markDrained();\n const report = await params.shutdownProviders({ timeoutMs: settings.providerTimeoutMs });\n const clean = drained && report.failed.length === 0 && report.timedOut.length === 0;\n console.log(`[gemi] Shutdown ${clean ? \"complete\" : \"finished with errors\"}.`);\n return clean ? 0 : 1;\n}\n\nasync function settlesWithin(promise: Promise<unknown>, ms: number): Promise<boolean> {\n // `drain` decides for itself what a budget that is already spent means, so\n // this is only a guard: a race against `setTimeout(0)` is a coin toss, and\n // \"did not settle within no time at all\" is the honest answer to it.\n if (ms <= 0) return false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const result = await Promise.race([\n promise.then(\n () => true,\n () => true,\n ),\n new Promise<false>((resolve) => {\n timer = setTimeout(() => resolve(false), ms);\n }),\n ]);\n clearTimeout(timer);\n return result;\n}\n\nfunction sleep(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n}\n\n/**\n * `Connection: close` on a response sent while shutting down, so the client's\n * next request opens a connection — which the load balancer can send elsewhere\n * — instead of reusing this one.\n *\n * The spec makes some responses' headers immutable (a `Response.redirect`, a\n * proxied `fetch`), and setting one throws. Bun 1.3.14 lets both be set, so\n * the copy is for a Bun that follows the spec — a throw here would turn every\n * such response in the drain window into a 500.\n */\nexport function closeConnectionWhileShuttingDown(res: Response): Response {\n if (!isShuttingDown()) return res;\n try {\n res.headers.set(\"Connection\", \"close\");\n return res;\n } catch {\n const copy = new Response(res.body, res);\n copy.headers.set(\"Connection\", \"close\");\n return copy;\n }\n}\n\n/**\n * What every response goes through while this process serves: the app's\n * instrumentation, with the two changes a shutdown makes to it.\n *\n * - While the requests in flight drain, a response carries `Connection: close`\n * (`closeConnectionWhileShuttingDown`).\n * - Once they have drained, a request is answered `503` with\n * `Connection: close` and reaches neither the app nor its instrumentation.\n * The listener is closed by then, so only a kept-alive connection can still\n * deliver one, and `server.stop()` leaves those open. Serving it would run a\n * handler against providers whose `shutdown()` is closing the pool, the\n * queue or the cache it uses — until the process exits. The instrumentation\n * is skipped too because it is app code, and a tracer or error reporter is\n * exactly what a provider flushes and closes at this point.\n */\nexport function serveForShutdown(instrumentation: Instrumentation): Instrumentation {\n return async (req, next) => {\n if (state().drained) {\n return new Response(\"Service Unavailable\", {\n status: 503,\n headers: { Connection: \"close\" },\n });\n }\n return closeConnectionWhileShuttingDown(await instrumentation(req, next));\n };\n}\n\n// Installed once per process, not once per `Server`: a listener per instance\n// would each start a shutdown of its own, and read the other's signal as the\n// second one.\nconst SIGNALS_INSTALLED = Symbol.for(\"gemi.server.signalsInstalled\");\n\n/**\n * A repeat of the first signal inside this window is the same shutdown\n * delivered twice, not an operator asking to skip the drain. One shutdown\n * routinely arrives more than once, milliseconds apart: Ctrl+C on\n * `bun run start` reaches the server directly (the whole foreground group is\n * signalled), and again through `gemi start`'s relay — twice over, since\n * `bun run` forwards it to `gemi start` too. systemd's default\n * `KillMode=control-group` and `tini -g` do the same to every process in the\n * group. A person pressing Ctrl+C again is well outside a second.\n */\nconst REPEAT_WINDOW_MS = 1000;\n\n/**\n * `SIGTERM` or `SIGINT` runs `stop` and exits with the code it resolves with.\n * Another signal of either kind, more than `repeatWindowMs` after the first,\n * exits at once with `128 + n` — an operator pressing Ctrl+C again does not\n * want to wait out the grace period. Without a listener the first signal would\n * do that too: the default action for both is to terminate the process on the\n * spot.\n */\nexport function installShutdownSignals(\n stop: () => Promise<number>,\n options: { repeatWindowMs?: number } = {},\n) {\n const global = globalThis as { [SIGNALS_INSTALLED]?: boolean };\n if (global[SIGNALS_INSTALLED]) return;\n global[SIGNALS_INSTALLED] = true;\n\n const repeatWindowMs = options.repeatWindowMs ?? REPEAT_WINDOW_MS;\n let receivedAt: number | undefined;\n const onSignal = (signal: \"SIGTERM\" | \"SIGINT\") => {\n if (receivedAt !== undefined) {\n if (Date.now() - receivedAt < repeatWindowMs) return;\n console.error(`[gemi] ${signal} received again; exiting without waiting for the drain.`);\n process.exit(128 + constants.signals[signal]);\n }\n receivedAt = Date.now();\n console.log(`[gemi] ${signal} received.`);\n stop().then(\n (code) => process.exit(code),\n (error) => {\n console.error(\"[gemi] Shutdown failed:\", error);\n process.exit(1);\n },\n );\n };\n process.on(\"SIGTERM\", onSignal);\n process.on(\"SIGINT\", onSignal);\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "5354B4538A37316B64756E2164756E21",
9
- "names": []
10
- }
@@ -1,24 +0,0 @@
1
- // @bun
2
- import{S as q,T as KJ,U as OJ}from"./chunk-shfe0b22.js";import{oa as B,sa as L,ta as UJ}from"./chunk-a22h5cby.js";import"./chunk-kk18xqgc.js";import{De as QJ,Ee as A,Vc as a}from"./chunk-3s21p9n2.js";import"./chunk-65beps2q.js";import"./chunk-m53fgf20.js";import{Ef as o,Ff as n,If as i}from"./chunk-4acpw92g.js";import{Jg as HJ}from"./chunk-cca0grq3.js";import"./chunk-m83zm86m.js";import"./chunk-rhek27sj.js";import"./chunk-19dpzp4y.js";import"./chunk-yds7zwc3.js";import"./chunk-meg8rtr6.js";import"./chunk-h43jp8ws.js";import"./chunk-s07v65x6.js";import"./chunk-gc2dz11k.js";import"./chunk-cskqet01.js";import"./chunk-fenc5tpv.js";import"./chunk-nz8z8jr1.js";import"./chunk-4fx8p02c.js";import{Ui as e}from"./chunk-er8acmch.js";import"./chunk-2n3pgfnb.js";import{fj as YJ}from"./chunk-d5f1m3d2.js";import{gj as N}from"./chunk-rhvc1axc.js";import"./chunk-tewhms9w.js";import{createHash as X2}from"crypto";import{join as w,resolve as VJ,sep as zJ}from"path";import{Duplex as TJ}from"stream";import{constants as P,createBrotliCompress as RJ,createGzip as bJ}from"zlib";var kJ=["br","gzip"],SJ=1024,wJ=5,vJ=19,EJ=6;function yJ(J){if(J===void 0)return 1;if(!/^\d+(\.\d{0,3})?$/.test(J.trim()))return 1;let $=Number(J.trim());return Number.isFinite($)?Math.min($,1):1}function gJ(J){if(J===null||J===void 0)return null;let $=new Map;for(let Z of J.split(",")){let[X,...Q]=Z.split(";"),H=X.trim().toLowerCase();if(H==="")continue;let j=1;for(let Y of Q){let K=Y.indexOf("=");if(K===-1)continue;if(Y.slice(0,K).trim().toLowerCase()==="q"){j=yJ(Y.slice(K+1));break}}$.set(H,j)}return $}function b(J,$=kJ){let Z=gJ(J);if(!Z)return null;let X=Z.get("*"),Q=null,H=0;for(let j of $){let Y=Z.get(j)??X??0;if(Y>H)Q=j,H=Y}return Q}function fJ(J,$){if(!J)return!1;return J.split(",").some((Z)=>Z.trim().toLowerCase().split("=")[0].trim()===$)}function mJ(J,$){if(J==="HEAD")return!1;if($.status<200||$.status===204||$.status===206||$.status===304)return!1;if($.headers.has("Content-Range"))return!1;let Z=$.headers.get("Content-Type");if(!Z||!/^\s*text\/html\s*(;|$)/i.test(Z))return!1;if(!$.body)return!1;let X=$.headers.get("Content-Encoding");if(X&&X.trim().toLowerCase()!=="identity")return!1;if(fJ($.headers.get("Cache-Control"),"no-transform"))return!1;let Q=$.headers.get("Content-Length");if(Q!==null&&Number(Q)<SJ)return!1;return!0}function uJ(J){let $=J==="br"?RJ({params:{[P.BROTLI_PARAM_QUALITY]:wJ,[P.BROTLI_PARAM_LGWIN]:vJ},flush:P.BROTLI_OPERATION_FLUSH}):bJ({level:EJ,flush:P.Z_SYNC_FLUSH});return TJ.toWeb($)}function hJ(J){let $=J.get("Vary");if(!$){J.set("Vary","Accept-Encoding");return}let Z=$.split(",").map((X)=>X.trim().toLowerCase());if(Z.includes("*")||Z.includes("accept-encoding"))return;J.append("Vary","Accept-Encoding")}function c(J,$){if(!mJ(J.method,$))return $;let Z=new Headers($.headers);hJ(Z);let X=b(J.headers.get("Accept-Encoding"));if(!X)return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z});let Q;try{Q=uJ(X)}catch{return new Response($.body,{status:$.status,statusText:$.statusText,headers:Z})}return Z.set("Content-Encoding",X),Z.delete("Content-Length"),new Response($.body.pipeThrough(Q),{status:$.status,statusText:$.statusText,headers:Z})}import{readdir as iJ,stat as t}from"fs/promises";import{join as aJ}from"path";import{mkdir as O2,readFile as cJ,stat as V2,writeFile as z2}from"fs/promises";import{dirname as L2,join as pJ}from"path";import{promisify as p}from"util";import{brotliCompress as dJ,constants as I2,gzip as lJ}from"zlib";var oJ=".vite/static-assets.json";var d={br:".br",gzip:".gz"},nJ=["br","gzip"],P2=p(dJ),_2=p(lJ);async function l(J){let $;try{$=await cJ(pJ(J,oJ),"utf8")}catch{return null}let Z;try{Z=JSON.parse($)?.files}catch{return null}if(!Z||typeof Z!=="object")return null;let X={files:{}};for(let[Q,H]of Object.entries(Z)){if(!Array.isArray(H))continue;X.files[Q]=nJ.filter((j)=>H.includes(j))}return X}function k(J){let $=J.toString(),Z=new Bun.CryptoHasher("sha1");return Z.update($),Z.digest("hex")}async function r(J){let $=new Set;async function Z(X,Q){let H;try{H=await iJ(X,{withFileTypes:!0})}catch{return}for(let j of H){if(j.name.startsWith("."))continue;if(Q&&j.name==="assets")continue;let Y=aJ(X,j.name),K=j.isDirectory(),V=j.isFile();if(j.isSymbolicLink())try{let F=await t(Y);K=F.isDirectory(),V=F.isFile()}catch{continue}if(V)$.add(Y);else if(K&&!j.isSymbolicLink())await Z(Y,!1)}}return await Z(J,!0),$}var tJ="public, max-age=31536000, must-revalidate",rJ="public, max-age=31536000, immutable";async function sJ(J,$,Z){if(Z.length===0||J.headers.has("Range"))return null;let X=b(J.headers.get("Accept-Encoding"),Z);if(!X)return null;let Q=$+d[X];try{let H=await t(Q);return H.isFile()?{encoding:X,path:Q,size:H.size}:null}catch{return null}}async function s(J,$,Z={}){let X=Bun.file($),Q=Z.encodings??[],H={"Content-Type":X.type||"application/octet-stream","Cache-Control":Z.immutable?rJ:tJ,"Accept-Ranges":"bytes"};if(Q.length>0)H.Vary="Accept-Encoding";let j=await sJ(J,$,Q);if(j){let V=Bun.file(j.path);return new Response(V.stream(),{headers:{...H,"Content-Encoding":j.encoding,"Content-Length":String(j.size),ETag:`${k(V.lastModified)}-${j.encoding}`}})}let Y=X.size;H.ETag=k(X.lastModified);let K=o(J.headers.get("Range"));if(K){let V=n(K,Y);if(!V)return a(Y);return new Response(X.slice(V.start,V.end+1).stream(),{status:206,headers:{...H,"Content-Range":i(V.start,V.end,Y),"Content-Length":String(V.length)}})}return new Response(X.stream(),{headers:{...H,"Content-Length":String(Y)}})}import{stat as Q2}from"fs/promises";var _="app/client.tsx";function JJ(J,$=B){let Z=J[_]?.file;if(!Z)return;return{module:L(Z,$),preload:M(J,_,$)}}function M(J,$,Z=B){let X=[],Q=new Set,H=(j)=>{if(Q.has(j))return;Q.add(j);let Y=J[j];if(!Y?.file)return;X.push(L(Y.file,Z));for(let K of Y.imports??[])H(K)};return H($),X}function $J(J,$){let Z=[],X=new Set,Q=new Set,H=(j)=>{if(Q.has(j))return;Q.add(j);let Y=J[j];if(!Y)return;for(let K of Y.imports??[])H(K);for(let K of Y.css??[])if(!X.has(K))X.add(K),Z.push(K)};return H($),Z}function ZJ(J,$=B){let Z=new Map;return(X)=>{if(!Z.has(X)){let Q=`${X}${e}`,H=J[Q]?.file;Z.set(X,H?{src:L(H,$),preload:M(J,Q,$)}:void 0)}return Z.get(X)}}function XJ(J,$=30000){let Z=JSON.stringify("gemi:chunk-reload"),X=JSON.stringify(`Failed to fetch dynamically imported module: ${J}`);return`var r=!1;try{var s=window.sessionStorage,m=JSON.parse(s.getItem(${Z})||"null");var d=m?Date.now()-m.at:NaN;if(navigator.onLine!==!1&&!(d>=0&&d<${$})){s.setItem(${Z},JSON.stringify({url:window.location.href,at:Date.now()}));r=!0}}catch(e){}if(r)window.location.reload();else throw new Error(${X});export {}`}var eJ=new RegExp(`^${QJ}/.+\\.m?js$`);function J2(J){return eJ.test(J)}function WJ(J){if(J2(J))return new Response(XJ(J),{headers:{"Content-Type":"application/javascript","Cache-Control":"no-store"}});if(A(J))return new Response("Not found",{status:404});return}var $2=`<!DOCTYPE html>
3
- <html lang="en">
4
- <head>
5
- <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Internal Server Error</title>
8
- <style>
9
- body { font-family: system-ui, sans-serif; margin: 0; min-height: 100vh; display: grid; place-items: center; }
10
- main { text-align: center; padding: 16px; }
11
- h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
12
- p { margin: 0; opacity: 0.7; }
13
- </style>
14
- </head>
15
- <body>
16
- <main>
17
- <h1>Something went wrong</h1>
18
- <p>Please try again later.</p>
19
- </main>
20
- </body>
21
- </html>`;function S(J,$,Z){if(!Z)console.error(J);else try{Z(J instanceof Error?J:Error(String(J),{cause:J}))}catch(X){console.error(J),console.error(X)}if(N($))return new Response(JSON.stringify({error:HJ("server_error","Internal Server Error",500)}),{status:500,headers:{"Content-Type":"application/json"}});return new Response($2,{status:500,headers:{"Content-Type":"text/html; charset=utf-8"}})}var Z2={kind:"none"};function jJ(J){let $=(J??"").trim().toLowerCase();if($===""||$==="false"||$==="off"||$==="0")return Z2;if($==="true")return{kind:"all"};if(/^[1-9]\d*$/.test($))return{kind:"hops",hops:Number($)};throw Error(`GEMI_TRUST_PROXY must be "false", "true" or the number of proxies in front of the app (e.g. 1). Got ${JSON.stringify(J)}.`)}function GJ(J,$,Z){if(Z.kind==="all"){if(!J.get("x-forwarded-for")&&$)J.set("x-forwarded-for",$);return}J.delete("x-real-ip");let X=$;if(Z.kind==="hops"){let Q=(J.get("x-forwarded-for")??"").split(",").map((j)=>j.trim()).filter(Boolean),H=Q.length-Z.hops;X=H>=0?Q[H]:Q[0]??$}if(X)J.set("x-forwarded-for",X);else J.delete("x-forwarded-for")}var D=YJ(),W2=w(D,"app"),x=w(D,"dist"),C=w(x,"client");function H2(J){let $;try{$=decodeURIComponent(J)}catch{return null}let Z=VJ(C,"."+$.replace("/assets/assets","/assets"));return Z.startsWith(C+zJ)?Z:null}async function j2(J){try{return(await Q2(J)).isFile()}catch{return!1}}async function W3(J,$){let Z=await import(`${x}/client/.vite/manifest.json`),X=await import(`${x}/server/.vite/manifest.json`),Q=await UJ(`${x}/client`);process.env.ROOT_DIR=D,process.env.APP_DIR=W2,process.env.DIST_DIR=x;let H={},j={},Y={},K={},V={},F=(W,G)=>`"${W}": () => import("${G}")`,v=[];for(let W of["404",...J.getFlatComponentTree.call(J)]){let G=X[`app/views/${W}.tsx`];if(!G?.file){console.log(`Server file not found for ${W}`),console.log(G);let z=Object.keys(X),I=`app/views/${W}.tsx`;console.log(`${I} not found in server manifest`),console.log(z)}let U=await import(`${process.env.DIST_DIR}/server/${G?.file}`);H[W]=U.default,j[W]=U.OpenGraph,Y[W]=U;let O=Z[`app/views/${W}.tsx`];if(O?.css&&O?.css.length>0)K[W]=O?.css;if(O)v.push(F(W,L(O?.file,Q))),V[W]=M(Z,`app/views/${W}.tsx`,Q)}let xJ=`{${v.join(",")}}`,E=JJ(Z,Q);if(!E)console.error(`Client manifest has no "${_}" entry \u2014 dist/client is incomplete, so documents will render but never hydrate.`);let LJ=Z["app/client.tsx"]?.css??[],y=(await Promise.all(LJ.map((W)=>Bun.file(`${x}/client/${W}`).text()))).join(`
22
- `),CJ=ZJ(Z,Q),FJ=X2("sha256").update(await Bun.file(`${x}/client/.vite/manifest.json`).text()).digest("base64url").slice(0,12),g=new Map,IJ=(W)=>{let G=g.get(W);if(!G)G=Bun.file(`${x}/client/${W}`).text(),g.set(W,G);return G},PJ=async(W,G)=>{let U=new Set;for(let z of G?[G,...W]:W)for(let I of $J(Z,`app/views/${z}.tsx`))U.add(I);let O=await Promise.all([...U].map(async(z)=>({id:z,content:await IJ(z)})));return q(G?O:[{content:y},...O])},_J=await r(C),f=new Map;for(let[W,G]of Object.entries((await l(C))?.files??{})){let U=VJ(C,W);if(U.startsWith(C+zJ))f.set(U,{immutable:!0,encodings:G})}async function BJ(W){let{pathname:G}=new URL(W.url);if(N(G))return await T(W,G);let U=H2(G);if(A(G)||G.startsWith("/.well-known")||U!==null&&_J.has(U)){if(!U||!await j2(U))return WJ(G)??await T(W,G);try{return await s(W,U,f.get(U))}catch(z){return J.onException?.(z),new Response("Not found",{status:404})}}return await T(W,G)}async function T(W,G){let U=J.fetch.bind(J);try{let O=await U(W);if(O instanceof Response)return O;else{let z=[];return z.push({content:y}),await O({getStyles:async(R,u)=>{if(!R)return q([]);if(u?.static)return PJ(R,u.layout);for(let NJ of R){let qJ=Z[`app/views/${NJ}.tsx`];for(let h of qJ?.css??[]){let DJ=Bun.file(`${process.env.DIST_DIR}/client/${h}`);z.push({id:h,content:await DJ.text()})}}return q(z)},clientEntry:E,modulePreloadManifest:V,loaders:xJ,viewImportMap:H,viewModules:Y,ogMap:j,cssManifest:K,assetBase:Q,resolveIsland:CJ,buildId:FJ})}}catch(O){return S(O,G,J.onException)}}let MJ=(process.env.GEMI_COMPRESSION??"auto").toLowerCase()!=="off",AJ=jJ(process.env.GEMI_TRUST_PROXY),m=Bun.serve({maxRequestBodySize:10737418240,fetch:async(W,G)=>{GJ(W.headers,G.requestIP(W)?.address??null,AJ);let U=await $(W,(O)=>J.withGlobalMiddleware(O,BJ,(z)=>S(z,new URL(O.url).pathname,J.onException)));return MJ?c(W,U):U},idleTimeout:OJ(),port:process.env.PORT||5173});return KJ({port:m.port,rootDir:D}),m}export{W3 as httpProd};
23
-
24
- //# debugId=ADE18CB5D0F98F4264756E2164756E21
@@ -1,58 +0,0 @@
1
- // @bun
2
- import{R as C,T as h,U as m}from"./chunk-shfe0b22.js";import{ma as w,na as q}from"./chunk-zg6hmc48.js";import"./chunk-a22h5cby.js";import"./chunk-epvpmakp.js";import"./chunk-r3v2cnrj.js";import"./chunk-c0v61fp5.js";import"./chunk-kk18xqgc.js";import"./chunk-krtybgz1.js";import{Jg as _}from"./chunk-cca0grq3.js";import{Ui as y}from"./chunk-er8acmch.js";import{gj as N}from"./chunk-rhvc1axc.js";import"./chunk-tewhms9w.js";import{join as $J}from"path";import{createServer as zJ}from"vite";import{join as l}from"path";import{IncomingMessage as d,ServerResponse as p}from"http";import{Socket as i}from"net";function v(J){return J.replace(/\x1b\[[0-9;]*m/g,"").replace(/\[[0-9;]*m/g,"").replace(/\n\s*\n/g,`
3
- `).split(`
4
- `).map((W)=>W.trim()).filter((W)=>W.length>0).join(`
5
- `).trim()}function D(J){return`
6
- <!DOCTYPE html>
7
- <html lang="en">
8
- <head>
9
- <meta charset="UTF-8" />
10
- <meta
11
- name="viewport"
12
- content="width=device-width, initial-scale=1.0"
13
- />
14
-
15
- <title>Error</title>
16
- <style>
17
- body {
18
- font-family: Arial, sans-serif;
19
- padding: 20px;
20
- }
21
- h1 {
22
- color: red;
23
- }
24
- </style>
25
- </head>
26
- <body>
27
- <div id="overlay"></div>
28
- <script type="module" src="/refresh.js"></script>
29
- <script type="module" src="/@vite/client"></script>
30
- <script>
31
- ${v.toString()}
32
- const err = ${JSON.stringify(J,Object.getOwnPropertyNames(J),2)}
33
-
34
- window.addEventListener('load', () => {
35
- const container = document.getElementById('overlay')
36
- const ErrorOverlay = customElements.get('vite-error-overlay')
37
- if (ErrorOverlay) {
38
- const overlay = new ErrorOverlay({ ...err, message: cleanupTerminalText(err.message) });
39
- container.appendChild(overlay)
40
- }
41
- })
42
- </script>
43
- </body>
44
- </html>
45
- `}function n(J,W){let Z=new URL(W.url),$=new d(new i);$.url=Z.pathname+Z.search,$.method=W.method,$.headers=Object.fromEntries(W.headers);let Q=new p($),X=[];return new Promise((Y)=>{Q.write=(G)=>{if(G)X.push(Buffer.from(G));return!0},Q.end=(G)=>{if(G&&typeof G!=="function")X.push(Buffer.from(G));let z=new Headers;for(let[U,B]of Object.entries(Q.getHeaders())){if(B==null)continue;for(let K of Array.isArray(B)?B:[B])z.append(U,String(K))}return Y(new Response(X.length?Buffer.concat(X):null,{status:Q.statusCode,headers:z})),Q},J.middlewares($,Q,()=>Y(null))})}function o(J,W,Z){let $=String(W),Q=" ".repeat($.length),X=" ".repeat(Math.max(0,Z-1))+"^";return`${$} | ${J}
46
- ${Q} | ${X}
47
- `}function H(J){if(J&&typeof J==="object"){let W=J.position;if(W&&typeof W==="object"&&typeof W.file==="string")return{message:typeof J.message==="string"?J.message:String(J),stack:"",frame:typeof W.lineText==="string"?o(W.lineText,W.line,W.column):void 0,loc:{file:W.file,line:W.line,column:W.column}};return{message:typeof J.message==="string"?J.message:String(J),stack:typeof J.stack==="string"?J.stack:""}}return{message:String(J),stack:""}}function O(J){let W=globalThis.__gemiVite;if(!W)return;W.ws.send({type:"error",err:H(J)}),globalThis.__gemiErrorActive=!0}function I(J){let W=J.environments.ssr;if(typeof W?.runner?.import!=="function")throw Error("gemi dev requires a runnable Vite SSR environment.");return W.runner}var S="/@gemi/dev-entry.js",s=`const report = (error) => console.error(error);
48
- await import("/refresh.js").catch(report);
49
- await import("/@vite/client").catch(report);
50
- await import("/app/client.tsx");
51
- `,a=`import RefreshRuntime from "/@react-refresh";
52
- RefreshRuntime.injectIntoGlobalHook(window);
53
- window.$RefreshReg$ = () => {};
54
- window.$RefreshSig$ = () => (type) => type;
55
- window.__vite_plugin_react_preamble_installed__ = true;
56
- `;function t(J){let{pathname:W}=new URL(J.url),Z=($)=>new Response($,{headers:{"Content-Type":"application/javascript"}});if(W===S)return Z(s);if(W.startsWith("/render-error.js"))return Z("window.render_error = true");if(W.startsWith("/refresh.js"))return Z(a);return null}function k(J,W,Z){let $=l(process.cwd(),"app");return async(Q)=>{let{pathname:X}=new URL(Q.url),Y=(z)=>{if(console.error(z),Z.ssrFixStacktrace?.(z),O(z),N(X))return new Response(JSON.stringify({error:_("server_error",z?.message??String(z),500)}),{status:500,headers:{"Content-Type":"application/json"}});return globalThis.__gemiErrorPageServed=!0,new Response(D(H(z)),{status:500,headers:{"Content-Type":"text/html"}})},G=async(z)=>{let U=t(z);if(U)return U;try{let B=J.fetch.bind(J),K=await n(Z,z)??await B(z);if(K instanceof Response)return K;let j={},b={},M={},c=(L,A)=>`"${L}": () => import("${A}")`,F=[];for(let L of["404",...J.getFlatComponentTree.call(J)]){if(Bun.env.NODE_ENV==="test")break;let A=`${process.env.APP_DIR}`,x=await I(Z).import(`${A}/views/${L}.tsx`);j[L]=x.default,b[L]=x?.OpenGraph,M[L]=x,F.push(c(L,`/app/views/${L}.tsx`))}let u=`{${F.join(",")}}`;return await K({getStyles:async(L,A)=>await C($,Z,L,A?.layout),resolveIsland:(L)=>({src:`/${L}${y}`,preload:[]}),buildId:"dev",bootstrapModules:[S],viewImportMap:j,viewModules:M,ogMap:b,loaders:u,cssManifest:{}})}catch(B){return Y(B)}};return await W(Q,(z)=>J.withGlobalMiddleware(z,G,Y))}}function r(J){try{return Bun.listen({hostname:"0.0.0.0",port:J,socket:{data(){}}}).stop(!0),!0}catch(W){return W?.code!=="EADDRINUSE"}}function T(J,W=r){let Z=24678+(J-5173);if(Z>65471)Z=24678;for(let $=Z;$<Z+64;$++)if(W($))return $;throw Error(`Could not find a free port for Vite's HMR websocket: tried ${Z}-${Z+64-1}. Set one explicitly in gemi.config.ts as \`vite: { server: { ws: { port } } }\`.`)}import{createServer as e}from"http";var JJ=new Set(["vite-hmr","vite-ping"]);async function P(J){let W=e((Z,$)=>{$.writeHead(426,{"Content-Type":"text/plain"}),$.end("Upgrade Required")});return await new Promise((Z,$)=>{W.once("error",$),W.listen(J,"127.0.0.1",()=>{W.off("error",$),Z()})}),W}function f(J){if(!J||typeof J!=="object")return null;let Z=J.server?.address?.();if(Z&&typeof Z==="object"&&"port"in Z)return Number(Z.port)||null;return null}function R(J){if(J.headers.get("upgrade")?.toLowerCase()!=="websocket")return!1;return JJ.has(J.headers.get("sec-websocket-protocol")??"")}var WJ=(J)=>typeof J==="string"?J:new Uint8Array(J);async function g(J,W,Z){let $=new URL(J.url),Q=J.headers.get("sec-websocket-protocol")??"vite-hmr",X={host:J.headers.get("host")??$.host},Y=J.headers.get("origin");if(Y)X.origin=Y;let G=new WebSocket(`ws://127.0.0.1:${Z}${$.pathname}${$.search}`,{protocols:[Q],headers:X});G.binaryType="arraybuffer";let z={upstream:G,early:[]};if(G.onmessage=(K)=>{let j=WJ(K.data);if(z.browser)z.browser.send(j);else z.early.push(j)},G.onclose=(K)=>{z.browser?.close(ZJ(K.code),K.reason)},G.onerror=()=>{},!await new Promise((K)=>{G.addEventListener("open",()=>K(!0),{once:!0}),G.addEventListener("close",()=>K(!1),{once:!0})}))return new Response("Vite refused the HMR websocket",{status:403});if(!W.upgrade(J,{headers:{"Sec-WebSocket-Protocol":Q},data:z}))return G.close(),new Response("WebSocket upgrade failed",{status:400});return}function ZJ(J){if(J===1000||J>=1001&&J<=1003||J>=1007&&J<=1014)return J;if(J>=3000&&J<=4999)return J;return 1000}var E={open(J){let W=J.data;for(let Z of W.early)J.send(Z);if(W.early=[],W.browser=J,W.upstream.readyState!==WebSocket.OPEN)J.close(1000)},message(J,W){if(J.data.upstream.readyState===WebSocket.OPEN)J.data.upstream.send(W)},close(J){let{upstream:W}=J.data;if(W.readyState<=WebSocket.OPEN)W.close()}};var V=process.cwd(),GJ=$J(V,"app");if(!globalThis.__gemiErrorHooked)globalThis.__gemiErrorHooked=!0,process.on("unhandledRejection",(J)=>{console.error(J),O(J)}),process.on("uncaughtException",(J)=>{console.error(J),O(J)});async function SJ(J,W){let Z=Number(process.env.PORT)||5173,$=m(),Q=Boolean(globalThis.__gemiVite),X=globalThis.__gemiVite??=await zJ({configFile:!1,plugins:[q()],server:{middlewareMode:!0,port:Z,ws:{server:await P(T(Z))},allowedHosts:J.devAllowedHosts.call(J),watch:{ignored:(U)=>{let B=U.split(/[/\\]/).pop()??"";return B===".env"||B.startsWith(".env.")}}},appType:"custom",resolve:{dedupe:["react","react-dom"]},optimizeDeps:{include:["react","react-dom","react-dom/client"]},ssr:{external:[...w]}});process.env.ROOT_DIR=V,process.env.APP_DIR=GJ;let Y=k(J,W,X),G=f(X.config?.server?.ws),z=Bun.serve({port:Z,idleTimeout:$,fetch:(U,B)=>{if(G!==null&&R(U))return g(U,B,G);return Y(U)},websocket:E});if(Q)I(X).clearCache();if(Q)if(globalThis.__gemiErrorActive&&globalThis.__gemiErrorPageServed)globalThis.__gemiErrorActive=!1,globalThis.__gemiErrorPageServed=!1,X.ws.send({type:"full-reload"});else globalThis.__gemiErrorActive=!1,X.ws.send({type:"custom",event:"http-reload"});else h({port:z.port,rootDir:V});return z}export{H as viteErrorPayload,SJ as httpDev};
57
-
58
- //# debugId=DD235AA05CAC380F64756E2164756E21