ts-procedures 7.2.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +65 -3
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +6 -8
  3. package/agent_config/claude-code/skills/ts-procedures/SKILL.md +30 -33
  4. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +139 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +208 -231
  6. package/agent_config/claude-code/skills/ts-procedures/patterns.md +80 -153
  7. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +1 -1
  8. package/agent_config/claude-code/skills/ts-procedures-review/checklist.md +4 -5
  9. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +4 -7
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono.md +223 -0
  11. package/agent_config/copilot/copilot-instructions.md +36 -48
  12. package/agent_config/cursor/cursorrules +36 -48
  13. package/build/client/call.js +4 -1
  14. package/build/client/call.js.map +1 -1
  15. package/build/client/call.test.js +23 -0
  16. package/build/client/call.test.js.map +1 -1
  17. package/build/client/fetch-adapter.js +3 -1
  18. package/build/client/fetch-adapter.js.map +1 -1
  19. package/build/client/fetch-adapter.test.js +11 -1
  20. package/build/client/fetch-adapter.test.js.map +1 -1
  21. package/build/client/index.test.js +7 -7
  22. package/build/client/index.test.js.map +1 -1
  23. package/build/client/request-builder.d.ts +1 -1
  24. package/build/client/request-builder.js +2 -2
  25. package/build/client/request-builder.js.map +1 -1
  26. package/build/client/stream.js +13 -2
  27. package/build/client/stream.js.map +1 -1
  28. package/build/client/stream.test.js +32 -7
  29. package/build/client/stream.test.js.map +1 -1
  30. package/build/client/typed-error-dispatch.test.js +8 -92
  31. package/build/client/typed-error-dispatch.test.js.map +1 -1
  32. package/build/client/types.d.ts +21 -3
  33. package/build/codegen/bin/cli.js +0 -0
  34. package/build/codegen/e2e.test.js +87 -23
  35. package/build/codegen/e2e.test.js.map +1 -1
  36. package/build/codegen/emit-errors.integration.test.js +1 -1
  37. package/build/codegen/emit-errors.integration.test.js.map +1 -1
  38. package/build/codegen/emit-scope.js +308 -47
  39. package/build/codegen/emit-scope.js.map +1 -1
  40. package/build/codegen/emit-scope.test.js +363 -110
  41. package/build/codegen/emit-scope.test.js.map +1 -1
  42. package/build/codegen/pipeline.test.js +7 -7
  43. package/build/codegen/pipeline.test.js.map +1 -1
  44. package/build/codegen/resolve-envelope.js +1 -1
  45. package/build/codegen/resolve-envelope.js.map +1 -1
  46. package/build/codegen/resolve-envelope.test.js +5 -5
  47. package/build/codegen/resolve-envelope.test.js.map +1 -1
  48. package/build/codegen/targets/_shared/route-slots.d.ts +8 -3
  49. package/build/codegen/targets/_shared/route-slots.js +49 -8
  50. package/build/codegen/targets/_shared/route-slots.js.map +1 -1
  51. package/build/codegen/targets/_shared/route-slots.test.js +99 -26
  52. package/build/codegen/targets/_shared/route-slots.test.js.map +1 -1
  53. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js +88 -17
  54. package/build/codegen/targets/kotlin/emit-route-kotlin.test.js.map +1 -1
  55. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js +9 -6
  56. package/build/codegen/targets/kotlin/emit-scope-kotlin.test.js.map +1 -1
  57. package/build/codegen/targets/kotlin/integration.test.js +6 -0
  58. package/build/codegen/targets/kotlin/integration.test.js.map +1 -1
  59. package/build/codegen/targets/swift/access-level.test.js +8 -11
  60. package/build/codegen/targets/swift/access-level.test.js.map +1 -1
  61. package/build/codegen/targets/swift/emit-route-swift.test.js +91 -20
  62. package/build/codegen/targets/swift/emit-route-swift.test.js.map +1 -1
  63. package/build/codegen/targets/swift/emit-scope-swift.test.js +12 -9
  64. package/build/codegen/targets/swift/emit-scope-swift.test.js.map +1 -1
  65. package/build/codegen/targets/swift/integration.test.js +6 -0
  66. package/build/codegen/targets/swift/integration.test.js.map +1 -1
  67. package/build/create-http-stream.d.ts +58 -0
  68. package/build/create-http-stream.js +122 -0
  69. package/build/create-http-stream.js.map +1 -0
  70. package/build/create-http-stream.test.js +88 -0
  71. package/build/create-http-stream.test.js.map +1 -0
  72. package/build/create-http.d.ts +49 -0
  73. package/build/create-http.js +108 -0
  74. package/build/create-http.js.map +1 -0
  75. package/build/create-http.test.js +137 -0
  76. package/build/create-http.test.js.map +1 -0
  77. package/build/create-stream.d.ts +35 -0
  78. package/build/create-stream.js +123 -0
  79. package/build/create-stream.js.map +1 -0
  80. package/build/create-stream.test.js +428 -0
  81. package/build/create-stream.test.js.map +1 -0
  82. package/build/create.d.ts +28 -0
  83. package/build/create.js +82 -0
  84. package/build/create.js.map +1 -0
  85. package/build/create.test.js +483 -0
  86. package/build/create.test.js.map +1 -0
  87. package/build/exports.d.ts +2 -0
  88. package/build/implementations/http/astro/index.test.js +20 -12
  89. package/build/implementations/http/astro/index.test.js.map +1 -1
  90. package/build/implementations/http/doc-registry.js +1 -1
  91. package/build/implementations/http/doc-registry.js.map +1 -1
  92. package/build/implementations/http/doc-registry.test.js +36 -5
  93. package/build/implementations/http/doc-registry.test.js.map +1 -1
  94. package/build/implementations/http/error-dispatch.d.ts +76 -0
  95. package/build/implementations/http/error-dispatch.js +77 -0
  96. package/build/implementations/http/error-dispatch.js.map +1 -0
  97. package/build/implementations/http/error-dispatch.test.js +254 -0
  98. package/build/implementations/http/error-dispatch.test.js.map +1 -0
  99. package/build/implementations/http/error-taxonomy.d.ts +5 -5
  100. package/build/implementations/http/hono/docs/http-doc.d.ts +6 -0
  101. package/build/implementations/http/hono/docs/http-doc.js +42 -0
  102. package/build/implementations/http/hono/docs/http-doc.js.map +1 -0
  103. package/build/implementations/http/hono/docs/http-stream-doc.d.ts +6 -0
  104. package/build/implementations/http/hono/docs/http-stream-doc.js +40 -0
  105. package/build/implementations/http/hono/docs/http-stream-doc.js.map +1 -0
  106. package/build/implementations/http/hono/docs/rpc-doc.d.ts +6 -0
  107. package/build/implementations/http/hono/docs/rpc-doc.js +24 -0
  108. package/build/implementations/http/hono/docs/rpc-doc.js.map +1 -0
  109. package/build/implementations/http/hono/docs/stream-doc.d.ts +6 -0
  110. package/build/implementations/http/hono/docs/stream-doc.js +42 -0
  111. package/build/implementations/http/hono/docs/stream-doc.js.map +1 -0
  112. package/build/implementations/http/hono/handlers/http-stream.d.ts +10 -0
  113. package/build/implementations/http/hono/handlers/http-stream.js +123 -0
  114. package/build/implementations/http/hono/handlers/http-stream.js.map +1 -0
  115. package/build/implementations/http/hono/handlers/http-stream.test.js +128 -0
  116. package/build/implementations/http/hono/handlers/http-stream.test.js.map +1 -0
  117. package/build/implementations/http/hono/handlers/http.d.ts +10 -0
  118. package/build/implementations/http/hono/handlers/http.js +115 -0
  119. package/build/implementations/http/hono/handlers/http.js.map +1 -0
  120. package/build/implementations/http/hono/handlers/http.test.js +118 -0
  121. package/build/implementations/http/hono/handlers/http.test.js.map +1 -0
  122. package/build/implementations/http/hono/handlers/rpc.d.ts +11 -0
  123. package/build/implementations/http/hono/handlers/rpc.js +32 -0
  124. package/build/implementations/http/hono/handlers/rpc.js.map +1 -0
  125. package/build/implementations/http/hono/handlers/rpc.test.js +73 -0
  126. package/build/implementations/http/hono/handlers/rpc.test.js.map +1 -0
  127. package/build/implementations/http/hono/handlers/stream.d.ts +23 -0
  128. package/build/implementations/http/hono/handlers/stream.js +147 -0
  129. package/build/implementations/http/hono/handlers/stream.js.map +1 -0
  130. package/build/implementations/http/hono/handlers/stream.test.d.ts +1 -0
  131. package/build/implementations/http/hono/handlers/stream.test.js +177 -0
  132. package/build/implementations/http/hono/handlers/stream.test.js.map +1 -0
  133. package/build/implementations/http/hono/index.d.ts +57 -0
  134. package/build/implementations/http/hono/index.js +149 -0
  135. package/build/implementations/http/hono/index.js.map +1 -0
  136. package/build/implementations/http/hono/index.test.d.ts +1 -0
  137. package/build/implementations/http/hono/index.test.js +274 -0
  138. package/build/implementations/http/hono/index.test.js.map +1 -0
  139. package/build/implementations/http/hono/path.d.ts +17 -0
  140. package/build/implementations/http/hono/path.js +39 -0
  141. package/build/implementations/http/hono/path.js.map +1 -0
  142. package/build/implementations/http/hono/path.test.d.ts +1 -0
  143. package/build/implementations/http/hono/path.test.js +83 -0
  144. package/build/implementations/http/hono/path.test.js.map +1 -0
  145. package/build/implementations/http/hono/types.d.ts +51 -0
  146. package/build/implementations/http/hono/types.js.map +1 -0
  147. package/build/implementations/http/on-request-error.test.js +6 -96
  148. package/build/implementations/http/on-request-error.test.js.map +1 -1
  149. package/build/implementations/http/route-errors.test.js +11 -59
  150. package/build/implementations/http/route-errors.test.js.map +1 -1
  151. package/build/implementations/types.d.ts +43 -9
  152. package/build/index.d.ts +125 -115
  153. package/build/index.js +10 -222
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +30 -822
  156. package/build/index.test.js.map +1 -1
  157. package/build/migration.test.d.ts +1 -0
  158. package/build/migration.test.js +34 -0
  159. package/build/migration.test.js.map +1 -0
  160. package/build/schema/compute-schema.d.ts +11 -3
  161. package/build/schema/compute-schema.js +13 -7
  162. package/build/schema/compute-schema.js.map +1 -1
  163. package/build/schema/parser.d.ts +11 -3
  164. package/build/schema/parser.js +49 -9
  165. package/build/schema/parser.js.map +1 -1
  166. package/build/stack-utils.js +8 -0
  167. package/build/stack-utils.js.map +1 -1
  168. package/build/types.d.ts +142 -0
  169. package/build/types.js.map +1 -0
  170. package/docs/astro-adapter.md +5 -5
  171. package/docs/core.md +34 -17
  172. package/docs/http-integrations.md +83 -170
  173. package/docs/streaming.md +3 -60
  174. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +2 -7
  175. package/docs/superpowers/plans/2026-05-08-create-http.md +3355 -0
  176. package/docs/superpowers/plans/2026-05-08-hono-app-builder-convergence.md +3365 -0
  177. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +1 -3
  178. package/docs/superpowers/specs/2026-05-08-create-http-design.md +409 -0
  179. package/docs/superpowers/specs/2026-05-08-hono-app-builder-convergence-design.md +411 -0
  180. package/package.json +4 -22
  181. package/src/client/call.test.ts +26 -0
  182. package/src/client/call.ts +4 -1
  183. package/src/client/fetch-adapter.test.ts +14 -1
  184. package/src/client/fetch-adapter.ts +3 -1
  185. package/src/client/index.test.ts +7 -7
  186. package/src/client/request-builder.ts +2 -2
  187. package/src/client/stream.test.ts +39 -7
  188. package/src/client/stream.ts +16 -2
  189. package/src/client/typed-error-dispatch.test.ts +7 -97
  190. package/src/client/types.ts +21 -3
  191. package/src/codegen/__fixtures__/users-envelope.json +119 -38
  192. package/src/codegen/e2e.test.ts +98 -24
  193. package/src/codegen/emit-errors.integration.test.ts +1 -1
  194. package/src/codegen/emit-scope.test.ts +395 -110
  195. package/src/codegen/emit-scope.ts +350 -55
  196. package/src/codegen/pipeline.test.ts +7 -7
  197. package/src/codegen/resolve-envelope.test.ts +5 -5
  198. package/src/codegen/resolve-envelope.ts +1 -1
  199. package/src/codegen/targets/_shared/route-slots.test.ts +109 -26
  200. package/src/codegen/targets/_shared/route-slots.ts +48 -11
  201. package/src/codegen/targets/kotlin/__fixtures__/users-golden.kt +73 -0
  202. package/src/codegen/targets/kotlin/emit-route-kotlin.test.ts +100 -17
  203. package/src/codegen/targets/kotlin/emit-scope-kotlin.test.ts +9 -6
  204. package/src/codegen/targets/kotlin/integration.test.ts +19 -0
  205. package/src/codegen/targets/swift/__fixtures__/users-golden.swift +79 -0
  206. package/src/codegen/targets/swift/access-level.test.ts +8 -11
  207. package/src/codegen/targets/swift/emit-route-swift.test.ts +103 -20
  208. package/src/codegen/targets/swift/emit-scope-swift.test.ts +12 -9
  209. package/src/codegen/targets/swift/integration.test.ts +17 -0
  210. package/src/create-http-stream.test.ts +97 -0
  211. package/src/create-http-stream.ts +191 -0
  212. package/src/create-http.test.ts +163 -0
  213. package/src/create-http.ts +211 -0
  214. package/src/create-stream.test.ts +565 -0
  215. package/src/create-stream.ts +228 -0
  216. package/src/create.test.ts +658 -0
  217. package/src/create.ts +172 -0
  218. package/src/exports.ts +2 -0
  219. package/src/implementations/http/README.md +135 -95
  220. package/src/implementations/http/astro/README.md +4 -5
  221. package/src/implementations/http/astro/index.test.ts +25 -18
  222. package/src/implementations/http/doc-registry.test.ts +42 -5
  223. package/src/implementations/http/doc-registry.ts +1 -1
  224. package/src/implementations/http/error-dispatch.test.ts +283 -0
  225. package/src/implementations/http/error-dispatch.ts +176 -0
  226. package/src/implementations/http/error-taxonomy.ts +5 -5
  227. package/src/implementations/http/hono/docs/http-doc.ts +43 -0
  228. package/src/implementations/http/hono/docs/http-stream-doc.ts +44 -0
  229. package/src/implementations/http/hono/docs/rpc-doc.ts +34 -0
  230. package/src/implementations/http/hono/docs/stream-doc.ts +53 -0
  231. package/src/implementations/http/hono/handlers/http-stream.test.ts +150 -0
  232. package/src/implementations/http/hono/handlers/http-stream.ts +152 -0
  233. package/src/implementations/http/hono/handlers/http.test.ts +130 -0
  234. package/src/implementations/http/hono/handlers/http.ts +147 -0
  235. package/src/implementations/http/hono/handlers/rpc.test.ts +81 -0
  236. package/src/implementations/http/hono/handlers/rpc.ts +54 -0
  237. package/src/implementations/http/hono/handlers/stream.test.ts +198 -0
  238. package/src/implementations/http/hono/handlers/stream.ts +208 -0
  239. package/src/implementations/http/hono/index.test.ts +329 -0
  240. package/src/implementations/http/hono/index.ts +204 -0
  241. package/src/implementations/http/hono/path.test.ts +96 -0
  242. package/src/implementations/http/hono/path.ts +59 -0
  243. package/src/implementations/http/hono/types.ts +93 -0
  244. package/src/implementations/http/on-request-error.test.ts +10 -116
  245. package/src/implementations/http/route-errors.test.ts +11 -77
  246. package/src/implementations/types.ts +44 -9
  247. package/src/index.test.ts +35 -1091
  248. package/src/index.ts +50 -474
  249. package/src/migration.test.ts +48 -0
  250. package/src/schema/compute-schema.ts +26 -12
  251. package/src/schema/parser.ts +62 -12
  252. package/src/stack-utils.ts +8 -0
  253. package/src/types.ts +133 -0
  254. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/express-rpc.md +0 -137
  255. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-api.md +0 -173
  256. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.md +0 -142
  257. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-stream.md +0 -147
  258. package/build/implementations/http/express-rpc/error-taxonomy.test.js +0 -83
  259. package/build/implementations/http/express-rpc/error-taxonomy.test.js.map +0 -1
  260. package/build/implementations/http/express-rpc/index.d.ts +0 -125
  261. package/build/implementations/http/express-rpc/index.js +0 -216
  262. package/build/implementations/http/express-rpc/index.js.map +0 -1
  263. package/build/implementations/http/express-rpc/index.test.js +0 -684
  264. package/build/implementations/http/express-rpc/index.test.js.map +0 -1
  265. package/build/implementations/http/express-rpc/types.d.ts +0 -11
  266. package/build/implementations/http/express-rpc/types.js.map +0 -1
  267. package/build/implementations/http/hono-api/error-taxonomy.test.js +0 -137
  268. package/build/implementations/http/hono-api/error-taxonomy.test.js.map +0 -1
  269. package/build/implementations/http/hono-api/index.d.ts +0 -151
  270. package/build/implementations/http/hono-api/index.js +0 -344
  271. package/build/implementations/http/hono-api/index.js.map +0 -1
  272. package/build/implementations/http/hono-api/index.test.js +0 -992
  273. package/build/implementations/http/hono-api/index.test.js.map +0 -1
  274. package/build/implementations/http/hono-api/types.d.ts +0 -13
  275. package/build/implementations/http/hono-api/types.js.map +0 -1
  276. package/build/implementations/http/hono-rpc/error-taxonomy.test.js +0 -64
  277. package/build/implementations/http/hono-rpc/error-taxonomy.test.js.map +0 -1
  278. package/build/implementations/http/hono-rpc/index.d.ts +0 -130
  279. package/build/implementations/http/hono-rpc/index.js +0 -209
  280. package/build/implementations/http/hono-rpc/index.js.map +0 -1
  281. package/build/implementations/http/hono-rpc/index.test.js +0 -828
  282. package/build/implementations/http/hono-rpc/index.test.js.map +0 -1
  283. package/build/implementations/http/hono-rpc/types.d.ts +0 -11
  284. package/build/implementations/http/hono-rpc/types.js +0 -2
  285. package/build/implementations/http/hono-rpc/types.js.map +0 -1
  286. package/build/implementations/http/hono-stream/error-taxonomy.test.js +0 -159
  287. package/build/implementations/http/hono-stream/error-taxonomy.test.js.map +0 -1
  288. package/build/implementations/http/hono-stream/index.d.ts +0 -171
  289. package/build/implementations/http/hono-stream/index.js +0 -415
  290. package/build/implementations/http/hono-stream/index.js.map +0 -1
  291. package/build/implementations/http/hono-stream/index.test.js +0 -1383
  292. package/build/implementations/http/hono-stream/index.test.js.map +0 -1
  293. package/build/implementations/http/hono-stream/types.d.ts +0 -15
  294. package/build/implementations/http/hono-stream/types.js +0 -2
  295. package/build/implementations/http/hono-stream/types.js.map +0 -1
  296. package/src/implementations/http/express-rpc/README.md +0 -280
  297. package/src/implementations/http/express-rpc/error-taxonomy.test.ts +0 -103
  298. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  299. package/src/implementations/http/express-rpc/index.ts +0 -327
  300. package/src/implementations/http/express-rpc/types.ts +0 -16
  301. package/src/implementations/http/hono-api/README.md +0 -284
  302. package/src/implementations/http/hono-api/error-taxonomy.test.ts +0 -179
  303. package/src/implementations/http/hono-api/index.test.ts +0 -1341
  304. package/src/implementations/http/hono-api/index.ts +0 -519
  305. package/src/implementations/http/hono-api/types.ts +0 -16
  306. package/src/implementations/http/hono-rpc/README.md +0 -357
  307. package/src/implementations/http/hono-rpc/error-taxonomy.test.ts +0 -82
  308. package/src/implementations/http/hono-rpc/index.test.ts +0 -1107
  309. package/src/implementations/http/hono-rpc/index.ts +0 -320
  310. package/src/implementations/http/hono-rpc/types.ts +0 -16
  311. package/src/implementations/http/hono-stream/README.md +0 -559
  312. package/src/implementations/http/hono-stream/error-taxonomy.test.ts +0 -178
  313. package/src/implementations/http/hono-stream/index.test.ts +0 -1804
  314. package/src/implementations/http/hono-stream/index.ts +0 -622
  315. package/src/implementations/http/hono-stream/types.ts +0 -20
  316. /package/build/{implementations/http/express-rpc/error-taxonomy.test.d.ts → create-http-stream.test.d.ts} +0 -0
  317. /package/build/{implementations/http/express-rpc/index.test.d.ts → create-http.test.d.ts} +0 -0
  318. /package/build/{implementations/http/hono-api/error-taxonomy.test.d.ts → create-stream.test.d.ts} +0 -0
  319. /package/build/{implementations/http/hono-api/index.test.d.ts → create.test.d.ts} +0 -0
  320. /package/build/implementations/http/{hono-rpc/error-taxonomy.test.d.ts → error-dispatch.test.d.ts} +0 -0
  321. /package/build/implementations/http/{hono-rpc/index.test.d.ts → hono/handlers/http-stream.test.d.ts} +0 -0
  322. /package/build/implementations/http/{hono-stream/error-taxonomy.test.d.ts → hono/handlers/http.test.d.ts} +0 -0
  323. /package/build/implementations/http/{hono-stream/index.test.d.ts → hono/handlers/rpc.test.d.ts} +0 -0
  324. /package/build/implementations/http/{express-rpc → hono}/types.js +0 -0
  325. /package/build/{implementations/http/hono-api/types.js → types.js} +0 -0
