acrylblends 0.3.0 → 0.4.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 (315) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +28 -3
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/acryl-inspect/plugin.json +2 -1
  6. package/plugins/acryl-inspect/src/report.ts +16 -13
  7. package/plugins/acryl-inspect/src/service.ts +9 -7
  8. package/plugins/acryl-inspect/tests/report.spec.ts +7 -8
  9. package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
  10. package/plugins/acryl-inspect/upstream.json +2 -2
  11. package/plugins/acryl-node-server/LICENSE +26 -0
  12. package/plugins/acryl-node-server/plugin.json +2 -1
  13. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  14. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  15. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  16. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  17. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  18. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  19. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  20. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  21. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  22. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  23. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  24. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  25. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  26. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  27. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  28. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  29. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  30. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  31. package/plugins/acryl-node-server/src/index.ts +19 -6
  32. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  33. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  34. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  35. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  36. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  37. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  38. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  39. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  40. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  41. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  42. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  43. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  44. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  45. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  46. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  47. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  48. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  49. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  50. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  51. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  52. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  53. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  54. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  55. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  56. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  57. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  58. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  59. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  60. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  61. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  62. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  63. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  64. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  65. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  66. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  67. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  68. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  69. package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
  70. package/plugins/acryl-node-server/upstream.json +3 -3
  71. package/plugins/acryl-router/LICENSE +26 -0
  72. package/plugins/acryl-router/plugin.json +2 -1
  73. package/plugins/acryl-router/src/client.ts +2 -0
  74. package/plugins/acryl-router/src/connection.ts +42 -0
  75. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  76. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  77. package/plugins/acryl-router/src/core/app.ts +34 -0
  78. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  79. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  80. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  81. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  82. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  83. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  84. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  85. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  86. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  87. package/plugins/acryl-router/src/core/compose.ts +73 -0
  88. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  89. package/plugins/acryl-router/src/core/context.ts +781 -0
  90. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  91. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  92. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  93. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  94. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  95. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  96. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  97. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  98. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  99. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  100. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  101. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  102. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  103. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  104. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  105. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  106. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  107. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  108. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  109. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  110. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  111. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  112. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  113. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  114. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  115. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  116. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  117. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  118. package/plugins/acryl-router/src/core/index.ts +53 -0
  119. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  120. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  121. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  122. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  123. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  124. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  125. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  126. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  127. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  128. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  129. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  130. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  131. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  132. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  133. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  134. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  135. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  136. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  137. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  138. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  139. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  140. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  141. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  142. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  143. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  144. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  145. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  146. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  147. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  148. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  149. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  150. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  151. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  152. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  153. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  154. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  155. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  156. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  157. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  158. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  159. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  160. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  161. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  162. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  163. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  164. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  165. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  166. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  167. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  168. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  169. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  170. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  171. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  172. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  173. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  174. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  175. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  176. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  177. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  178. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  179. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  180. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  181. package/plugins/acryl-router/src/core/request.ts +502 -0
  182. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  183. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  184. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  185. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  186. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  187. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  188. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  189. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  190. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  191. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  192. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  193. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  194. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  195. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  196. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  197. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  198. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  199. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  200. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  201. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  202. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  203. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  204. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  205. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  206. package/plugins/acryl-router/src/core/router.ts +103 -0
  207. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  208. package/plugins/acryl-router/src/core/types.ts +2782 -0
  209. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  210. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  211. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  212. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  213. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  214. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  215. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  216. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  217. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  218. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  219. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  220. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  221. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  222. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  223. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  224. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  225. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  226. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  227. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  228. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  229. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  230. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  231. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  232. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  233. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  234. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  235. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  236. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  237. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  238. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  239. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  240. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  241. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  242. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  243. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  244. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  245. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  246. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  247. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  248. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  249. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  250. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  251. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  252. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  253. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  254. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  255. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  256. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  257. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  258. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  259. package/plugins/acryl-router/src/helpers/index.ts +13 -3
  260. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  261. package/plugins/acryl-router/src/index.ts +6 -6
  262. package/plugins/acryl-router/src/middleware/index.ts +25 -6
  263. package/plugins/acryl-router/src/middleware/loopback-only.ts +19 -13
  264. package/plugins/acryl-router/src/service.ts +256 -64
  265. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  266. package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
  267. package/plugins/acryl-router/tests/loader.spec.ts +3 -2
  268. package/plugins/acryl-router/tests/service.spec.ts +264 -66
  269. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  270. package/plugins/acryl-router/upstream.json +2 -2
  271. package/plugins/acryl-router-http/plugin.json +2 -1
  272. package/plugins/acryl-router-http/src/bridge.ts +3 -1
  273. package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
  274. package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
  275. package/plugins/acryl-router-http/upstream.json +3 -3
  276. package/plugins/acryl-state/NOTICE +30 -0
  277. package/plugins/acryl-state/plugin.json +1 -1
  278. package/plugins/acryl-state/src/cell.ts +1 -1
  279. package/plugins/acryl-state/src/service.ts +33 -15
  280. package/plugins/acryl-state/upstream.json +6 -4
  281. package/plugins/builder/script.ts +4 -7
  282. package/plugins/extensions/index.ts +2 -2
  283. package/plugins/extensions/resolve.ts +28 -3
  284. package/plugins/openai/prompt.ts +9 -7
  285. package/plugins/tsconfig.json +5 -4
  286. package/templates/agent/src/routes.ts +37 -27
  287. package/templates/agent/test/model.test.ts +2 -1
  288. package/templates/agent/tsconfig.json +8 -5
  289. package/templates/blank/src/routes.ts +6 -4
  290. package/templates/blank/tsconfig.json +2 -4
  291. package/templates/todo/src/page.ts +1 -1
  292. package/templates/todo/src/todo.ts +56 -29
  293. package/templates/todo/tsconfig.json +2 -4
  294. package/templates/todo/upstream.json +2 -2
  295. package/plugins/acryl-router/src/compose.ts +0 -21
  296. package/plugins/acryl-router/src/context.ts +0 -59
  297. package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
  298. package/plugins/acryl-router/src/helpers/sse.ts +0 -83
  299. package/plugins/acryl-router/src/helpers/validate.ts +0 -44
  300. package/plugins/acryl-router/src/http-error.ts +0 -21
  301. package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
  302. package/plugins/acryl-router/src/middleware/cors.ts +0 -46
  303. package/plugins/acryl-router/src/middleware/logger.ts +0 -21
  304. package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
  305. package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
  306. package/plugins/acryl-router/src/pattern.ts +0 -89
  307. package/plugins/acryl-router/src/registrar.ts +0 -90
  308. package/plugins/acryl-router/src/table.ts +0 -156
  309. package/plugins/acryl-router/src/types.ts +0 -73
  310. package/plugins/acryl-router/src/util.ts +0 -23
  311. package/plugins/acryl-router/tests/api.spec.ts +0 -223
  312. package/plugins/acryl-router/tests/conformance.ts +0 -170
  313. package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
  314. package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
  315. package/plugins/acryl-router/tests/table.spec.ts +0 -47