@@ -0,0 +1,43 @@
1
+ import type { THttpProcedureRegistration } from '../../../../types.js'
2
+ import type { APIHttpRouteDoc } from '../../../types.js'
3
+ import { resolveFullPath } from '../path.js'
4
+
5
+ export function buildHttpRouteDoc(
6
+ procedure: THttpProcedureRegistration<any>,
7
+ prefix: string | undefined,
8
+ extend?: (params: { base: APIHttpRouteDoc; procedure: THttpProcedureRegistration<any> }) => Record<string, unknown>,
9
+ ): APIHttpRouteDoc {
10
+ const config = procedure.config
11
+ const fullPath = resolveFullPath(config.path, prefix)
12
+ const reqSchema = config.schema?.req
13
+ const resSchema = config.schema?.res
14
+ const jsonSchema: APIHttpRouteDoc['jsonSchema'] = {}
15
+
16
+ if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
17
+ jsonSchema.req = {}
18
+ if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
19
+ if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
20
+ if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
21
+ if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
22
+ }
23
+ if (resSchema && (resSchema.body || resSchema.headers)) {
24
+ jsonSchema.res = {}
25
+ if (resSchema.body) jsonSchema.res.body = resSchema.body as any
26
+ if (resSchema.headers) jsonSchema.res.headers = resSchema.headers as any
27
+ }
28
+
29
+ const base: APIHttpRouteDoc = {
30
+ kind: 'api',
31
+ name: procedure.name,
32
+ scope: config.scope,
33
+ path: config.path,
34
+ method: config.method,
35
+ fullPath,
36
+ jsonSchema,
37
+ }
38
+ if (config.successStatus) base.successStatus = config.successStatus
39
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
40
+
41
+ const extended = extend ? extend({ base, procedure }) : {}
42
+ return { ...extended, ...base }
43
+ }
@@ -0,0 +1,44 @@
1
+ import type { THttpStreamProcedureRegistration } from '../../../../types.js'
2
+ import type { HttpStreamRouteDoc } from '../../../types.js'
3
+ import { resolveFullPath } from '../path.js'
4
+
5
+ export function buildHttpStreamRouteDoc(
6
+ procedure: THttpStreamProcedureRegistration<any>,
7
+ prefix: string | undefined,
8
+ extend?: (params: {
9
+ base: HttpStreamRouteDoc
10
+ procedure: THttpStreamProcedureRegistration<any>
11
+ }) => Record<string, unknown>,
12
+ ): HttpStreamRouteDoc {
13
+ const config = procedure.config
14
+ const fullPath = resolveFullPath(config.path, prefix)
15
+ const reqSchema = config.schema?.req
16
+ const resSchema = config.schema?.res
17
+ const jsonSchema: HttpStreamRouteDoc['jsonSchema'] = {}
18
+
19
+ if (reqSchema && (reqSchema.pathParams || reqSchema.query || reqSchema.body || reqSchema.headers)) {
20
+ jsonSchema.req = {}
21
+ if (reqSchema.pathParams) jsonSchema.req.pathParams = reqSchema.pathParams as any
22
+ if (reqSchema.query) jsonSchema.req.query = reqSchema.query as any
23
+ if (reqSchema.body) jsonSchema.req.body = reqSchema.body as any
24
+ if (reqSchema.headers) jsonSchema.req.headers = reqSchema.headers as any
25
+ }
26
+ if (resSchema?.headers) jsonSchema.res = { headers: resSchema.headers as any }
27
+ if (config.schema?.yield) jsonSchema.yield = config.schema.yield as any
28
+ if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType as any
29
+
30
+ const base: HttpStreamRouteDoc = {
31
+ kind: 'http-stream',
32
+ name: procedure.name,
33
+ scope: config.scope,
34
+ path: config.path,
35
+ method: config.method,
36
+ fullPath,
37
+ streamMode: 'sse',
38
+ jsonSchema,
39
+ }
40
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
41
+
42
+ const extended = extend ? extend({ base, procedure }) : {}
43
+ return { ...extended, ...base }
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { TProcedureRegistration } from '../../../../types.js'
2
+ import type { RPCConfig, RPCHttpRouteDoc } from '../../../types.js'
3
+ import { makeRoutePath } from '../path.js'
4
+
5
+ export function buildRpcRouteDoc(
6
+ procedure: TProcedureRegistration<any, RPCConfig>,
7
+ prefix: string | undefined,
8
+ extend?: (params: { base: RPCHttpRouteDoc; procedure: TProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
9
+ ): RPCHttpRouteDoc {
10
+ const config = procedure.config as RPCConfig & {
11
+ schema?: { params?: Record<string, unknown>; returnType?: Record<string, unknown> }
12
+ errors?: string[]
13
+ }
14
+
15
+ const jsonSchema: { body?: Record<string, unknown>; response?: Record<string, unknown> } = {}
16
+ if (config.schema?.params) jsonSchema.body = config.schema.params
17
+ if (config.schema?.returnType) jsonSchema.response = config.schema.returnType
18
+
19
+ const base: RPCHttpRouteDoc = {
20
+ kind: 'rpc',
21
+ name: procedure.name,
22
+ version: config.version,
23
+ scope: config.scope,
24
+ path: makeRoutePath({ procedure, prefix }),
25
+ method: 'post',
26
+ jsonSchema,
27
+ }
28
+ if (config.errors && config.errors.length > 0) {
29
+ base.errors = [...config.errors]
30
+ }
31
+
32
+ const extended = extend ? extend({ base, procedure }) : {}
33
+ return { ...extended, ...base }
34
+ }
@@ -0,0 +1,53 @@
1
+ import type { TStreamProcedureRegistration } from '../../../../types.js'
2
+ import type { RPCConfig, StreamHttpRouteDoc, StreamMode } from '../../../types.js'
3
+ import { makeRoutePath } from '../path.js'
4
+
5
+ export function buildStreamRouteDoc(
6
+ procedure: TStreamProcedureRegistration<any, RPCConfig>,
7
+ streamMode: StreamMode,
8
+ prefix: string | undefined,
9
+ extend?: (params: { base: StreamHttpRouteDoc; procedure: TStreamProcedureRegistration<any, RPCConfig> }) => Record<string, unknown>,
10
+ ): StreamHttpRouteDoc {
11
+ const config = procedure.config as RPCConfig & {
12
+ schema?: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> }
13
+ errors?: string[]
14
+ }
15
+
16
+ const jsonSchema: StreamHttpRouteDoc['jsonSchema'] = {}
17
+ if (config.schema?.params) jsonSchema.params = config.schema.params
18
+
19
+ if (streamMode === 'sse') {
20
+ jsonSchema.yieldType = {
21
+ type: 'object',
22
+ description: 'SSE message envelope. The data field contains the procedure yield value.',
23
+ required: ['data', 'event', 'id'],
24
+ properties: {
25
+ data: config.schema?.yieldType ?? {},
26
+ event: { type: 'string' },
27
+ id: { type: 'string' },
28
+ retry: { type: 'number' },
29
+ },
30
+ }
31
+ } else if (config.schema?.yieldType) {
32
+ jsonSchema.yieldType = config.schema.yieldType
33
+ }
34
+
35
+ if (config.schema?.returnType) jsonSchema.returnType = config.schema.returnType
36
+
37
+ // POST first so codegen (which reads `methods[0]`) defaults to POST. POST is
38
+ // the canonical method for streams because it can carry a body for params.
39
+ const base: StreamHttpRouteDoc = {
40
+ kind: 'stream',
41
+ name: procedure.name,
42
+ version: config.version,
43
+ scope: config.scope,
44
+ path: makeRoutePath({ procedure, prefix }),
45
+ methods: ['post', 'get'],
46
+ streamMode,
47
+ jsonSchema,
48
+ }
49
+ if (config.errors && config.errors.length > 0) base.errors = [...config.errors]
50
+
51
+ const extended = extend ? extend({ base, procedure }) : {}
52
+ return { ...extended, ...base }
53
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { Hono } from 'hono'
3
+ import { Type } from 'typebox'
4
+ import { Procedures } from '../../../../index.js'
5
+ import { installHttpStreamRoute } from './http-stream.js'
6
+
7
+ function buildApp(setup: (P: ReturnType<typeof Procedures<{ uid: string }>>) => void, cfg: any = {}) {
8
+ const P = Procedures<{ uid: string }>()
9
+ setup(P)
10
+ const app = new Hono()
11
+ const docs: any[] = []
12
+ for (const proc of P.getProcedures().values()) {
13
+ if (proc.kind !== 'http-stream') continue
14
+ installHttpStreamRoute({
15
+ app,
16
+ procedure: proc as any,
17
+ factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
18
+ cfg,
19
+ docs,
20
+ })
21
+ }
22
+ return { app, docs }
23
+ }
24
+
25
+ describe('installHttpStreamRoute', () => {
26
+ test('SSE pumping with initialHeaders and event:return', async () => {
27
+ const { app, docs } = buildApp((P) => {
28
+ P.CreateHttpStream('Tail', {
29
+ path: '/logs/tail', method: 'get',
30
+ schema: {
31
+ yield: Type.Object({ line: Type.String() }),
32
+ res: { headers: Type.Object({ 'x-trace': Type.String() }) },
33
+ },
34
+ }, async () => ({
35
+ headers: { 'x-trace': 'abc' },
36
+ stream: (async function* () {
37
+ yield { line: 'a' }
38
+ yield { line: 'b' }
39
+ return 'fin'
40
+ })(),
41
+ }))
42
+ })
43
+ expect(docs[0].kind).toBe('http-stream')
44
+
45
+ const res = await app.request('/logs/tail')
46
+ const text = await res.text()
47
+ expect(res.headers.get('x-trace')).toBe('abc')
48
+ expect(text).toContain('"line":"a"')
49
+ expect(text).toContain('"line":"b"')
50
+ expect(text).toContain('event: return')
51
+ expect(text).toContain('fin')
52
+ })
53
+
54
+ test('mid-stream throw → dispatchMidStreamError default { error }', async () => {
55
+ const { app } = buildApp((P) => {
56
+ P.CreateHttpStream('Boom', { path: '/boom', method: 'get', schema: {} },
57
+ (async function* () {
58
+ yield { ok: 1 }
59
+ throw new Error('mid')
60
+ }) as any
61
+ )
62
+ })
63
+ const res = await app.request('/boom')
64
+ const text = await res.text()
65
+ expect(text).toContain('event: error')
66
+ expect(text).toContain('"error":"mid"')
67
+ })
68
+
69
+ test('pre-stream throw → dispatchPreStreamError 500', async () => {
70
+ const { app } = buildApp((P) => {
71
+ P.CreateHttpStream('Pre', { path: '/pre', method: 'get', schema: {} },
72
+ (async () => { throw new Error('pre') }) as any)
73
+ })
74
+ const res = await app.request('/pre')
75
+ expect(res.status).toBe(500)
76
+ const body = await res.json()
77
+ expect(body.error).toContain('pre')
78
+ })
79
+
80
+ test('validateYields: true → invalid yield surfaces ProcedureYieldValidationError on the wire', async () => {
81
+ const { app } = buildApp((P) => {
82
+ P.CreateHttpStream(
83
+ 'BadHttpYield',
84
+ {
85
+ path: '/bad-yield',
86
+ method: 'get',
87
+ schema: { yield: Type.Object({ id: Type.Number() }) },
88
+ validateYields: true,
89
+ },
90
+ (async function* () {
91
+ yield { id: 1 }
92
+ yield { id: 'not-a-number' } as any
93
+ }) as any,
94
+ )
95
+ })
96
+
97
+ const res = await app.request('/bad-yield')
98
+ expect(res.status).toBe(200)
99
+ const text = await res.text()
100
+ expect(text).toContain('event: error')
101
+ expect(text).toContain('"name":"ProcedureYieldValidationError"')
102
+ expect(text).toContain('"procedureName":"BadHttpYield"')
103
+ expect(text).toContain('"id":1')
104
+ })
105
+
106
+ test('AbortSignal: natural completion → ctx.signal.reason === "stream-completed"', async () => {
107
+ let capturedSignal: AbortSignal | undefined
108
+ const { app } = buildApp((P) => {
109
+ P.CreateHttpStream('NaturalSig', { path: '/sig-natural', method: 'get', schema: {} },
110
+ (async function* (ctx: any) {
111
+ capturedSignal = ctx.signal
112
+ yield { tick: 1 }
113
+ }) as any,
114
+ )
115
+ })
116
+
117
+ const res = await app.request('/sig-natural')
118
+ await res.text()
119
+
120
+ expect(capturedSignal).toBeInstanceOf(AbortSignal)
121
+ expect(capturedSignal!.aborted).toBe(true)
122
+ expect(capturedSignal!.reason).toBe('stream-completed')
123
+ })
124
+
125
+ test('AbortSignal: pre-aborted request signal → ctx.signal.reason !== "stream-completed"', async () => {
126
+ // Same proxy approach as the rpc-stream variant: pre-abort the request
127
+ // signal and rely on `AbortSignal.any` preserving the earlier reason over
128
+ // the internal 'stream-completed' that fires in the wrappedHandler's
129
+ // finally clause.
130
+ let capturedSignal: AbortSignal | undefined
131
+ const { app } = buildApp((P) => {
132
+ P.CreateHttpStream('DisconnectSig', { path: '/sig-disconnect', method: 'get', schema: {} },
133
+ (async function* (ctx: any) {
134
+ capturedSignal = ctx.signal
135
+ yield { tick: 1 }
136
+ }) as any,
137
+ )
138
+ })
139
+
140
+ const ac = new AbortController()
141
+ ac.abort('client-gone')
142
+ const req = new Request('http://localhost/sig-disconnect', { method: 'GET', signal: ac.signal })
143
+ const res = await app.request(req)
144
+ await res.text().catch(() => '')
145
+
146
+ expect(capturedSignal).toBeInstanceOf(AbortSignal)
147
+ expect(capturedSignal!.aborted).toBe(true)
148
+ expect(capturedSignal!.reason).not.toBe('stream-completed')
149
+ })
150
+ })
@@ -0,0 +1,152 @@
1
+ import type { Context, Hono } from 'hono'
2
+ import { streamSSE } from 'hono/streaming'
3
+ import type { THttpStreamProcedureRegistration } from '../../../../types.js'
4
+ import type { HttpMethod } from '../../../types.js'
5
+ import { dispatchMidStreamError, dispatchPreStreamError } from '../../error-dispatch.js'
6
+ import { buildHttpStreamRouteDoc } from '../docs/http-stream-doc.js'
7
+ import type { DocAccumulator, HonoAppBuilderConfig, HonoFactoryItem, QueryParser } from '../types.js'
8
+
9
+ const BODY_METHODS: HttpMethod[] = ['post', 'put', 'patch']
10
+
11
+ function parseQueryNative(queryString: string): Record<string, unknown> {
12
+ const sp = new URLSearchParams(queryString)
13
+ const result: Record<string, unknown> = {}
14
+ for (const key of new Set(sp.keys())) {
15
+ const values = sp.getAll(key)
16
+ result[key] = values.length > 1 ? values : values[0]
17
+ }
18
+ return result
19
+ }
20
+
21
+ function extractQuery(url: string, parser: QueryParser): Record<string, unknown> {
22
+ const q = url.indexOf('?')
23
+ if (q === -1) return {}
24
+ const raw = url.slice(q + 1)
25
+ return raw ? parser(raw) : {}
26
+ }
27
+
28
+ async function extractParams(
29
+ c: Context,
30
+ method: HttpMethod,
31
+ reqSchema: Record<string, unknown>,
32
+ parser: QueryParser,
33
+ ): Promise<Record<string, unknown>> {
34
+ const params: Record<string, unknown> = {}
35
+ for (const channel of Object.keys(reqSchema)) {
36
+ switch (channel) {
37
+ case 'pathParams':
38
+ params.pathParams = c.req.param()
39
+ break
40
+ case 'query':
41
+ params.query = extractQuery(c.req.url, parser)
42
+ break
43
+ case 'body':
44
+ if (BODY_METHODS.includes(method)) {
45
+ params.body = await c.req.json().catch(() => ({}))
46
+ }
47
+ break
48
+ case 'headers': {
49
+ const obj: Record<string, string> = {}
50
+ c.req.raw.headers.forEach((v, k) => { obj[k] = v })
51
+ params.headers = obj
52
+ break
53
+ }
54
+ default:
55
+ params[channel] = undefined
56
+ }
57
+ }
58
+ return params
59
+ }
60
+
61
+ export function installHttpStreamRoute(params: {
62
+ app: Hono
63
+ procedure: THttpStreamProcedureRegistration<any>
64
+ factoryItem: HonoFactoryItem
65
+ cfg: HonoAppBuilderConfig
66
+ docs: DocAccumulator
67
+ }): void {
68
+ const { app, procedure, factoryItem, cfg, docs } = params
69
+ const queryParser = cfg.api?.queryParser ?? parseQueryNative
70
+
71
+ const route = buildHttpStreamRouteDoc(
72
+ procedure,
73
+ cfg.pathPrefix,
74
+ factoryItem.extendProcedureDoc as any,
75
+ )
76
+ docs.push(route)
77
+
78
+ const reqSchema = procedure.config.schema?.req
79
+
80
+ app.on(procedure.config.method.toUpperCase(), route.fullPath, async (c: Context) => {
81
+ try {
82
+ const context =
83
+ typeof factoryItem.factoryContext === 'function'
84
+ ? await (factoryItem.factoryContext as (c: Context) => any)(c)
85
+ : factoryItem.factoryContext
86
+
87
+ const reqParams = reqSchema
88
+ ? await extractParams(c, procedure.config.method, reqSchema as Record<string, unknown>, queryParser)
89
+ : undefined
90
+
91
+ const { stream: gen, initialHeaders } = await procedure.handler(
92
+ { ...context, signal: c.req.raw.signal },
93
+ reqParams,
94
+ )
95
+
96
+ if (initialHeaders) {
97
+ for (const [k, v] of Object.entries(initialHeaders)) c.header(k, v)
98
+ }
99
+
100
+ cfg.stream?.onStreamStart?.(procedure, c, 'sse')
101
+
102
+ return streamSSE(c, async (stream) => {
103
+ let eventId = 0
104
+ try {
105
+ let it = await gen.next()
106
+ while (!it.done) {
107
+ const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
108
+ await stream.writeSSE({ data, event: procedure.name, id: String(eventId++) })
109
+ it = await gen.next()
110
+ }
111
+ if (it.value !== undefined) {
112
+ const data = typeof it.value === 'string' ? it.value : JSON.stringify(it.value)
113
+ await stream.writeSSE({ data, event: 'return', id: String(eventId++) })
114
+ }
115
+ } catch (error) {
116
+ const dispatched = await dispatchMidStreamError({
117
+ err: error,
118
+ procedure,
119
+ raw: c,
120
+ cfg: {
121
+ errors: cfg.errors,
122
+ unknownError: cfg.unknownError,
123
+ onMidStreamError: cfg.stream?.onMidStreamError,
124
+ onRequestError: cfg.onRequestError,
125
+ },
126
+ })
127
+ await stream.writeSSE({
128
+ data: typeof dispatched.data === 'string' ? dispatched.data : JSON.stringify(dispatched.data),
129
+ event: dispatched.sseEvent ?? 'error',
130
+ id: String(eventId++),
131
+ })
132
+ if (dispatched.runOnCatch) await dispatched.runOnCatch()
133
+ } finally {
134
+ cfg.stream?.onStreamEnd?.(procedure, c, 'sse')
135
+ cfg.onRequestEnd?.(c)
136
+ }
137
+ })
138
+ } catch (error) {
139
+ return dispatchPreStreamError({
140
+ err: error,
141
+ procedure,
142
+ raw: c,
143
+ cfg: {
144
+ errors: cfg.errors,
145
+ unknownError: cfg.unknownError,
146
+ onError: cfg.onError,
147
+ onRequestError: cfg.onRequestError,
148
+ },
149
+ })
150
+ }
151
+ })
152
+ }
@@ -0,0 +1,130 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import { Hono } from 'hono'
3
+ import { Type } from 'typebox'
4
+ import { Procedures } from '../../../../index.js'
5
+ import { installHttpRoute } from './http.js'
6
+
7
+ function buildApp(setup: (P: ReturnType<typeof Procedures<{ uid: string }>>) => void, cfg: any = {}) {
8
+ const P = Procedures<{ uid: string }>()
9
+ setup(P)
10
+ const app = new Hono()
11
+ const docs: any[] = []
12
+ for (const proc of P.getProcedures().values()) {
13
+ if (proc.kind !== 'http') continue
14
+ installHttpRoute({
15
+ app,
16
+ procedure: proc as any,
17
+ factoryItem: { factory: P, factoryContext: () => ({ uid: 'u1' }) },
18
+ cfg,
19
+ docs,
20
+ })
21
+ }
22
+ return { app, docs }
23
+ }
24
+
25
+ describe('installHttpRoute', () => {
26
+ test('GET with pathParams and query', async () => {
27
+ const { app, docs } = buildApp((P) => {
28
+ P.CreateHttp('GetUser', {
29
+ path: '/users/:id',
30
+ method: 'get',
31
+ scope: 'users',
32
+ schema: {
33
+ req: {
34
+ pathParams: Type.Object({ id: Type.String() }),
35
+ query: Type.Object({ include: Type.Optional(Type.String()) }),
36
+ },
37
+ res: { body: Type.Object({ id: Type.String(), include: Type.Optional(Type.String()) }) },
38
+ },
39
+ }, async (_ctx, { pathParams, query }) => ({ id: pathParams.id, include: query.include }))
40
+ })
41
+
42
+ const res = await app.request('/users/abc?include=foo')
43
+ expect(res.status).toBe(200)
44
+ expect(await res.json()).toEqual({ id: 'abc', include: 'foo' })
45
+ expect(docs[0].kind).toBe('api')
46
+ expect(docs[0].fullPath).toBe('/users/:id')
47
+ })
48
+
49
+ test('POST with body, default 201', async () => {
50
+ const { app } = buildApp((P) => {
51
+ P.CreateHttp('Create', {
52
+ path: '/items',
53
+ method: 'post',
54
+ schema: {
55
+ req: { body: Type.Object({ name: Type.String() }) },
56
+ res: { body: Type.Object({ name: Type.String() }) },
57
+ },
58
+ }, async (_ctx, { body }) => body)
59
+ })
60
+
61
+ const res = await app.request('/items', {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ name: 'gizmo' }),
65
+ })
66
+ expect(res.status).toBe(201)
67
+ expect(await res.json()).toEqual({ name: 'gizmo' })
68
+ })
69
+
70
+ test('DELETE returns 204 with no body', async () => {
71
+ const { app } = buildApp((P) => {
72
+ P.CreateHttp('Delete', {
73
+ path: '/items/:id',
74
+ method: 'delete',
75
+ schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
76
+ }, async () => undefined)
77
+ })
78
+
79
+ const res = await app.request('/items/x', { method: 'DELETE' })
80
+ expect(res.status).toBe(204)
81
+ expect(await res.text()).toBe('')
82
+ })
83
+
84
+ test('headers in result are forwarded', async () => {
85
+ const { app } = buildApp((P) => {
86
+ P.CreateHttp('WithHeaders', {
87
+ path: '/h', method: 'get',
88
+ schema: { res: { body: Type.Object({ ok: Type.Boolean() }), headers: Type.Object({}) } },
89
+ }, async () => ({ body: { ok: true }, headers: { 'x-trace': 'abc' } }))
90
+ })
91
+ const res = await app.request('/h')
92
+ expect(res.headers.get('x-trace')).toBe('abc')
93
+ expect(await res.json()).toEqual({ ok: true })
94
+ })
95
+
96
+ test('api.onSuccess fires after handler', async () => {
97
+ const onSuccess = vi.fn()
98
+ const { app } = buildApp((P) => {
99
+ P.CreateHttp('Ping', { path: '/ping', method: 'get', schema: {} }, async () => undefined)
100
+ }, { api: { onSuccess } })
101
+
102
+ await app.request('/ping')
103
+ expect(onSuccess).toHaveBeenCalledOnce()
104
+ })
105
+
106
+ test('thrown error → dispatchPreStreamError default 500', async () => {
107
+ const { app } = buildApp((P) => {
108
+ P.CreateHttp('Boom', { path: '/boom', method: 'get', schema: {} }, async () => { throw new Error('x') })
109
+ })
110
+ const res = await app.request('/boom')
111
+ expect(res.status).toBe(500)
112
+ const body = await res.json()
113
+ expect(body.error).toContain('x')
114
+ })
115
+
116
+ test('custom queryParser is used', async () => {
117
+ const queryParser = vi.fn((q: string) => ({ parsed: true, raw: q }))
118
+ const { app } = buildApp((P) => {
119
+ P.CreateHttp('Q', {
120
+ path: '/q', method: 'get',
121
+ schema: { req: { query: Type.Any() }, res: { body: Type.Any() } },
122
+ },
123
+ async (_ctx, { query }) => ({ body: query }))
124
+ }, { api: { queryParser } })
125
+
126
+ const res = await app.request('/q?a=1&b=2')
127
+ expect(queryParser).toHaveBeenCalledWith('a=1&b=2')
128
+ expect(await res.json()).toEqual({ parsed: true, raw: 'a=1&b=2' })
129
+ })
130
+ })