@@ -0,0 +1,2313 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+
3
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
4
+ import { HttpResponse, http } from 'msw'
5
+ import { setupServer } from 'msw/node'
6
+ import { expectTypeOf, vi } from 'vitest'
7
+ import { defineWebSocketHelper } from '../helper/websocket/index.ts'
8
+ import { App } from '../app.ts'
9
+ import { parse } from '../utils/cookie.ts'
10
+ import type { Equal, Expect, JSONValue, SimplifyDeepArray } from '../utils/types.ts'
11
+ import { validator } from '../validator/index.ts'
12
+ import { hc } from './client.ts'
13
+ import type {
14
+ ClientResponse,
15
+ InferRequestType,
16
+ InferResponseType,
17
+ ApplyGlobalResponse,
18
+ PickResponseByStatusCode,
19
+ } from './types.ts'
20
+
21
+ // The Deno adapter is not part of ACRYL; these tests only build client URLs, so a helper that never upgrades stands in for it.
22
+ const upgradeWebSocket = defineWebSocketHelper(async () => {})
23
+
24
+ class SafeBigInt {
25
+ unsafe = BigInt(42)
26
+
27
+ toJSON() {
28
+ return {
29
+ value: '42n',
30
+ }
31
+ }
32
+ }
33
+
34
+ describe('Basic - JSON', () => {
35
+ const app = new App()
36
+
37
+ const route = app
38
+ .post(
39
+ '/posts',
40
+ validator('cookie', () => {
41
+ return {} as {
42
+ debug: string
43
+ }
44
+ }),
45
+ validator('header', () => {
46
+ return {} as {
47
+ 'x-message': string
48
+ }
49
+ }),
50
+ validator('json', () => {
51
+ return {} as {
52
+ id: number
53
+ title: string
54
+ }
55
+ }),
56
+ (c) => {
57
+ return c.json({
58
+ success: true,
59
+ message: 'dummy',
60
+ requestContentType: 'dummy',
61
+ requestApp: 'dummy',
62
+ requestMessage: 'dummy',
63
+ requestBody: {
64
+ id: 123,
65
+ title: 'dummy',
66
+ },
67
+ })
68
+ }
69
+ )
70
+ .get('/hello-not-found', (c) => c.notFound())
71
+ .get('/null', (c) => c.json(null))
72
+ .get('/empty', (c) => c.json({}))
73
+ .get('/bigint', (c) => c.json({ value: BigInt(42) }))
74
+ .get('/safe-bigint', (c) => c.json(new SafeBigInt()))
75
+
76
+ type AppType = typeof route
77
+
78
+ const server = setupServer(
79
+ http.post('http://localhost/posts', async ({ request }) => {
80
+ const requestContentType = request.headers.get('content-type')
81
+ const requestApp = request.headers.get('x-acryl')
82
+ const requestMessage = request.headers.get('x-message')
83
+ const requestBody = await request.json()
84
+ const payload = {
85
+ message: 'Hello!',
86
+ success: true,
87
+ requestContentType,
88
+ requestApp,
89
+ requestMessage,
90
+ requestBody,
91
+ }
92
+ return HttpResponse.json(payload)
93
+ }),
94
+ http.get('http://localhost/hello-not-found', () => {
95
+ return HttpResponse.text(null, {
96
+ status: 404,
97
+ })
98
+ }),
99
+ http.get('http://localhost/null', () => {
100
+ return HttpResponse.json(null)
101
+ }),
102
+ http.get('http://localhost/empty', () => {
103
+ return HttpResponse.json({})
104
+ }),
105
+ http.get('http://localhost/bigint', () => {
106
+ return HttpResponse.json({ value: BigInt(42) })
107
+ }),
108
+ http.get('http://localhost/safe-bigint', () => {
109
+ return HttpResponse.json(new SafeBigInt())
110
+ }),
111
+ http.get('http://localhost/api/string', () => {
112
+ return HttpResponse.json('a-string')
113
+ }),
114
+ http.get('http://localhost/api/number', async () => {
115
+ return HttpResponse.json(37)
116
+ }),
117
+ http.get('http://localhost/api/boolean', async () => {
118
+ return HttpResponse.json(true)
119
+ }),
120
+ http.get('http://localhost/api/generic', async () => {
121
+ return HttpResponse.json(Math.random() > 0.5 ? Boolean(Math.random()) : Math.random())
122
+ })
123
+ )
124
+
125
+ beforeAll(() => server.listen())
126
+ afterEach(() => server.resetHandlers())
127
+ afterAll(() => server.close())
128
+
129
+ const payload = {
130
+ id: 123,
131
+ title: 'Hello! App!',
132
+ }
133
+
134
+ const client = hc<AppType>('http://localhost', { headers: { 'x-acryl': 'acryl' } })
135
+
136
+ it('Should get 200 response', async () => {
137
+ const res = await client.posts.$post(
138
+ {
139
+ json: payload,
140
+ header: {
141
+ 'x-message': 'foobar',
142
+ },
143
+ cookie: {
144
+ debug: 'true',
145
+ },
146
+ },
147
+ {}
148
+ )
149
+
150
+ expect(res.ok).toBe(true)
151
+ const data = await res.json()
152
+ expect(data.success).toBe(true)
153
+ expect(data.message).toBe('Hello!')
154
+ expect(data.requestContentType).toBe('application/json')
155
+ expect(data.requestApp).toBe('acryl')
156
+ expect(data.requestMessage).toBe('foobar')
157
+ expect(data.requestBody).toEqual(payload)
158
+ })
159
+
160
+ it('Should get 404 response', async () => {
161
+ const res = await client['hello-not-found'].$get()
162
+ expect(res.status).toBe(404)
163
+ })
164
+
165
+ it('Should get a `null` content', async () => {
166
+ const client = hc<AppType>('http://localhost')
167
+ const res = await client.null.$get()
168
+ const data = await res.json()
169
+ expectTypeOf(data).toMatchTypeOf<null>()
170
+ expect(data).toBe(null)
171
+ })
172
+
173
+ it('Should get a `{}` content', async () => {
174
+ const client = hc<AppType>('http://localhost')
175
+ const res = await client.empty.$get()
176
+ const data = await res.json()
177
+ expectTypeOf(data).toMatchTypeOf<{}>()
178
+ expect(data).toStrictEqual({})
179
+ })
180
+
181
+ it('Should get a `{}` content', async () => {
182
+ const client = hc<AppType>('http://localhost')
183
+ const res = await client['safe-bigint'].$get()
184
+ const data = await res.json()
185
+ expectTypeOf(data).toMatchTypeOf<{ value: string }>()
186
+ expect(data).toStrictEqual({ value: '42n' })
187
+ })
188
+
189
+ it('Should get an error response', async () => {
190
+ const client = hc<AppType>('http://localhost')
191
+ const res = await client.bigint.$get()
192
+ const data = await res.json()
193
+ expectTypeOf(data).toMatchTypeOf<never>()
194
+ expect(res.status).toBe(500)
195
+ expect(data).toMatchObject({
196
+ message: 'Do not know how to serialize a BigInt',
197
+ name: 'TypeError',
198
+ })
199
+ })
200
+
201
+ it('Should have correct types - primitives', async () => {
202
+ const app = new App()
203
+ const route = app
204
+ .get('/api/string', (c) => c.json('a-string'))
205
+ .get('/api/number', (c) => c.json(37))
206
+ .get('/api/boolean', (c) => c.json(true))
207
+ .get('/api/generic', (c) =>
208
+ c.json(Math.random() > 0.5 ? Boolean(Math.random()) : Math.random())
209
+ )
210
+ type AppType = typeof route
211
+ const client = hc<AppType>('http://localhost')
212
+ const stringFetch = await client.api.string.$get()
213
+ const stringRes = await stringFetch.json()
214
+ const numberFetch = await client.api.number.$get()
215
+ const numberRes = await numberFetch.json()
216
+ const booleanFetch = await client.api.boolean.$get()
217
+ const booleanRes = await booleanFetch.json()
218
+ const genericFetch = await client.api.generic.$get()
219
+ const genericRes = await genericFetch.json()
220
+ type stringVerify = Expect<Equal<'a-string', typeof stringRes>>
221
+ expect(stringRes).toBe('a-string')
222
+ type numberVerify = Expect<Equal<37, typeof numberRes>>
223
+ expect(numberRes).toBe(37)
224
+ type booleanVerify = Expect<Equal<true, typeof booleanRes>>
225
+ expect(booleanRes).toBe(true)
226
+ type genericVerify = Expect<Equal<number | boolean, typeof genericRes>>
227
+ expect(typeof genericRes === 'number' || typeof genericRes === 'boolean').toBe(true)
228
+
229
+ // using .text() on json endpoint should return string
230
+ type textTest = Expect<Equal<Promise<string>, ReturnType<typeof genericFetch.text>>>
231
+ })
232
+ })
233
+
234
+ describe('Basic - query, queries, form, path params, header and cookie', () => {
235
+ const app = new App()
236
+
237
+ const route = app
238
+ .get(
239
+ '/search',
240
+ validator('query', () => {
241
+ return {} as { q: string; tag: string[]; filter: string }
242
+ }),
243
+ (c) => {
244
+ return c.json({
245
+ q: 'fake',
246
+ tag: ['fake'],
247
+ filter: 'fake',
248
+ })
249
+ }
250
+ )
251
+ .put(
252
+ '/posts/:id',
253
+ validator('form', () => {
254
+ return {
255
+ title: 'Hello',
256
+ }
257
+ }),
258
+ (c) => {
259
+ const data = c.req.valid('form')
260
+ return c.json(data)
261
+ }
262
+ )
263
+ .get(
264
+ '/header',
265
+ validator('header', () => {
266
+ return {
267
+ 'x-message-id': 'Hello',
268
+ }
269
+ }),
270
+ (c) => {
271
+ const data = c.req.valid('header')
272
+ return c.json(data)
273
+ }
274
+ )
275
+ .get(
276
+ '/cookie',
277
+ validator('cookie', () => {
278
+ return {
279
+ hello: 'world',
280
+ goodbye: 'moon',
281
+ }
282
+ }),
283
+ (c) => {
284
+ const data = c.req.valid('cookie')
285
+ return c.json(data)
286
+ }
287
+ )
288
+
289
+ const server = setupServer(
290
+ http.get('http://localhost/api/search', ({ request }) => {
291
+ const url = new URL(request.url)
292
+ const query = url.searchParams.get('q')
293
+ const tag = url.searchParams.getAll('tag')
294
+ const filter = url.searchParams.get('filter')
295
+ return HttpResponse.json({
296
+ q: query,
297
+ tag,
298
+ filter,
299
+ })
300
+ }),
301
+ http.get('http://localhost/api/posts', ({ request }) => {
302
+ const url = new URL(request.url)
303
+ const tags = url.searchParams.getAll('tags')
304
+ return HttpResponse.json({
305
+ tags: tags,
306
+ })
307
+ }),
308
+ http.put('http://localhost/api/posts/123', async ({ request }) => {
309
+ const buffer = await request.arrayBuffer()
310
+ // @ts-ignore
311
+ const string = String.fromCharCode.apply('', new Uint8Array(buffer))
312
+ return HttpResponse.text(string)
313
+ }),
314
+ http.get('http://localhost/api/header', async ({ request }) => {
315
+ const message = await request.headers.get('x-message-id')
316
+ return HttpResponse.json({ 'x-message-id': message })
317
+ }),
318
+ http.get('http://localhost/api/cookie', async ({ request }) => {
319
+ return HttpResponse.json(parse(request.headers.get('cookie') || ''))
320
+ })
321
+ )
322
+
323
+ beforeAll(() => server.listen())
324
+ afterEach(() => server.resetHandlers())
325
+ afterAll(() => server.close())
326
+
327
+ type AppType = typeof route
328
+
329
+ const client = hc<AppType>('http://localhost/api')
330
+
331
+ it('Should get 200 response - query', async () => {
332
+ const res = await client.search.$get({
333
+ query: {
334
+ q: 'foobar',
335
+ tag: ['a', 'b'],
336
+ // @ts-expect-error
337
+ filter: undefined,
338
+ },
339
+ })
340
+
341
+ expect(res.status).toBe(200)
342
+ expect(await res.json()).toEqual({
343
+ q: 'foobar',
344
+ tag: ['a', 'b'],
345
+ filter: null,
346
+ })
347
+ })
348
+
349
+ it('Should get 200 response - form, params', async () => {
350
+ const res = await client.posts[':id'].$put({
351
+ form: {
352
+ title: 'Good Night',
353
+ },
354
+ param: {
355
+ id: '123',
356
+ },
357
+ })
358
+
359
+ expect(res.status).toBe(200)
360
+ expect(await res.text()).toMatch('Good Night')
361
+ })
362
+
363
+ it('Should get 200 response - header', async () => {
364
+ const header = {
365
+ 'x-message-id': 'Hello',
366
+ }
367
+ const res = await client.header.$get({
368
+ header,
369
+ })
370
+
371
+ expect(res.status).toBe(200)
372
+ expect(await res.json()).toEqual(header)
373
+ })
374
+
375
+ it('Should get 200 response - cookie', async () => {
376
+ const cookie = {
377
+ hello: 'world',
378
+ goodbye: 'moon',
379
+ }
380
+ const res = await client.cookie.$get({
381
+ cookie,
382
+ })
383
+
384
+ expect(res.status).toBe(200)
385
+ expect(await res.json()).toEqual(cookie)
386
+ })
387
+ })
388
+
389
+ describe('Basic - $url()', () => {
390
+ const api = new App().get('/', (c) => c.text('API')).get('/posts/:id', (c) => c.text('Post'))
391
+ const content = new App().get(
392
+ '/search',
393
+ validator('query', () => {
394
+ return { page: '1', limit: '10' }
395
+ }),
396
+ (c) => c.text('Search')
397
+ )
398
+ const app = new App()
399
+ .get('/', (c) => c.text('Index'))
400
+ .route('/api', api)
401
+ .route('/content', content)
402
+
403
+ it('Should return a correct url via $url().href', async () => {
404
+ const client = hc<typeof app>('http://fake')
405
+ expect(client.index.$url().href).toBe('http://fake/')
406
+ expect(
407
+ client.index.$url({
408
+ query: {
409
+ page: '123',
410
+ limit: '20',
411
+ },
412
+ }).href
413
+ ).toBe('http://fake/?page=123&limit=20')
414
+ expect(client.api.$url().href).toBe('http://fake/api')
415
+ expect(
416
+ client.api.posts[':id'].$url({
417
+ param: {
418
+ id: '123',
419
+ },
420
+ }).href
421
+ ).toBe('http://fake/api/posts/123')
422
+ expect(
423
+ client.content.search.$url({
424
+ query: {
425
+ page: '123',
426
+ limit: '20',
427
+ },
428
+ }).href
429
+ ).toBe('http://fake/content/search?page=123&limit=20')
430
+ })
431
+
432
+ it.each(['http://fake', 'http://fake/', 'http://fake//', 'http://fake/api'])(
433
+ 'Should return a correct path via $path() regardless of %s',
434
+ async (baseURL) => {
435
+ const client = hc<typeof app>(baseURL)
436
+ expect(client.index.$path()).toBe('/')
437
+ expect(
438
+ client.index.$path({
439
+ query: {
440
+ page: '123',
441
+ limit: '20',
442
+ },
443
+ })
444
+ ).toBe('/?page=123&limit=20')
445
+ expect(client.api.$path()).toBe('/api')
446
+ expect(
447
+ client.api.posts[':id'].$path({
448
+ param: {
449
+ id: '123',
450
+ },
451
+ })
452
+ ).toBe('/api/posts/123')
453
+ expect(
454
+ client.content.search.$path({
455
+ query: {
456
+ page: '123',
457
+ limit: '20',
458
+ },
459
+ })
460
+ ).toBe('/content/search?page=123&limit=20')
461
+ }
462
+ )
463
+ })
464
+
465
+ describe('Form - Multiple Values', () => {
466
+ const server = setupServer(
467
+ http.post('http://localhost/multiple-values', async ({ request }) => {
468
+ const data = await request.formData()
469
+ return HttpResponse.json(data.getAll('key'))
470
+ })
471
+ )
472
+
473
+ beforeAll(() => server.listen())
474
+ afterEach(() => server.resetHandlers())
475
+ afterAll(() => server.close())
476
+
477
+ const client = hc('http://localhost/')
478
+
479
+ it('Should get 200 response - query', async () => {
480
+ // @ts-expect-error `client['multiple-values'].$post` is not typed
481
+ const res = await client['multiple-values'].$post({
482
+ form: {
483
+ key: ['foo', 'bar'],
484
+ },
485
+ })
486
+ expect(res.status).toBe(200)
487
+ expect(await res.json()).toEqual(['foo', 'bar'])
488
+ })
489
+ })
490
+
491
+ describe('Form - Undefined Values', () => {
492
+ const server = setupServer(
493
+ http.post('http://localhost/form-undefined', async ({ request }) => {
494
+ const data = await request.formData()
495
+ return HttpResponse.json({
496
+ keys: [...data.keys()],
497
+ title: data.get('title'),
498
+ optional: data.get('optional'),
499
+ })
500
+ })
501
+ )
502
+
503
+ beforeAll(() => server.listen())
504
+ afterEach(() => server.resetHandlers())
505
+ afterAll(() => server.close())
506
+
507
+ const client = hc('http://localhost/')
508
+
509
+ it('Should skip undefined values in form data', async () => {
510
+ // @ts-expect-error `client['form-undefined'].$post` is not typed
511
+ const res = await client['form-undefined'].$post({
512
+ form: {
513
+ title: 'Hello',
514
+ optional: undefined,
515
+ },
516
+ })
517
+ expect(res.status).toBe(200)
518
+ const json = await res.json()
519
+ expect(json).toEqual({
520
+ keys: ['title'],
521
+ title: 'Hello',
522
+ optional: null,
523
+ })
524
+ })
525
+ })
526
+
527
+ describe('Form arrays containing undefined', () => {
528
+ const app = new App().post(
529
+ '/',
530
+ validator('form', () => ({}) as { tag: (string | undefined)[] | undefined }),
531
+ async (c) => {
532
+ const body = await c.req.parseBody({ all: true })
533
+ return c.json({ tag: body['tag'] })
534
+ }
535
+ )
536
+ const client = hc<typeof app>('', { fetch: app.request })
537
+
538
+ it('Should not send the literal string "undefined" for an absent entry', async () => {
539
+ const res = await client.index.$post({
540
+ form: {
541
+ tag: ['a', undefined, 'b'],
542
+ },
543
+ })
544
+ expect(res.status).toBe(200)
545
+ expect(await res.json()).toEqual({ tag: ['a', 'b'] })
546
+ })
547
+
548
+ it('Should keep an empty string in an array', async () => {
549
+ const res = await client.index.$post({
550
+ form: {
551
+ tag: ['a', '', 'b'],
552
+ },
553
+ })
554
+ expect(res.status).toBe(200)
555
+ expect(await res.json()).toEqual({ tag: ['a', '', 'b'] })
556
+ })
557
+ })
558
+
559
+ describe('Optional header and cookie values', () => {
560
+ const app = new App().get(
561
+ '/',
562
+ validator('header', () => {
563
+ return {} as {
564
+ 'x-required': string
565
+ 'x-optional'?: string
566
+ }
567
+ }),
568
+ validator('cookie', () => {
569
+ return {} as {
570
+ required: string
571
+ optional?: string
572
+ }
573
+ }),
574
+ (c) =>
575
+ c.json({
576
+ requiredHeader: c.req.header('x-required'),
577
+ optionalHeader: c.req.header('x-optional') ?? null,
578
+ cookies: parse(c.req.header('cookie') ?? ''),
579
+ })
580
+ )
581
+ const client = hc<typeof app>('', { fetch: app.request })
582
+
583
+ it('Should skip undefined header and cookie values', async () => {
584
+ const res = await client.index.$get({
585
+ header: {
586
+ 'x-required': 'header-value',
587
+ 'x-optional': undefined,
588
+ },
589
+ cookie: {
590
+ required: 'cookie-value',
591
+ optional: undefined,
592
+ },
593
+ })
594
+
595
+ expect(res.status).toBe(200)
596
+ expect(await res.json()).toEqual({
597
+ requiredHeader: 'header-value',
598
+ optionalHeader: null,
599
+ cookies: {
600
+ required: 'cookie-value',
601
+ },
602
+ })
603
+ })
604
+
605
+ it('Should not send a Cookie header if all cookie values are undefined', async () => {
606
+ const app = new App().get(
607
+ '/',
608
+ validator('cookie', () => {
609
+ return {} as {
610
+ optional?: string
611
+ }
612
+ }),
613
+ (c) => c.json({ cookieHeader: c.req.header('cookie') ?? null })
614
+ )
615
+ const client = hc<typeof app>('', { fetch: app.request })
616
+
617
+ const res = await client.index.$get({
618
+ cookie: {
619
+ optional: undefined,
620
+ },
621
+ })
622
+
623
+ expect(res.status).toBe(200)
624
+ expect(await res.json()).toEqual({ cookieHeader: null })
625
+ })
626
+ })
627
+
628
+ describe('Infer the response/request type', () => {
629
+ const app = new App()
630
+ const route = app.get(
631
+ '/',
632
+ validator('query', () => {
633
+ return {
634
+ name: 'dummy',
635
+ age: 'dummy',
636
+ }
637
+ }),
638
+ validator('header', () => {
639
+ return {
640
+ 'x-request-id': 'dummy',
641
+ }
642
+ }),
643
+ validator('cookie', () => {
644
+ return {
645
+ name: 'dummy',
646
+ }
647
+ }),
648
+ (c) =>
649
+ c.json({
650
+ id: 123,
651
+ title: 'Morning!',
652
+ })
653
+ )
654
+
655
+ type AppType = typeof route
656
+
657
+ it('Should infer response type the type correctly', () => {
658
+ const client = hc<AppType>('/')
659
+ const req = client.index.$get
660
+
661
+ type Actual = InferResponseType<typeof req>
662
+ type Expected = {
663
+ id: number
664
+ title: string
665
+ }
666
+ type verify = Expect<Equal<Expected, Actual>>
667
+ })
668
+
669
+ it('Should infer request type the type correctly', () => {
670
+ const client = hc<AppType>('/')
671
+ const req = client.index.$get
672
+
673
+ type Actual = InferRequestType<typeof req>
674
+ type Expected = {
675
+ age: string | string[]
676
+ name: string | string[]
677
+ }
678
+ type verify = Expect<Equal<Expected, Actual['query']>>
679
+ })
680
+
681
+ it('Should infer request header type the type correctly', () => {
682
+ const client = hc<AppType>('/')
683
+ const req = client.index.$get
684
+ type c = typeof req
685
+
686
+ type Actual = InferRequestType<c>
687
+ type Expected = {
688
+ 'x-request-id': string
689
+ }
690
+ type verify = Expect<Equal<Expected, Actual['header']>>
691
+ })
692
+
693
+ it('Should infer request cookie type the type correctly', () => {
694
+ const client = hc<AppType>('/')
695
+ const req = client.index.$get
696
+ type c = typeof req
697
+
698
+ type Actual = InferRequestType<c>
699
+ type Expected = {
700
+ name: string
701
+ }
702
+ type verify = Expect<Equal<Expected, Actual['cookie']>>
703
+ })
704
+
705
+ describe('Without input', () => {
706
+ const route = app.get('/', (c) => c.json({ ok: true }))
707
+ type AppType = typeof route
708
+
709
+ it('Should infer response type the type correctly', () => {
710
+ const client = hc<AppType>('/')
711
+ const req = client.index.$get
712
+
713
+ type Actual = InferResponseType<typeof req>
714
+ type Expected = { ok: true }
715
+ type verify = Expect<Equal<Expected, Actual>>
716
+ })
717
+
718
+ it('Should infer request type the type correctly', () => {
719
+ const client = hc<AppType>('/')
720
+ const req = client.index.$get
721
+
722
+ type Actual = InferRequestType<typeof req>
723
+ type Expected = {}
724
+ type verify = Expect<Equal<Expected, Actual>>
725
+ })
726
+ })
727
+ })
728
+
729
+ describe('Merge path with `app.route()`', () => {
730
+ const server = setupServer(
731
+ http.get('http://localhost/api/search', async () => {
732
+ return HttpResponse.json({
733
+ ok: true,
734
+ })
735
+ }),
736
+ http.get('http://localhost/api/searchArray', async () => {
737
+ return HttpResponse.json([
738
+ {
739
+ ok: true,
740
+ },
741
+ ])
742
+ }),
743
+ http.get('http://localhost/api/foo', async () => {
744
+ return HttpResponse.json({
745
+ ok: true,
746
+ })
747
+ }),
748
+ http.post('http://localhost/api/bar', async () => {
749
+ return HttpResponse.json({
750
+ ok: true,
751
+ })
752
+ }),
753
+ http.get('http://localhost/v1/book', async () => {
754
+ return HttpResponse.json({
755
+ ok: true,
756
+ })
757
+ })
758
+ )
759
+
760
+ beforeAll(() => server.listen())
761
+ afterEach(() => server.resetHandlers())
762
+ afterAll(() => server.close())
763
+
764
+ type Env = {
765
+ Bindings: {
766
+ TOKEN: string
767
+ }
768
+ }
769
+
770
+ it('Should have correct types', async () => {
771
+ const api = new App<Env>().get('/search', (c) => c.json({ ok: true }))
772
+ const app = new App<Env>().route('/api', api)
773
+ type AppType = typeof app
774
+ const client = hc<AppType>('http://localhost')
775
+ const res = await client.api.search.$get()
776
+ const data = await res.json()
777
+ type verify = Expect<Equal<true, typeof data.ok>>
778
+ expect(data.ok).toBe(true)
779
+ })
780
+
781
+ it('Should have correct types - basePath() then get()', async () => {
782
+ const base = new App<Env>().basePath('/api')
783
+ const app = base.get('/search', (c) => c.json({ ok: true }))
784
+ type AppType = typeof app
785
+ const client = hc<AppType>('http://localhost')
786
+ const res = await client.api.search.$get()
787
+ const data = await res.json()
788
+ type verify = Expect<Equal<true, typeof data.ok>>
789
+ expect(data.ok).toBe(true)
790
+ })
791
+
792
+ it('Should have correct types - basePath(), route(), get()', async () => {
793
+ const book = new App().get('/', (c) => c.json({ ok: true }))
794
+ const app = new App().basePath('/v1').route('/book', book)
795
+ type AppType = typeof app
796
+ const client = hc<AppType>('http://localhost')
797
+ const res = await client.v1.book.$get()
798
+ const data = await res.json()
799
+ type verify = Expect<Equal<true, typeof data.ok>>
800
+ expect(data.ok).toBe(true)
801
+ })
802
+
803
+ it('Should have correct types - with interface', async () => {
804
+ interface Result {
805
+ ok: boolean
806
+ okUndefined?: boolean
807
+ }
808
+ const result: Result = { ok: true }
809
+ const base = new App<Env>().basePath('/api')
810
+ const app = base.get('/search', (c) => c.json(result))
811
+ type AppType = typeof app
812
+ const client = hc<AppType>('http://localhost')
813
+ const res = await client.api.search.$get()
814
+ const data = await res.json()
815
+ type verify = Expect<Equal<Result, typeof data>>
816
+ expect(data.ok).toBe(true)
817
+
818
+ // A few more types only tests
819
+ interface DeepInterface {
820
+ l2: {
821
+ l3: Result
822
+ }
823
+ }
824
+ interface ExtraDeepInterface {
825
+ l4: DeepInterface
826
+ }
827
+ type verifyDeepInterface = Expect<
828
+ Equal<SimplifyDeepArray<DeepInterface> extends JSONValue ? true : false, true>
829
+ >
830
+ type verifyExtraDeepInterface = Expect<
831
+ Equal<SimplifyDeepArray<ExtraDeepInterface> extends JSONValue ? true : false, true>
832
+ >
833
+ })
834
+
835
+ it('Should have correct types - with array of interfaces', async () => {
836
+ interface Result {
837
+ ok: boolean
838
+ okUndefined?: boolean
839
+ }
840
+ type Results = Result[]
841
+
842
+ const results: Results = [{ ok: true }]
843
+ const base = new App<Env>().basePath('/api')
844
+ const app = base.get('/searchArray', (c) => c.json(results))
845
+ type AppType = typeof app
846
+ const client = hc<AppType>('http://localhost')
847
+ const res = await client.api.searchArray.$get()
848
+ const data = await res.json()
849
+ type verify = Expect<Equal<Results, typeof data>>
850
+ expect(data[0].ok).toBe(true)
851
+
852
+ // A few more types only tests
853
+ type verifyNestedArrayTyped = Expect<
854
+ Equal<SimplifyDeepArray<[string, Results]> extends JSONValue ? true : false, true>
855
+ >
856
+ type verifyNestedArrayInterfaceArray = Expect<
857
+ Equal<SimplifyDeepArray<[string, Result[]]> extends JSONValue ? true : false, true>
858
+ >
859
+ type verifyExtraNestedArrayTyped = Expect<
860
+ Equal<SimplifyDeepArray<[string, Results[]]> extends JSONValue ? true : false, true>
861
+ >
862
+ type verifyExtraNestedArrayInterfaceArray = Expect<
863
+ Equal<SimplifyDeepArray<[string, Result[][]]> extends JSONValue ? true : false, true>
864
+ >
865
+ })
866
+
867
+ it('Should allow a Date object and return it as a string', async () => {
868
+ const app = new App()
869
+ const route = app.get('/api/foo', (c) => c.json({ datetime: new Date() }))
870
+ type AppType = typeof route
871
+ const client = hc<AppType>('http://localhost')
872
+ const res = await client.api.foo.$get()
873
+ const { datetime } = await res.json()
874
+ type verify = Expect<Equal<string, typeof datetime>>
875
+ })
876
+
877
+ describe('Multiple endpoints', () => {
878
+ const api = new App()
879
+ .get('/foo', (c) => c.json({ foo: '' }))
880
+ .post('/bar', (c) => c.json({ bar: 0 }))
881
+ const app = new App().route('/api', api)
882
+ type AppType = typeof app
883
+ const client = hc<typeof app>('http://localhost')
884
+
885
+ it('Should return correct types - GET /api/foo', async () => {
886
+ const res = await client.api.foo.$get()
887
+ const data = await res.json()
888
+ type verify = Expect<Equal<string, typeof data.foo>>
889
+ })
890
+
891
+ it('Should return correct types - POST /api/bar', async () => {
892
+ const res = await client.api.bar.$post()
893
+ const data = await res.json()
894
+ type verify = Expect<Equal<number, typeof data.bar>>
895
+ })
896
+ it('Should work with $url', async () => {
897
+ const url = client.api.bar.$url()
898
+ expect(url.href).toBe('http://localhost/api/bar')
899
+ })
900
+ it('Should work with $path', async () => {
901
+ const path = client.api.bar.$path()
902
+ expect(path).toBe('/api/bar')
903
+ })
904
+ })
905
+
906
+ describe('With a blank path', () => {
907
+ const app = new App().basePath('/api/v1')
908
+ const routes = app.route(
909
+ '/me',
910
+ new App().route(
911
+ '',
912
+ new App().get('', async (c) => {
913
+ return c.json({ name: 'acryl' })
914
+ })
915
+ )
916
+ )
917
+ const client = hc<typeof routes>('http://localhost')
918
+
919
+ it('Should infer paths correctly', async () => {
920
+ // Should not a throw type error
921
+ const url = client.api.v1.me.$url()
922
+ expectTypeOf<URL>(url)
923
+ expect(url.href).toBe('http://localhost/api/v1/me')
924
+
925
+ const path = client.api.v1.me.$path()
926
+ expectTypeOf<'/api/v1/me'>(path)
927
+ expect(path).toBe('/api/v1/me')
928
+ })
929
+ })
930
+
931
+ describe('With endpoint pathname', () => {
932
+ const app = new App().basePath('/api/v1')
933
+ const routes = app.route(
934
+ '/me',
935
+ new App().route(
936
+ '',
937
+ new App().get('', async (c) => {
938
+ return c.json({ name: 'acryl' })
939
+ })
940
+ )
941
+ )
942
+ const client = hc<typeof routes>('http://localhost/proxy')
943
+
944
+ it('Should infer paths correctly', async () => {
945
+ // Should not a throw type error
946
+ const url = client.api.v1.me.$url()
947
+ expectTypeOf<URL>(url)
948
+ expect(url.href).toBe('http://localhost/proxy/api/v1/me')
949
+
950
+ const path = client.api.v1.me.$path()
951
+ expectTypeOf<'/api/v1/me'>(path)
952
+ expect(path).toBe('/api/v1/me')
953
+ })
954
+ })
955
+ })
956
+
957
+ describe('Use custom fetch method', () => {
958
+ it('Should call the custom fetch method when provided', async () => {
959
+ const fetchMock = vi.fn()
960
+
961
+ const api = new App().get('/search', (c) => c.json({ ok: true }))
962
+ const app = new App().route('/api', api)
963
+ type AppType = typeof app
964
+ const client = hc<AppType>('http://localhost', { fetch: fetchMock })
965
+ await client.api.search.$get()
966
+ expect(fetchMock).toHaveBeenCalledTimes(1)
967
+ })
968
+
969
+ it('Should return Response from custom fetch method', async () => {
970
+ const fetchMock = vi.fn()
971
+ const returnValue = new Response(null, { status: 200 })
972
+ fetchMock.mockReturnValueOnce(returnValue)
973
+
974
+ const api = new App().get('/search', (c) => c.json({ ok: true }))
975
+ const app = new App().route('/api', api)
976
+ type AppType = typeof app
977
+ const client = hc<AppType>('http://localhost', { fetch: fetchMock })
978
+ const res = await client.api.search.$get()
979
+ expect(res.ok).toBe(true)
980
+ expect(res).toEqual(returnValue)
981
+ })
982
+ })
983
+
984
+ describe('Use custom fetch (app.request) method', () => {
985
+ it('Should return Response from app request method', async () => {
986
+ const app = new App().get('/search', (c) => c.json({ ok: true }))
987
+ type AppType = typeof app
988
+ const client = hc<AppType>('', { fetch: app.request })
989
+ const res = await client.search.$get()
990
+ expect(res.ok).toBe(true)
991
+ })
992
+
993
+ it.each([false, 0, '', null])('Should send falsy JSON value: %j', async (json) => {
994
+ const app = new App().post(
995
+ '/json',
996
+ validator('json', (value) => value as boolean | number | string | null),
997
+ (c) => c.json(c.req.valid('json'))
998
+ )
999
+ const client = hc<typeof app>('', { fetch: app.request })
1000
+
1001
+ const res = await client.json.$post({ json })
1002
+
1003
+ expect(res.status).toBe(200)
1004
+ expect(await res.json()).toBe(json)
1005
+ })
1006
+ })
1007
+
1008
+ describe('Optional parameters in JSON response', () => {
1009
+ it('Should return the correct type', async () => {
1010
+ const app = new App().get('/', (c) => {
1011
+ return c.json({ message: 'foo' } as { message?: string })
1012
+ })
1013
+ type AppType = typeof app
1014
+ const client = hc<AppType>('', { fetch: app.request })
1015
+ const res = await client.index.$get()
1016
+ const data = await res.json()
1017
+ expectTypeOf(data).toEqualTypeOf<{
1018
+ message?: string
1019
+ }>()
1020
+ })
1021
+ })
1022
+
1023
+ describe('ClientResponse<T>.json() returns a Union type correctly', () => {
1024
+ const condition = () => true
1025
+ const app = new App().get('/', async (c) => {
1026
+ const ok = condition()
1027
+ if (ok) {
1028
+ return c.json({ data: 'foo' })
1029
+ }
1030
+ return c.json({ message: 'error' })
1031
+ })
1032
+
1033
+ const client = hc<typeof app>('', { fetch: app.request })
1034
+ it('Should be a Union type', async () => {
1035
+ const res = await client.index.$get()
1036
+ const json = await res.json()
1037
+ expectTypeOf(json).toEqualTypeOf<{ data: string } | { message: string }>()
1038
+ })
1039
+ })
1040
+
1041
+ describe('Response with different status codes', () => {
1042
+ const condition = () => true
1043
+ const app = new App().get('/', async (c) => {
1044
+ const ok = condition()
1045
+ if (ok) {
1046
+ return c.json({ data: 'foo' }, 200)
1047
+ }
1048
+ if (!ok) {
1049
+ return c.json({ message: 'error' }, 400)
1050
+ }
1051
+ return c.json(null)
1052
+ })
1053
+
1054
+ const client = hc<typeof app>('', { fetch: app.request })
1055
+
1056
+ it('all', async () => {
1057
+ const res = await client.index.$get()
1058
+ const json = await res.json()
1059
+ expectTypeOf(json).toEqualTypeOf<{ data: string } | { message: string } | null>()
1060
+ })
1061
+
1062
+ it('status 200', async () => {
1063
+ const res = await client.index.$get()
1064
+ if (res.status === 200) {
1065
+ const json = await res.json()
1066
+ expectTypeOf(json).toEqualTypeOf<{ data: string } | null>()
1067
+ }
1068
+ })
1069
+
1070
+ it('status 400', async () => {
1071
+ const res = await client.index.$get()
1072
+ if (res.status === 400) {
1073
+ const json = await res.json()
1074
+ expectTypeOf(json).toEqualTypeOf<{ message: string } | null>()
1075
+ }
1076
+ })
1077
+
1078
+ it('response is ok', async () => {
1079
+ const res = await client.index.$get()
1080
+ if (res.ok) {
1081
+ const json = await res.json()
1082
+ expectTypeOf(json).toEqualTypeOf<{ data: string } | null>()
1083
+ }
1084
+ })
1085
+
1086
+ it('response is not ok', async () => {
1087
+ const res = await client.index.$get()
1088
+ if (!res.ok) {
1089
+ const json = await res.json()
1090
+ expectTypeOf(json).toEqualTypeOf<{ message: string } | null>()
1091
+ }
1092
+ })
1093
+ })
1094
+
1095
+ describe('Infer the response type with different status codes', () => {
1096
+ const condition = () => true
1097
+ const app = new App().get('/', async (c) => {
1098
+ const ok = condition()
1099
+ if (ok) {
1100
+ return c.json({ data: 'foo' }, 200)
1101
+ }
1102
+ if (!ok) {
1103
+ return c.json({ message: 'error' }, 400)
1104
+ }
1105
+ return c.json(null)
1106
+ })
1107
+
1108
+ const client = hc<typeof app>('', { fetch: app.request })
1109
+
1110
+ it('Should infer response type correctly', () => {
1111
+ const req = client.index.$get
1112
+
1113
+ type Actual = InferResponseType<typeof req>
1114
+ type Expected =
1115
+ | {
1116
+ data: string
1117
+ }
1118
+ | {
1119
+ message: string
1120
+ }
1121
+ | null
1122
+ type verify = Expect<Equal<Expected, Actual>>
1123
+ })
1124
+
1125
+ it('Should infer response type of status 200 correctly', () => {
1126
+ const req = client.index.$get
1127
+
1128
+ type Actual = InferResponseType<typeof req, 200>
1129
+ type Expected = {
1130
+ data: string
1131
+ } | null
1132
+ type verify = Expect<Equal<Expected, Actual>>
1133
+ })
1134
+ })
1135
+
1136
+ describe('Infer the response types from middlewares', () => {
1137
+ const app = new App()
1138
+ .get(
1139
+ '/',
1140
+ validator('query', (input, c) => {
1141
+ if (!input.page || typeof input.page !== 'string') {
1142
+ return c.json({ error: 'Bad request' as const }, 400)
1143
+ }
1144
+
1145
+ return input as { page: string }
1146
+ }),
1147
+ async (c) => {
1148
+ const query = c.req.valid('query')
1149
+ return c.json({ data: 'foo', page: query.page }, 200)
1150
+ }
1151
+ )
1152
+ .post(
1153
+ '/posts',
1154
+ async (c, next) => {
1155
+ const auth = c.req.header('authorization')
1156
+ if (!auth || !auth.startsWith('Bearer ')) {
1157
+ return c.json({ error: 'Unauthorized' as const }, 401)
1158
+ }
1159
+ return next()
1160
+ },
1161
+ validator('json', (input, c) => {
1162
+ if (!input.title) {
1163
+ return c.json({ error: 'Bad request' as const }, 400)
1164
+ }
1165
+
1166
+ return input as { title: string }
1167
+ }),
1168
+ (c) => {
1169
+ const data = c.req.valid('json')
1170
+ return c.json(data, 200)
1171
+ }
1172
+ )
1173
+
1174
+ type AppType = typeof app
1175
+ const client = hc<AppType>('', { fetch: app.request })
1176
+
1177
+ it('Should infer response type of status 200 correctly', () => {
1178
+ const req = client.posts.$post
1179
+
1180
+ type Actual = InferResponseType<typeof req, 200>
1181
+ type Expected = {
1182
+ title: string
1183
+ }
1184
+ type verify = Expect<Equal<Expected, Actual>>
1185
+ })
1186
+
1187
+ it('Should infer response type of status 400 correctly', () => {
1188
+ const req = client.posts.$post
1189
+
1190
+ type Actual = InferResponseType<typeof req, 400>
1191
+ type Expected = {
1192
+ error: 'Bad request'
1193
+ }
1194
+ type verify = Expect<Equal<Expected, Actual>>
1195
+ })
1196
+
1197
+ it('Should infer response type of status 401 correctly', () => {
1198
+ const req = client.posts.$post
1199
+
1200
+ type Actual = InferResponseType<typeof req, 401>
1201
+ type Expected = {
1202
+ error: 'Unauthorized'
1203
+ }
1204
+ type verify = Expect<Equal<Expected, Actual>>
1205
+ })
1206
+
1207
+ it('Should infer all possible response statuses', async () => {
1208
+ const req = await client.posts.$post({
1209
+ json: {
1210
+ title: 'hello',
1211
+ },
1212
+ })
1213
+
1214
+ type Actual = typeof req.status
1215
+ type Expected = 200 | 400 | 401
1216
+ type verify = Expect<Equal<Expected, Actual>>
1217
+ })
1218
+
1219
+ it('Should properly assign response to corresponding status', async () => {
1220
+ const req = await client.posts.$post({
1221
+ json: {
1222
+ title: 'hello',
1223
+ },
1224
+ })
1225
+
1226
+ if (req.status === 200) {
1227
+ const data = await req.json()
1228
+
1229
+ expectTypeOf(data).toEqualTypeOf<{ title: string }>()
1230
+ } else if (req.status === 400) {
1231
+ const data = await req.json()
1232
+
1233
+ expectTypeOf(data).toEqualTypeOf<{ error: 'Bad request' }>()
1234
+ } else if (req.status === 401) {
1235
+ const data = await req.json()
1236
+
1237
+ expectTypeOf(data).toEqualTypeOf<{ error: 'Unauthorized' }>()
1238
+ }
1239
+ })
1240
+ })
1241
+
1242
+ const pathname = <T extends URL | string>(value: T): string =>
1243
+ value instanceof URL ? value.pathname : value
1244
+
1245
+ describe.each(['$path', '$url'] as const)('%s() with a param option', (cmd) => {
1246
+ const app = new App()
1247
+ .get('/posts/:id/comments', (c) => c.json({ ok: true }))
1248
+ .get('/something/:firstId/:secondId/:version?', (c) => c.json({ ok: true }))
1249
+ .get('/docs/:page', (c) => c.json({ ok: true }))
1250
+ .get('/files/:dir/', (c) => c.json({ ok: true }))
1251
+ .get('/:page', (c) => c.json({ ok: true }))
1252
+ .get('/index/:v?', (c) => c.json({ ok: true }))
1253
+ type AppType = typeof app
1254
+ const client = hc<AppType>('http://localhost')
1255
+
1256
+ it('Should return the correct url path - /posts/123/comments', async () => {
1257
+ const value = client.posts[':id'].comments[cmd]({
1258
+ param: {
1259
+ id: '123',
1260
+ },
1261
+ })
1262
+ expect(pathname(value)).toBe('/posts/123/comments')
1263
+ })
1264
+
1265
+ it('Should return the correct path - /posts/:id/comments', async () => {
1266
+ const value = client.posts[':id'].comments[cmd]()
1267
+ expect(pathname(value)).toBe('/posts/:id/comments')
1268
+ })
1269
+
1270
+ it('Should return the correct path - /something/123/456', async () => {
1271
+ const value = client.something[':firstId'][':secondId'][':version?'][cmd]({
1272
+ param: {
1273
+ firstId: '123',
1274
+ secondId: '456',
1275
+ version: undefined,
1276
+ },
1277
+ })
1278
+ expect(pathname(value)).toBe('/something/123/456')
1279
+ })
1280
+
1281
+ it('Should keep a param value of "index" - /docs/index', async () => {
1282
+ const value = client.docs[':page'][cmd]({
1283
+ param: {
1284
+ page: 'index',
1285
+ },
1286
+ })
1287
+ expect(pathname(value)).toBe('/docs/index')
1288
+ })
1289
+
1290
+ it('Should keep a root-level param value of "index" - /index', async () => {
1291
+ const value = client[':page'][cmd]({
1292
+ param: {
1293
+ page: 'index',
1294
+ },
1295
+ })
1296
+ expect(pathname(value)).toBe('/index')
1297
+ })
1298
+
1299
+ it('Should keep a literal index segment when an optional param is omitted - /index', async () => {
1300
+ const value = client.index[':v?'][cmd]({
1301
+ param: {
1302
+ v: undefined,
1303
+ },
1304
+ })
1305
+ expect(pathname(value)).toBe('/index')
1306
+ })
1307
+
1308
+ it('Should still drop the index alias of a route with a param - /files/123', async () => {
1309
+ const value = client.files[':dir'].index[cmd]({
1310
+ param: {
1311
+ dir: '123',
1312
+ },
1313
+ })
1314
+ expect(pathname(value)).toBe('/files/123')
1315
+ })
1316
+ })
1317
+
1318
+ describe('$url() / $path() with a query option', () => {
1319
+ const app = new App()
1320
+ .get(
1321
+ '/posts',
1322
+ validator('query', () => {
1323
+ return {} as { filter: 'test' }
1324
+ }),
1325
+ (c) => c.json({ ok: true })
1326
+ )
1327
+ .get('/docs/:page', (c) => c.json({ ok: true }))
1328
+ type AppType = typeof app
1329
+ const client = hc<AppType>('http://localhost')
1330
+
1331
+ it('Should return the correct path - /posts?filter=test', async () => {
1332
+ const url = client.posts.$url({
1333
+ query: {
1334
+ filter: 'test',
1335
+ },
1336
+ })
1337
+ expect(url.search).toBe('?filter=test')
1338
+
1339
+ const path = client.posts.$path({
1340
+ query: {
1341
+ filter: 'test',
1342
+ },
1343
+ })
1344
+ expect(path).toBe('/posts?filter=test')
1345
+ })
1346
+
1347
+ it('Should return the correct path - /docs/index?filter=test', async () => {
1348
+ const url = client.docs[':page'].$url({
1349
+ param: {
1350
+ page: 'index',
1351
+ },
1352
+ query: {
1353
+ filter: 'test',
1354
+ },
1355
+ })
1356
+ expect(url.href).toBe('http://localhost/docs/index?filter=test')
1357
+
1358
+ const path = client.docs[':page'].$path({
1359
+ param: {
1360
+ page: 'index',
1361
+ },
1362
+ query: {
1363
+ filter: 'test',
1364
+ },
1365
+ })
1366
+ expect(path).toBe('/docs/index?filter=test')
1367
+ })
1368
+ })
1369
+
1370
+ describe('Client can be awaited', () => {
1371
+ it('Can be awaited without side effects', async () => {
1372
+ const client = hc('http://localhost')
1373
+
1374
+ const awaited = await client
1375
+
1376
+ expect(awaited).toEqual(client)
1377
+ })
1378
+ })
1379
+
1380
+ describe('Dynamic headers', () => {
1381
+ const app = new App()
1382
+
1383
+ const route = app.post('/posts', (c) => {
1384
+ return c.json({
1385
+ requestDynamic: 'dummy',
1386
+ requestApp: 'dummy',
1387
+ })
1388
+ })
1389
+
1390
+ type AppType = typeof route
1391
+
1392
+ const server = setupServer(
1393
+ http.post('http://localhost/posts', async ({ request }) => {
1394
+ const requestDynamic = request.headers.get('x-dynamic')
1395
+ const requestApp = request.headers.get('x-acryl')
1396
+ const payload = {
1397
+ requestDynamic,
1398
+ requestApp,
1399
+ }
1400
+ return HttpResponse.json(payload)
1401
+ })
1402
+ )
1403
+
1404
+ beforeAll(() => server.listen())
1405
+ afterEach(() => server.resetHandlers())
1406
+ afterAll(() => server.close())
1407
+
1408
+ let dynamic = ''
1409
+
1410
+ const client = hc<AppType>('http://localhost', {
1411
+ headers: () => ({ 'x-acryl': 'acryl', 'x-dynamic': dynamic }),
1412
+ })
1413
+
1414
+ it('Should have "x-dynamic": "one"', async () => {
1415
+ dynamic = 'one'
1416
+
1417
+ const res = await client.posts.$post()
1418
+
1419
+ expect(res.ok).toBe(true)
1420
+ const data = await res.json()
1421
+ expect(data.requestDynamic).toEqual('one')
1422
+ })
1423
+
1424
+ it('Should have "x-dynamic": "two"', async () => {
1425
+ dynamic = 'two'
1426
+
1427
+ const res = await client.posts.$post()
1428
+
1429
+ expect(res.ok).toBe(true)
1430
+ const data = await res.json()
1431
+ expect(data.requestDynamic).toEqual('two')
1432
+ })
1433
+
1434
+ it('Should merge function headers with per-request object headers', async () => {
1435
+ const clientWithFn = hc<AppType>('http://localhost', {
1436
+ headers: async () => ({ 'x-acryl': 'acryl' }),
1437
+ })
1438
+
1439
+ const res = await clientWithFn.posts.$post({}, { headers: { 'x-dynamic': 'request' } })
1440
+
1441
+ expect(res.ok).toBe(true)
1442
+ const data = await res.json()
1443
+ expect(data.requestApp).toEqual('acryl')
1444
+ expect(data.requestDynamic).toEqual('request')
1445
+ })
1446
+
1447
+ it('Should merge function headers with per-request function headers', async () => {
1448
+ const clientWithFn = hc<AppType>('http://localhost', {
1449
+ headers: () => ({ 'x-acryl': 'acryl' }),
1450
+ })
1451
+
1452
+ const res = await clientWithFn.posts.$post(
1453
+ {},
1454
+ { headers: async () => ({ 'x-dynamic': 'request' }) }
1455
+ )
1456
+
1457
+ expect(res.ok).toBe(true)
1458
+ const data = await res.json()
1459
+ expect(data.requestApp).toEqual('acryl')
1460
+ expect(data.requestDynamic).toEqual('request')
1461
+ })
1462
+
1463
+ it('Should merge object headers with per-request function headers', async () => {
1464
+ const clientWithObject = hc<AppType>('http://localhost', {
1465
+ headers: { 'x-acryl': 'acryl' },
1466
+ })
1467
+
1468
+ const res = await clientWithObject.posts.$post(
1469
+ {},
1470
+ { headers: async () => ({ 'x-dynamic': 'request' }) }
1471
+ )
1472
+
1473
+ expect(res.ok).toBe(true)
1474
+ const data = await res.json()
1475
+ expect(data.requestApp).toEqual('acryl')
1476
+ expect(data.requestDynamic).toEqual('request')
1477
+ })
1478
+
1479
+ it('Should prioritize per-request headers when the keys conflict', async () => {
1480
+ const clientWithFn = hc<AppType>('http://localhost', {
1481
+ headers: async () => ({ 'x-acryl': 'client' }),
1482
+ })
1483
+
1484
+ const res = await clientWithFn.posts.$post({}, { headers: { 'x-acryl': 'request' } })
1485
+
1486
+ expect(res.ok).toBe(true)
1487
+ const data = await res.json()
1488
+ expect(data.requestApp).toEqual('request')
1489
+ })
1490
+ })
1491
+
1492
+ describe('RequestInit work as expected', () => {
1493
+ const app = new App()
1494
+
1495
+ const route = app
1496
+ .get('/credentials', (c) => {
1497
+ return c.text('' as RequestCredentials)
1498
+ })
1499
+ .get('/headers', (c) => {
1500
+ return c.json({} as Record<string, string>)
1501
+ })
1502
+ .post('/headers', (c) => c.text('Not found', 404))
1503
+
1504
+ type AppType = typeof route
1505
+
1506
+ const server = setupServer(
1507
+ http.get('http://localhost/credentials', ({ request }) => {
1508
+ return HttpResponse.text(request.credentials)
1509
+ }),
1510
+ http.get('http://localhost/headers', ({ request }) => {
1511
+ const allHeaders: Record<string, string> = {}
1512
+ for (const [k, v] of request.headers.entries()) {
1513
+ allHeaders[k] = v
1514
+ }
1515
+
1516
+ return HttpResponse.json(allHeaders)
1517
+ }),
1518
+ http.post('http://localhost/headers', () => {
1519
+ return HttpResponse.text('Should not be here', {
1520
+ status: 400,
1521
+ })
1522
+ })
1523
+ )
1524
+
1525
+ beforeAll(() => server.listen())
1526
+ afterEach(() => server.resetHandlers())
1527
+ afterAll(() => server.close())
1528
+
1529
+ const client = hc<AppType>('http://localhost', {
1530
+ headers: { 'x-acryl': 'fire' },
1531
+ init: {
1532
+ credentials: 'include',
1533
+ },
1534
+ })
1535
+
1536
+ it('Should overwrite method and fail', async () => {
1537
+ const res = await client.headers.$get(undefined, { init: { method: 'POST' } })
1538
+
1539
+ expect(res.ok).toBe(false)
1540
+ })
1541
+
1542
+ it('Should clear headers', async () => {
1543
+ const res = await client.headers.$get(undefined, { init: { headers: undefined } })
1544
+
1545
+ expect(res.ok).toBe(true)
1546
+ const data = await res.json()
1547
+ expect(data).toEqual({})
1548
+ })
1549
+
1550
+ it('Should overwrite headers', async () => {
1551
+ const res = await client.headers.$get(undefined, {
1552
+ init: { headers: new Headers({ 'x-acryl': 'awesome' }) },
1553
+ })
1554
+
1555
+ expect(res.ok).toBe(true)
1556
+ const data = await res.json()
1557
+ expect(data).toEqual({ 'x-acryl': 'awesome' })
1558
+ })
1559
+
1560
+ it('credentials is include', async () => {
1561
+ const res = await client.credentials.$get()
1562
+
1563
+ expect(res.ok).toBe(true)
1564
+ const data = await res.text()
1565
+ expect(data).toEqual('include')
1566
+ })
1567
+
1568
+ it('deepMerge should works and not unset credentials', async () => {
1569
+ const res = await client.credentials.$get(undefined, { init: { headers: { hi: 'hello' } } })
1570
+
1571
+ expect(res.ok).toBe(true)
1572
+ const data = await res.text()
1573
+ expect(data).toEqual('include')
1574
+ })
1575
+
1576
+ it('Should unset credentials', async () => {
1577
+ const res = await client.credentials.$get(undefined, { init: { credentials: undefined } })
1578
+
1579
+ expect(res.ok).toBe(true)
1580
+ const data = await res.text()
1581
+ expect(data).toEqual('same-origin')
1582
+ })
1583
+ })
1584
+
1585
+ describe('WebSocket URL Protocol Translation', () => {
1586
+ const app = new App()
1587
+ const route = app.get(
1588
+ '/',
1589
+ upgradeWebSocket((c) => ({
1590
+ onMessage(event, ws) {
1591
+ console.log(`Message from client: ${event.data}`)
1592
+ ws.send('Hello from server!')
1593
+ },
1594
+ onClose: () => {
1595
+ console.log('Connection closed')
1596
+ },
1597
+ }))
1598
+ )
1599
+
1600
+ type AppType = typeof route
1601
+
1602
+ const server = setupServer()
1603
+ const webSocketMock = vi.fn()
1604
+
1605
+ beforeAll(() => server.listen())
1606
+ beforeEach(() => {
1607
+ vi.stubGlobal('WebSocket', webSocketMock)
1608
+ })
1609
+ afterEach(() => {
1610
+ vi.clearAllMocks()
1611
+ server.resetHandlers()
1612
+ })
1613
+ afterAll(() => server.close())
1614
+
1615
+ it('Translates HTTP to ws', async () => {
1616
+ const client = hc<AppType>('http://localhost')
1617
+ client.index.$ws()
1618
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/')
1619
+ })
1620
+
1621
+ it('Translates HTTPS to wss', async () => {
1622
+ const client = hc<AppType>('https://localhost')
1623
+ client.index.$ws()
1624
+ expect(webSocketMock).toHaveBeenCalledWith('wss://localhost/')
1625
+ })
1626
+
1627
+ it('Keeps ws unchanged', async () => {
1628
+ const client = hc<AppType>('ws://localhost')
1629
+ client.index.$ws()
1630
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/')
1631
+ })
1632
+
1633
+ it('Keeps wss unchanged', async () => {
1634
+ const client = hc<AppType>('wss://localhost')
1635
+ client.index.$ws()
1636
+ expect(webSocketMock).toHaveBeenCalledWith('wss://localhost/')
1637
+ })
1638
+
1639
+ it('Preserves an index path parameter value', async () => {
1640
+ const dynamicRoute = new App().get(
1641
+ '/:id',
1642
+ upgradeWebSocket(() => ({}))
1643
+ )
1644
+ const client = hc<typeof dynamicRoute>('http://localhost')
1645
+
1646
+ client[':id'].$ws({ param: { id: 'index' } })
1647
+
1648
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/index')
1649
+ })
1650
+ })
1651
+
1652
+ describe('WebSocket URL Protocol Translation with Query Parameters', () => {
1653
+ const app = new App()
1654
+ const route = app.get(
1655
+ '/',
1656
+ upgradeWebSocket((c) => ({
1657
+ onMessage(event, ws) {
1658
+ ws.send('Hello from server!')
1659
+ },
1660
+ onClose: () => {
1661
+ console.log('Connection closed')
1662
+ },
1663
+ }))
1664
+ )
1665
+
1666
+ type AppType = typeof route
1667
+
1668
+ const server = setupServer()
1669
+ const webSocketMock = vi.fn()
1670
+
1671
+ beforeAll(() => server.listen())
1672
+ beforeEach(() => {
1673
+ vi.stubGlobal('WebSocket', webSocketMock)
1674
+ })
1675
+ afterEach(() => {
1676
+ vi.clearAllMocks()
1677
+ server.resetHandlers()
1678
+ })
1679
+ afterAll(() => server.close())
1680
+
1681
+ it('Translates HTTP to ws and includes query parameters', async () => {
1682
+ const client = hc<AppType>('http://localhost')
1683
+ client.index.$ws({
1684
+ query: {
1685
+ id: '123',
1686
+ type: 'test',
1687
+ tag: ['a', 'b'],
1688
+ },
1689
+ })
1690
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/?id=123&type=test&tag=a&tag=b')
1691
+ })
1692
+
1693
+ it('Translates HTTPS to wss and includes query parameters', async () => {
1694
+ const client = hc<AppType>('https://localhost')
1695
+ client.index.$ws({
1696
+ query: {
1697
+ id: '456',
1698
+ type: 'secure',
1699
+ },
1700
+ })
1701
+ expect(webSocketMock).toHaveBeenCalledWith('wss://localhost/?id=456&type=secure')
1702
+ })
1703
+
1704
+ it('Keeps ws unchanged and includes query parameters', async () => {
1705
+ const client = hc<AppType>('ws://localhost')
1706
+ client.index.$ws({
1707
+ query: {
1708
+ id: '789',
1709
+ type: 'plain',
1710
+ },
1711
+ })
1712
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/?id=789&type=plain')
1713
+ })
1714
+
1715
+ it('Keeps wss unchanged and includes query parameters', async () => {
1716
+ const client = hc<AppType>('wss://localhost')
1717
+ client.index.$ws({
1718
+ query: {
1719
+ id: '1011',
1720
+ type: 'secure',
1721
+ },
1722
+ })
1723
+ expect(webSocketMock).toHaveBeenCalledWith('wss://localhost/?id=1011&type=secure')
1724
+ })
1725
+ })
1726
+
1727
+ describe('Client can be console.log in react native', () => {
1728
+ it('Returns a function name with function.name.toString', async () => {
1729
+ const client = hc('http://localhost')
1730
+ // @ts-ignore
1731
+ expect(client.posts.name.toString()).toEqual('posts')
1732
+ })
1733
+
1734
+ it('Returns a function name with function.name.valueOf', async () => {
1735
+ const client = hc('http://localhost')
1736
+ // @ts-ignore
1737
+ expect(client.posts.name.valueOf()).toEqual('posts')
1738
+ })
1739
+
1740
+ it('Returns a function with function.valueOf', async () => {
1741
+ const client = hc('http://localhost')
1742
+ expect(typeof client.posts.valueOf()).toEqual('function')
1743
+ })
1744
+
1745
+ it('Returns a function source with function.toString', async () => {
1746
+ const client = hc('http://localhost')
1747
+ expect(client.posts.toString()).toMatch('function proxyCallback')
1748
+ })
1749
+ })
1750
+
1751
+ describe('Text response', () => {
1752
+ const text = 'My name is App'
1753
+ const obj = { ok: true }
1754
+ const server = setupServer(
1755
+ http.get('http://localhost/about/me', async () => {
1756
+ return HttpResponse.text(text)
1757
+ }),
1758
+ http.get('http://localhost/api', async ({ request }) => {
1759
+ return HttpResponse.json(obj)
1760
+ })
1761
+ )
1762
+
1763
+ beforeAll(() => server.listen())
1764
+ afterEach(() => server.resetHandlers())
1765
+ afterAll(() => server.close())
1766
+
1767
+ const app = new App().get('/about/me', (c) => c.text(text)).get('/api', (c) => c.json(obj))
1768
+ const client = hc<typeof app>('http://localhost/')
1769
+
1770
+ it('Should be never with res.json() - /about/me', async () => {
1771
+ const res = await client.about.me.$get()
1772
+ type Actual = ReturnType<typeof res.json>
1773
+ type Expected = Promise<never>
1774
+ type verify = Expect<Equal<Expected, Actual>>
1775
+ })
1776
+
1777
+ it('Should be "Hello, World!" with res.text() - /about/me', async () => {
1778
+ const res = await client.about.me.$get()
1779
+ const data = await res.text()
1780
+ expectTypeOf(data).toEqualTypeOf<'My name is App'>()
1781
+ expect(data).toBe(text)
1782
+ })
1783
+
1784
+ /**
1785
+ * Also check the type of JSON response with res.text().
1786
+ */
1787
+ it('Should be string with res.text() - /api', async () => {
1788
+ const res = await client.api.$get()
1789
+ type Actual = ReturnType<typeof res.text>
1790
+ type Expected = Promise<string>
1791
+ type verify = Expect<Equal<Expected, Actual>>
1792
+ })
1793
+ })
1794
+
1795
+ describe('Redirect response - only types', () => {
1796
+ const server = setupServer(
1797
+ http.get('http://localhost/', async () => {
1798
+ return HttpResponse.redirect('/')
1799
+ })
1800
+ )
1801
+
1802
+ beforeAll(() => server.listen())
1803
+ afterEach(() => server.resetHandlers())
1804
+ afterAll(() => server.close())
1805
+
1806
+ const condition = () => true
1807
+ const app = new App().get('/', async (c) => {
1808
+ const ok = condition()
1809
+ const temporary = condition()
1810
+ if (ok) {
1811
+ return c.json({ ok: true }, 200)
1812
+ }
1813
+ if (temporary) {
1814
+ return c.redirect('/302')
1815
+ }
1816
+ return c.redirect('/301', 301)
1817
+ })
1818
+
1819
+ const client = hc<typeof app>('http://localhost/')
1820
+ const req = client.index.$get
1821
+
1822
+ it('Should infer request type the type correctly', () => {
1823
+ type Actual = InferResponseType<typeof req>
1824
+ type Expected =
1825
+ | {
1826
+ ok: true
1827
+ }
1828
+ | undefined
1829
+ type verify = Expect<Equal<Expected, Actual>>
1830
+ })
1831
+
1832
+ it('Should infer response type correctly', async () => {
1833
+ const res = await req()
1834
+ if (res.ok) {
1835
+ const data = await res.json()
1836
+ expectTypeOf(data).toMatchTypeOf({ ok: true })
1837
+ }
1838
+ if (res.status === 301) {
1839
+ type Expected = ClientResponse<undefined, 301, 'redirect'>
1840
+ type verify = Expect<Equal<Expected, typeof res>>
1841
+ }
1842
+ if (res.status === 302) {
1843
+ type Expected = ClientResponse<undefined, 302, 'redirect'>
1844
+ type verify = Expect<Equal<Expected, typeof res>>
1845
+ }
1846
+ })
1847
+ })
1848
+
1849
+ describe('WebSocket Provider Integration', () => {
1850
+ const app = new App()
1851
+ const route = app.get(
1852
+ '/',
1853
+ upgradeWebSocket((c) => ({
1854
+ onMessage(event, ws) {
1855
+ ws.send('Hello from server!')
1856
+ },
1857
+ onClose() {
1858
+ console.log('Connection closed')
1859
+ },
1860
+ }))
1861
+ )
1862
+
1863
+ type AppType = typeof route
1864
+
1865
+ const server = setupServer()
1866
+ beforeAll(() => server.listen())
1867
+ afterEach(() => {
1868
+ vi.clearAllMocks()
1869
+ server.resetHandlers()
1870
+ })
1871
+ afterAll(() => server.close())
1872
+
1873
+ it.each([
1874
+ {
1875
+ description: 'should initialize the WebSocket provider correctly',
1876
+ url: 'http://localhost',
1877
+ query: undefined,
1878
+ expectedUrl: 'ws://localhost/',
1879
+ },
1880
+ {
1881
+ description: 'should correctly add query parameters to the WebSocket URL',
1882
+ url: 'http://localhost',
1883
+ query: { id: '123', type: 'test', tag: ['a', 'b'] },
1884
+ expectedUrl: 'ws://localhost/?id=123&type=test&tag=a&tag=b',
1885
+ },
1886
+ ])('$description', ({ url, expectedUrl, query }) => {
1887
+ const webSocketMock = vi.fn()
1888
+ const client = hc<AppType>(url, {
1889
+ webSocket(url, options) {
1890
+ return webSocketMock(url, options)
1891
+ },
1892
+ })
1893
+ client.index.$ws({ query })
1894
+ expect(webSocketMock).toHaveBeenCalledWith(expectedUrl, undefined)
1895
+ })
1896
+ })
1897
+
1898
+ describe('Query arrays containing undefined', () => {
1899
+ // A validator whose query field is an optional union gives the client an input
1900
+ // type of `(string | undefined)[] | undefined`, so this compiles with no cast.
1901
+ const app = new App().get(
1902
+ '/search',
1903
+ validator('query', () => ({}) as { tag: (string | undefined)[] | undefined }),
1904
+ (c) => c.json({ ok: true })
1905
+ )
1906
+ type AppType = typeof app
1907
+
1908
+ it('Should not send the literal string "undefined" for an absent entry', async () => {
1909
+ let requestedUrl = ''
1910
+ const client = hc<AppType>('http://localhost', {
1911
+ fetch: (input: RequestInfo | URL) => {
1912
+ requestedUrl = input.toString()
1913
+ return Promise.resolve(new Response('{}'))
1914
+ },
1915
+ })
1916
+
1917
+ await client.search.$get({ query: { tag: ['a', undefined, 'b'] } })
1918
+
1919
+ expect(requestedUrl).toBe('http://localhost/search?tag=a&tag=b')
1920
+ })
1921
+ })
1922
+
1923
+ describe('Empty serialized query', () => {
1924
+ const app = new App().get(
1925
+ '/search',
1926
+ validator('query', () => ({}) as { q?: string }),
1927
+ (c) => c.json({ ok: true })
1928
+ )
1929
+ const input = { query: { q: undefined } }
1930
+
1931
+ it('Should omit the query delimiter when no query values are serialized', async () => {
1932
+ let requestedUrl = ''
1933
+ const client = hc<typeof app>('http://localhost', {
1934
+ fetch: (input: RequestInfo | URL) => {
1935
+ requestedUrl = input.toString()
1936
+ return Promise.resolve(new Response('{}'))
1937
+ },
1938
+ })
1939
+
1940
+ await client.search.$get(input)
1941
+
1942
+ expect(requestedUrl).toBe('http://localhost/search')
1943
+ expect(client.search.$url(input).href).toBe('http://localhost/search')
1944
+ expect(client.search.$path(input)).toBe('/search')
1945
+ })
1946
+
1947
+ it('Should omit the query delimiter when a custom serializer returns no values', async () => {
1948
+ let requestedUrl = ''
1949
+ const client = hc<typeof app>('http://localhost', {
1950
+ buildSearchParams: () => new URLSearchParams(),
1951
+ fetch: (input: RequestInfo | URL) => {
1952
+ requestedUrl = input.toString()
1953
+ return Promise.resolve(new Response('{}'))
1954
+ },
1955
+ })
1956
+ const customInput = { query: { q: 'ignored' } }
1957
+
1958
+ await client.search.$get(customInput)
1959
+
1960
+ expect(requestedUrl).toBe('http://localhost/search')
1961
+ expect(client.search.$url(customInput).href).toBe('http://localhost/search')
1962
+ expect(client.search.$path(customInput)).toBe('/search')
1963
+ })
1964
+
1965
+ it('Should preserve empty-string query values', async () => {
1966
+ let requestedUrl = ''
1967
+ const client = hc<typeof app>('http://localhost', {
1968
+ fetch: (input: RequestInfo | URL) => {
1969
+ requestedUrl = input.toString()
1970
+ return Promise.resolve(new Response('{}'))
1971
+ },
1972
+ })
1973
+ const emptyStringInput = { query: { q: '' } }
1974
+
1975
+ await client.search.$get(emptyStringInput)
1976
+
1977
+ expect(requestedUrl).toBe('http://localhost/search?q=')
1978
+ expect(client.search.$url(emptyStringInput).href).toBe('http://localhost/search?q=')
1979
+ expect(client.search.$path(emptyStringInput)).toBe('/search?q=')
1980
+ })
1981
+ })
1982
+
1983
+ describe('Custom buildSearchParams', () => {
1984
+ const app = new App()
1985
+ const route = app.get(
1986
+ '/search',
1987
+ validator('query', () => {
1988
+ return {} as { q: string; tags: string[] }
1989
+ }),
1990
+ (c) => {
1991
+ return c.json({
1992
+ message: 'success',
1993
+ queryString: '',
1994
+ })
1995
+ }
1996
+ )
1997
+
1998
+ type AppType = typeof route
1999
+
2000
+ const server = setupServer(
2001
+ http.get('http://localhost/search', ({ request }) => {
2002
+ const url = new URL(request.url)
2003
+ return HttpResponse.json({
2004
+ message: 'success',
2005
+ queryString: url.search,
2006
+ })
2007
+ })
2008
+ )
2009
+
2010
+ beforeAll(() => server.listen())
2011
+ afterEach(() => server.resetHandlers())
2012
+ afterAll(() => server.close())
2013
+
2014
+ // Custom buildSearchParams that uses bracket notation for arrays (key[]=value)
2015
+ const customBuildSearchParams = (query: Record<string, string | string[]>) => {
2016
+ const searchParams = new URLSearchParams()
2017
+ for (const [k, v] of Object.entries(query)) {
2018
+ if (v === undefined) {
2019
+ continue
2020
+ }
2021
+ if (Array.isArray(v)) {
2022
+ v.forEach((item) => searchParams.append(`${k}[]`, item))
2023
+ } else {
2024
+ searchParams.set(k, v)
2025
+ }
2026
+ }
2027
+ return searchParams
2028
+ }
2029
+
2030
+ it('Should use custom buildSearchParams for query serialization', async () => {
2031
+ const client = hc<AppType>('http://localhost', { buildSearchParams: customBuildSearchParams })
2032
+ const res = await client.search.$get({ query: { q: 'test', tags: ['tag1', 'tag2', 'tag3'] } })
2033
+ const data = await res.json()
2034
+
2035
+ expect(res.status).toBe(200)
2036
+ expect(data.queryString).toBe('?q=test&tags%5B%5D=tag1&tags%5B%5D=tag2&tags%5B%5D=tag3')
2037
+ })
2038
+
2039
+ it('Should use default buildSearchParams when custom one is not provided', async () => {
2040
+ const client = hc<AppType>('http://localhost')
2041
+ const res = await client.search.$get({ query: { q: 'test', tags: ['tag1', 'tag2'] } })
2042
+ const data = await res.json()
2043
+
2044
+ expect(res.status).toBe(200)
2045
+ expect(data.queryString).toBe('?q=test&tags=tag1&tags=tag2')
2046
+ })
2047
+
2048
+ it('Should use custom buildSearchParams in $url() method', () => {
2049
+ const client = hc<AppType>('http://localhost', { buildSearchParams: customBuildSearchParams })
2050
+ const url = client.search.$url({ query: { q: 'test', tags: ['tag1', 'tag2'] } })
2051
+
2052
+ expect(url.href).toBe('http://localhost/search?q=test&tags%5B%5D=tag1&tags%5B%5D=tag2')
2053
+ })
2054
+
2055
+ it('Should use default buildSearchParams in $url() when custom one is not provided', () => {
2056
+ const client = hc<AppType>('http://localhost')
2057
+ const url = client.search.$url({ query: { q: 'test', tags: ['tag1', 'tag2'] } })
2058
+
2059
+ expect(url.href).toBe('http://localhost/search?q=test&tags=tag1&tags=tag2')
2060
+ })
2061
+
2062
+ it('Should use custom buildSearchParams in $ws() method', () => {
2063
+ const webSocketMock = vi.fn()
2064
+ const client = hc<AppType>('http://localhost', {
2065
+ buildSearchParams: customBuildSearchParams,
2066
+ webSocket(url, options) {
2067
+ return webSocketMock(url, options)
2068
+ },
2069
+ })
2070
+ // @ts-expect-error search route does not explicitly define $ws in mock AppType
2071
+ client.search.$ws({ query: { q: 'test', tags: ['tag1', 'tag2'] } })
2072
+
2073
+ expect(webSocketMock).toHaveBeenCalledWith(
2074
+ 'ws://localhost/search?q=test&tags%5B%5D=tag1&tags%5B%5D=tag2',
2075
+ undefined
2076
+ )
2077
+ })
2078
+
2079
+ it('Should filter out undefined query parameters in $ws()', () => {
2080
+ const webSocketMock = vi.fn()
2081
+ const client = hc<AppType>('http://localhost', {
2082
+ webSocket(url, options) {
2083
+ return webSocketMock(url, options)
2084
+ },
2085
+ })
2086
+ // @ts-expect-error search route does not explicitly define $ws in mock AppType
2087
+ client.search.$ws({ query: { q: 'test', tags: undefined as any } })
2088
+
2089
+ expect(webSocketMock).toHaveBeenCalledWith('ws://localhost/search?q=test', undefined)
2090
+ })
2091
+ })
2092
+
2093
+ describe('ApplyGlobalResponse Type Helper', () => {
2094
+ const server = setupServer(
2095
+ http.get('http://localhost/api/users', () => {
2096
+ return HttpResponse.json({ users: ['alice', 'bob'] })
2097
+ }),
2098
+ http.get('http://localhost/api/error', () => {
2099
+ return HttpResponse.json(
2100
+ { error: 'Internal Server Error', message: 'Something went wrong' },
2101
+ { status: 500 }
2102
+ )
2103
+ }),
2104
+ http.get('http://localhost/api/unauthorized', () => {
2105
+ return HttpResponse.json({ error: 'Unauthorized', message: 'Please login' }, { status: 401 })
2106
+ })
2107
+ )
2108
+
2109
+ beforeAll(() => server.listen())
2110
+ afterEach(() => server.resetHandlers())
2111
+ afterAll(() => server.close())
2112
+
2113
+ it('Should add global error response types to all routes', () => {
2114
+ // Use explicit status codes for proper type narrowing
2115
+ const app = new App().get('/api/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
2116
+
2117
+ // Apply global error responses with new object syntax
2118
+ type AppWithGlobalErrors = ApplyGlobalResponse<
2119
+ typeof app,
2120
+ {
2121
+ 401: { json: { error: string; message: string } }
2122
+ 500: { json: { error: string; message: string } }
2123
+ }
2124
+ >
2125
+
2126
+ const client = hc<AppWithGlobalErrors>('http://localhost')
2127
+ const req = client.api.users.$get
2128
+
2129
+ // Type should be a union of normal response and global errors
2130
+ type ResponseType = InferResponseType<typeof req>
2131
+ type Expected = { users: string[] } | { error: string; message: string }
2132
+
2133
+ type verify = Expect<Equal<ResponseType, Expected>>
2134
+ })
2135
+
2136
+ it('Should support multiple global error status codes', async () => {
2137
+ const app = new App()
2138
+ .get('/api/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
2139
+ .get('/api/unauthorized', (c) =>
2140
+ c.json({ error: 'Unauthorized', message: 'Please login' }, 401)
2141
+ )
2142
+ .get('/api/error', (c) =>
2143
+ c.json({ error: 'Internal Server Error', message: 'Something went wrong' }, 500)
2144
+ )
2145
+
2146
+ // Apply multiple global error types in one definition
2147
+ type AppWithGlobalErrors = ApplyGlobalResponse<
2148
+ typeof app,
2149
+ {
2150
+ 401: { json: { error: string; message: string } }
2151
+ 500: { json: { error: string; message: string } }
2152
+ }
2153
+ >
2154
+
2155
+ const client = hc<AppWithGlobalErrors>('http://localhost')
2156
+
2157
+ // Verify runtime behavior for different status codes
2158
+ const usersRes = await client.api.users.$get()
2159
+ expect(usersRes.status).toBe(200)
2160
+
2161
+ const unauthorizedRes = await client.api.unauthorized.$get()
2162
+ expect(unauthorizedRes.status).toBe(401)
2163
+ expect(await unauthorizedRes.json()).toEqual({ error: 'Unauthorized', message: 'Please login' })
2164
+
2165
+ const errorRes = await client.api.error.$get()
2166
+ expect(errorRes.status).toBe(500)
2167
+ expect(await errorRes.json()).toEqual({
2168
+ error: 'Internal Server Error',
2169
+ message: 'Something went wrong',
2170
+ })
2171
+ })
2172
+
2173
+ it('Should work with onError handler pattern', () => {
2174
+ // Simulating typical App app with onError handler
2175
+ // Use explicit status code for proper type narrowing
2176
+ const app = new App().get('/api/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
2177
+
2178
+ // In real app: app.onError((err, c) => c.json({ error: err.message }, 500))
2179
+ type AppWithOnError = ApplyGlobalResponse<
2180
+ typeof app,
2181
+ {
2182
+ 500: { json: { error: string } }
2183
+ }
2184
+ >
2185
+
2186
+ const client = hc<AppWithOnError>('http://localhost')
2187
+ const req = client.api.users.$get
2188
+
2189
+ // RPC client should know about the error format
2190
+ type ResponseType = InferResponseType<typeof req>
2191
+ type Expected = { users: string[] } | { error: string }
2192
+
2193
+ type verify = Expect<Equal<ResponseType, Expected>>
2194
+ })
2195
+
2196
+ it('Should keep route() paths when global responses are applied', () => {
2197
+ const users = new App().get('/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
2198
+ const app = new App().route('/api', users)
2199
+
2200
+ type AppWithGlobalErrors = ApplyGlobalResponse<
2201
+ typeof app,
2202
+ {
2203
+ 500: { json: { error: string } }
2204
+ }
2205
+ >
2206
+
2207
+ const client = hc<AppWithGlobalErrors>('http://localhost')
2208
+ const req = client.api.users.$get
2209
+
2210
+ type ResponseType = InferResponseType<typeof req>
2211
+ type Expected = { users: string[] } | { error: string }
2212
+
2213
+ type verify = Expect<Equal<ResponseType, Expected>>
2214
+ })
2215
+ })
2216
+
2217
+ describe('PickResponseByStatusCode Type Helper', () => {
2218
+ it('Should keep only specified status code responses', () => {
2219
+ const app = new App().get('/api/users', (c) => {
2220
+ try {
2221
+ return c.json({ users: ['alice', 'bob'] }, 200)
2222
+ } catch {
2223
+ return c.json({ error: 'Internal Server Error' }, 500)
2224
+ }
2225
+ })
2226
+
2227
+ type AppSuccessOnly = PickResponseByStatusCode<typeof app, 200>
2228
+
2229
+ const client = hc<AppSuccessOnly>('http://localhost')
2230
+ const req = client.api.users.$get
2231
+
2232
+ type ResponseType = InferResponseType<typeof req>
2233
+ type Expected = { users: string[] }
2234
+ type verify = Expect<Equal<ResponseType, Expected>>
2235
+
2236
+ type Res = Awaited<ReturnType<typeof req>>
2237
+ type verifyOk = Expect<Equal<Res['ok'], true>>
2238
+ type verifyStatus = Expect<Equal<Res['status'], 200>>
2239
+ })
2240
+
2241
+ it('Should work with ApplyGlobalResponse', () => {
2242
+ const app = new App().get('/api/users', (c) => {
2243
+ try {
2244
+ return c.json({ users: ['alice', 'bob'] }, 200)
2245
+ } catch {
2246
+ return c.json({ error: 'Not Found' }, 404)
2247
+ }
2248
+ })
2249
+
2250
+ type AppWithGlobalErrors = ApplyGlobalResponse<
2251
+ typeof app,
2252
+ {
2253
+ 401: { json: { error: string; message: string } }
2254
+ 500: { json: { error: string; message: string } }
2255
+ }
2256
+ >
2257
+
2258
+ type AppSuccessOnly = PickResponseByStatusCode<AppWithGlobalErrors, 200>
2259
+
2260
+ const client = hc<AppSuccessOnly>('http://localhost')
2261
+ const req = client.api.users.$get
2262
+
2263
+ type ResponseType = InferResponseType<typeof req>
2264
+ type Expected = { users: string[] }
2265
+ type verify = Expect<Equal<ResponseType, Expected>>
2266
+ })
2267
+
2268
+ it('Should work with route() paths', () => {
2269
+ const users = new App().get('/users', (c) => {
2270
+ try {
2271
+ return c.json({ users: ['alice', 'bob'] }, 200)
2272
+ } catch {
2273
+ return c.json({ error: 'error' }, 500)
2274
+ }
2275
+ })
2276
+ const app = new App().route('/api', users)
2277
+
2278
+ type AppSuccessOnly = PickResponseByStatusCode<typeof app, 200>
2279
+
2280
+ const client = hc<AppSuccessOnly>('http://localhost')
2281
+ const req = client.api.users.$get
2282
+
2283
+ type ResponseType = InferResponseType<typeof req>
2284
+ type Expected = { users: string[] }
2285
+ type verify = Expect<Equal<ResponseType, Expected>>
2286
+ })
2287
+
2288
+ it('Should pick multiple status codes', () => {
2289
+ const app = new App().get('/api/users', (c) => {
2290
+ try {
2291
+ return c.json({ users: ['alice', 'bob'] }, 200)
2292
+ } catch {
2293
+ return c.json({ error: 'Bad Request' }, 400)
2294
+ }
2295
+ })
2296
+
2297
+ type AppWithGlobalErrors = ApplyGlobalResponse<
2298
+ typeof app,
2299
+ {
2300
+ 500: { json: { error: string } }
2301
+ }
2302
+ >
2303
+
2304
+ type AppFiltered = PickResponseByStatusCode<AppWithGlobalErrors, 200 | 400>
2305
+
2306
+ const client = hc<AppFiltered>('http://localhost')
2307
+ const req = client.api.users.$get
2308
+
2309
+ type ResponseType = InferResponseType<typeof req>
2310
+ type Expected = { users: string[] } | { error: string }
2311
+ type verify = Expect<Equal<ResponseType, Expected>>
2312
+ })
2313
+ })