ts-procedures 7.3.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 +104 -53
  5. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +205 -232
  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 +34 -48
  12. package/agent_config/cursor/cursorrules +34 -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 +124 -124
  153. package/build/index.js +10 -221
  154. package/build/index.js.map +1 -1
  155. package/build/index.test.js +20 -919
  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 +15 -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 +22 -1249
  248. package/src/index.ts +49 -485
  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
@@ -1,1107 +0,0 @@
1
- import { describe, expect, test, vi, beforeEach } from 'vitest'
2
- import { Hono } from 'hono'
3
- import { v } from 'suretype'
4
- import { Procedures } from '../../../index.js'
5
- import { HonoRPCAppBuilder } from './index.js'
6
- import { RPCConfig } from '../../types.js'
7
-
8
- /**
9
- * HonoRPCAppBuilder Test Suite
10
- *
11
- * Tests the RPC-style Hono integration for ts-procedures.
12
- * This builder creates POST routes at `/{name}/{version}` paths (with optional pathPrefix).
13
- */
14
- describe('HonoRPCAppBuilder', () => {
15
- // --------------------------------------------------------------------------
16
- // Constructor Tests
17
- // --------------------------------------------------------------------------
18
- describe('constructor', () => {
19
- test('creates default Hono app', async () => {
20
- const builder = new HonoRPCAppBuilder()
21
- const RPC = Procedures<{ userId: string }, RPCConfig>()
22
-
23
- RPC.Create(
24
- 'Echo',
25
- { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
26
- async (ctx, params) => params
27
- )
28
-
29
- builder.register(RPC, () => ({ userId: '123' }))
30
- const app = builder.build()
31
-
32
- // Hono has built-in JSON parsing via c.req.json()
33
- const res = await app.request('/echo/echo/1', {
34
- method: 'POST',
35
- headers: { 'Content-Type': 'application/json' },
36
- body: JSON.stringify({ message: 'hello' }),
37
- })
38
-
39
- expect(res.status).toBe(200)
40
- const body = await res.json()
41
- expect(body).toEqual({ message: 'hello' })
42
- })
43
-
44
- test('uses provided Hono app', async () => {
45
- const customApp = new Hono()
46
- // Add a custom route to verify it's the same app
47
- customApp.get('/custom', (c) => c.json({ custom: true }))
48
-
49
- const builder = new HonoRPCAppBuilder({ app: customApp })
50
- const RPC = Procedures<{ userId: string }, RPCConfig>()
51
-
52
- RPC.Create(
53
- 'Echo',
54
- { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
55
- async (ctx, params) => ({ received: params })
56
- )
57
-
58
- builder.register(RPC, () => ({ userId: '123' }))
59
- const app = builder.build()
60
-
61
- // Custom route should still work
62
- const customRes = await app.request('/custom')
63
- expect(customRes.status).toBe(200)
64
- const customBody = await customRes.json()
65
- expect(customBody).toEqual({ custom: true })
66
-
67
- // RPC route should also work
68
- const rpcRes = await app.request('/echo/echo/1', {
69
- method: 'POST',
70
- headers: { 'Content-Type': 'application/json' },
71
- body: JSON.stringify({ message: 'hello' }),
72
- })
73
- expect(rpcRes.status).toBe(200)
74
- })
75
-
76
- test('handles empty config', () => {
77
- const builder = new HonoRPCAppBuilder({})
78
- expect(builder.app).toBeDefined()
79
- expect(builder.docs).toEqual([])
80
- })
81
-
82
- test('handles undefined config', () => {
83
- const builder = new HonoRPCAppBuilder(undefined)
84
- expect(builder.app).toBeDefined()
85
- expect(builder.docs).toEqual([])
86
- })
87
- })
88
-
89
- // --------------------------------------------------------------------------
90
- // pathPrefix Option Tests
91
- // --------------------------------------------------------------------------
92
- describe('pathPrefix option', () => {
93
- test('uses custom pathPrefix for all routes', async () => {
94
- const builder = new HonoRPCAppBuilder({ pathPrefix: '/api/v1' })
95
- const RPC = Procedures<{}, RPCConfig>()
96
-
97
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
98
-
99
- builder.register(RPC, () => ({}))
100
- const app = builder.build()
101
-
102
- const res = await app.request('/api/v1/test/test/1', {
103
- method: 'POST',
104
- headers: { 'Content-Type': 'application/json' },
105
- body: JSON.stringify({}),
106
- })
107
- expect(res.status).toBe(200)
108
- const body = await res.json()
109
- expect(body).toEqual({ ok: true })
110
- })
111
-
112
- test('pathPrefix without leading slash gets normalized', async () => {
113
- const builder = new HonoRPCAppBuilder({ pathPrefix: 'custom' })
114
- const RPC = Procedures<{}, RPCConfig>()
115
-
116
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
117
-
118
- builder.register(RPC, () => ({}))
119
- const app = builder.build()
120
-
121
- const res = await app.request('/custom/test/test/1', {
122
- method: 'POST',
123
- headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify({}),
125
- })
126
- expect(res.status).toBe(200)
127
- })
128
-
129
- test('no prefix when pathPrefix not specified', async () => {
130
- const builder = new HonoRPCAppBuilder()
131
- const RPC = Procedures<{}, RPCConfig>()
132
-
133
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
134
-
135
- builder.register(RPC, () => ({}))
136
- const app = builder.build()
137
-
138
- const res = await app.request('/test/test/1', {
139
- method: 'POST',
140
- headers: { 'Content-Type': 'application/json' },
141
- body: JSON.stringify({}),
142
- })
143
- expect(res.status).toBe(200)
144
- })
145
-
146
- test('pathPrefix appears in generated docs', () => {
147
- const builder = new HonoRPCAppBuilder({ pathPrefix: '/api' })
148
- const RPC = Procedures<{}, RPCConfig>()
149
-
150
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
151
-
152
- builder.register(RPC, () => ({}))
153
- builder.build()
154
-
155
- expect(builder.docs[0]!.path).toBe('/api/test/test/1')
156
- })
157
-
158
- test('pathPrefix /rpc restores original behavior', async () => {
159
- const builder = new HonoRPCAppBuilder({ pathPrefix: '/rpc' })
160
- const RPC = Procedures<{}, RPCConfig>()
161
-
162
- RPC.Create('Users', { scope: 'users', version: 1 }, async () => ({ users: [] }))
163
-
164
- builder.register(RPC, () => ({}))
165
- const app = builder.build()
166
-
167
- const res = await app.request('/rpc/users/users/1', {
168
- method: 'POST',
169
- headers: { 'Content-Type': 'application/json' },
170
- body: JSON.stringify({}),
171
- })
172
- expect(res.status).toBe(200)
173
- expect(builder.docs[0]!.path).toBe('/rpc/users/users/1')
174
- })
175
- })
176
-
177
- // --------------------------------------------------------------------------
178
- // Lifecycle Hooks Tests
179
- // --------------------------------------------------------------------------
180
- describe('lifecycle hooks', () => {
181
- test('onRequestStart is called with context object', async () => {
182
- const onRequestStart = vi.fn()
183
- const builder = new HonoRPCAppBuilder({ onRequestStart })
184
- const RPC = Procedures<{}, RPCConfig>()
185
-
186
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
187
-
188
- builder.register(RPC, () => ({}))
189
- const app = builder.build()
190
-
191
- await app.request('/test/test/1', {
192
- method: 'POST',
193
- headers: { 'Content-Type': 'application/json' },
194
- body: JSON.stringify({}),
195
- })
196
-
197
- expect(onRequestStart).toHaveBeenCalledTimes(1)
198
- expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('req')
199
- })
200
-
201
- test('onRequestEnd is called after response', async () => {
202
- const onRequestEnd = vi.fn()
203
- const builder = new HonoRPCAppBuilder({ onRequestEnd })
204
- const RPC = Procedures<{}, RPCConfig>()
205
-
206
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
207
-
208
- builder.register(RPC, () => ({}))
209
- const app = builder.build()
210
-
211
- await app.request('/test/test/1', {
212
- method: 'POST',
213
- headers: { 'Content-Type': 'application/json' },
214
- body: JSON.stringify({}),
215
- })
216
-
217
- expect(onRequestEnd).toHaveBeenCalledTimes(1)
218
- expect(onRequestEnd.mock.calls[0]![0]).toHaveProperty('req')
219
- })
220
-
221
- test('onSuccess is called on successful procedure execution', async () => {
222
- const onSuccess = vi.fn()
223
- const builder = new HonoRPCAppBuilder({ onSuccess })
224
- const RPC = Procedures<{}, RPCConfig>()
225
-
226
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
227
-
228
- builder.register(RPC, () => ({}))
229
- const app = builder.build()
230
-
231
- await app.request('/test/test/1', {
232
- method: 'POST',
233
- headers: { 'Content-Type': 'application/json' },
234
- body: JSON.stringify({}),
235
- })
236
-
237
- expect(onSuccess).toHaveBeenCalledTimes(1)
238
- expect(onSuccess.mock.calls[0]![0]).toHaveProperty('name', 'Test')
239
- })
240
-
241
- test('onSuccess is NOT called when procedure throws', async () => {
242
- const onSuccess = vi.fn()
243
- const builder = new HonoRPCAppBuilder({ onSuccess })
244
- const RPC = Procedures<{}, RPCConfig>()
245
-
246
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
247
- throw new Error('Handler error')
248
- })
249
-
250
- builder.register(RPC, () => ({}))
251
- const app = builder.build()
252
-
253
- await app.request('/test/test/1', {
254
- method: 'POST',
255
- headers: { 'Content-Type': 'application/json' },
256
- body: JSON.stringify({}),
257
- })
258
-
259
- expect(onSuccess).not.toHaveBeenCalled()
260
- })
261
-
262
- test('hooks execute in correct order: start → handler → success → end', async () => {
263
- const order: string[] = []
264
-
265
- const builder = new HonoRPCAppBuilder({
266
- onRequestStart: () => order.push('start'),
267
- onRequestEnd: () => order.push('end'),
268
- onSuccess: () => order.push('success'),
269
- })
270
- const RPC = Procedures<{}, RPCConfig>()
271
-
272
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
273
- order.push('handler')
274
- return { ok: true }
275
- })
276
-
277
- builder.register(RPC, () => ({}))
278
- const app = builder.build()
279
-
280
- await app.request('/test/test/1', {
281
- method: 'POST',
282
- headers: { 'Content-Type': 'application/json' },
283
- body: JSON.stringify({}),
284
- })
285
-
286
- expect(order).toEqual(['start', 'handler', 'success', 'end'])
287
- })
288
- })
289
-
290
- // --------------------------------------------------------------------------
291
- // Error Handling Tests
292
- // --------------------------------------------------------------------------
293
- describe('error handling', () => {
294
- test('custom error handler receives procedure, context, and error', async () => {
295
- const errorHandler = vi.fn((procedure, c, error) => {
296
- return c.json({ customError: error.message }, 400)
297
- })
298
-
299
- const builder = new HonoRPCAppBuilder({ onError: errorHandler })
300
- const RPC = Procedures<{}, RPCConfig>()
301
-
302
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
303
- throw new Error('Test error')
304
- })
305
-
306
- builder.register(RPC, () => ({}))
307
- const app = builder.build()
308
-
309
- const res = await app.request('/test/test/1', {
310
- method: 'POST',
311
- headers: { 'Content-Type': 'application/json' },
312
- body: JSON.stringify({}),
313
- })
314
-
315
- expect(errorHandler).toHaveBeenCalledTimes(1)
316
- expect(errorHandler.mock.calls[0]![0]).toHaveProperty('name', 'Test')
317
- expect(errorHandler.mock.calls[0]![2]).toBeInstanceOf(Error)
318
- expect(res.status).toBe(400)
319
- const body = await res.json()
320
- // Error is wrapped by Procedures with "Error in handler for {name}" prefix
321
- expect(body.customError).toContain('Test error')
322
- })
323
-
324
- test('default error handling returns error message in response', async () => {
325
- const builder = new HonoRPCAppBuilder()
326
- const RPC = Procedures<{}, RPCConfig>()
327
-
328
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
329
- throw new Error('Something went wrong')
330
- })
331
-
332
- builder.register(RPC, () => ({}))
333
- const app = builder.build()
334
-
335
- const res = await app.request('/test/test/1', {
336
- method: 'POST',
337
- headers: { 'Content-Type': 'application/json' },
338
- body: JSON.stringify({}),
339
- })
340
-
341
- expect(res.status).toBe(500)
342
- const body = await res.json()
343
- // Default error handler returns error message in JSON body
344
- expect(body).toHaveProperty('error')
345
- expect(body.error).toContain('Something went wrong')
346
- })
347
-
348
- test('catches unhandled exceptions in handler', async () => {
349
- const builder = new HonoRPCAppBuilder()
350
- const RPC = Procedures<{}, RPCConfig>()
351
-
352
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
353
- // Simulate unhandled exception
354
- const obj: any = null
355
- return obj.property // This will throw
356
- })
357
-
358
- builder.register(RPC, () => ({}))
359
- const app = builder.build()
360
-
361
- const res = await app.request('/test/test/1', {
362
- method: 'POST',
363
- headers: { 'Content-Type': 'application/json' },
364
- body: JSON.stringify({}),
365
- })
366
-
367
- // Unhandled exceptions are caught and returned as error response
368
- const body = await res.json()
369
- expect(body).toHaveProperty('error')
370
- })
371
- })
372
-
373
- // --------------------------------------------------------------------------
374
- // register() Method Tests
375
- // --------------------------------------------------------------------------
376
- describe('register() method', () => {
377
- test('returns this for method chaining', () => {
378
- const builder = new HonoRPCAppBuilder()
379
- const RPC1 = Procedures<{}, RPCConfig>()
380
- const RPC2 = Procedures<{}, RPCConfig>()
381
-
382
- const result = builder.register(RPC1, () => ({}))
383
- expect(result).toBe(builder)
384
-
385
- // Chain multiple registrations
386
- const chainResult = builder.register(RPC1, () => ({})).register(RPC2, () => ({}))
387
-
388
- expect(chainResult).toBe(builder)
389
- })
390
-
391
- test('supports registering multiple factories', async () => {
392
- const builder = new HonoRPCAppBuilder()
393
-
394
- const PublicRPC = Procedures<{ public: true }, RPCConfig>()
395
- const PrivateRPC = Procedures<{ private: true }, RPCConfig>()
396
-
397
- PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
398
- isPublic: ctx.public,
399
- }))
400
-
401
- PrivateRPC.Create('PrivateMethod', { scope: 'private', version: 1 }, async (ctx) => ({
402
- isPrivate: ctx.private,
403
- }))
404
-
405
- builder
406
- .register(PublicRPC, () => ({ public: true as const }))
407
- .register(PrivateRPC, () => ({ private: true as const }))
408
-
409
- const app = builder.build()
410
-
411
- const publicRes = await app.request('/public/public-method/1', {
412
- method: 'POST',
413
- headers: { 'Content-Type': 'application/json' },
414
- body: JSON.stringify({}),
415
- })
416
- const privateRes = await app.request('/private/private-method/1', {
417
- method: 'POST',
418
- headers: { 'Content-Type': 'application/json' },
419
- body: JSON.stringify({}),
420
- })
421
-
422
- const publicBody = await publicRes.json()
423
- const privateBody = await privateRes.json()
424
-
425
- expect(publicBody).toEqual({ isPublic: true })
426
- expect(privateBody).toEqual({ isPrivate: true })
427
- })
428
-
429
- test('context can be a static object', async () => {
430
- const factoryContext = { requestId: 'req-123' }
431
-
432
- const builder = new HonoRPCAppBuilder()
433
- const RPC = Procedures<{ requestId: string }, RPCConfig>()
434
-
435
- RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
436
- id: ctx.requestId,
437
- }))
438
-
439
- builder.register(RPC, factoryContext)
440
- const app = builder.build()
441
-
442
- const res = await app.request('/get-request-id/get-request-id/1', {
443
- method: 'POST',
444
- headers: { 'Content-Type': 'application/json' },
445
- body: JSON.stringify({}),
446
- })
447
-
448
- const body = await res.json()
449
- expect(body).toEqual({ id: 'req-123' })
450
- })
451
-
452
- test('factoryContext can be async function', async () => {
453
- const factoryContext = vi.fn(async () => {
454
- return { requestId: 'req-456' }
455
- })
456
-
457
- const builder = new HonoRPCAppBuilder()
458
- const RPC = Procedures<{ requestId: string }, RPCConfig>()
459
-
460
- RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
461
- id: ctx.requestId,
462
- }))
463
-
464
- builder.register(RPC, factoryContext)
465
- const app = builder.build()
466
-
467
- await app.request('/get-request-id/get-request-id/1', {
468
- method: 'POST',
469
- headers: { 'Content-Type': 'application/json' },
470
- body: JSON.stringify({}),
471
- })
472
-
473
- expect(factoryContext).toHaveBeenCalledTimes(1)
474
- })
475
-
476
- test('factoryContext function receives Hono context object', async () => {
477
- const factoryContext = vi.fn((c) => ({
478
- authHeader: c.req.header('authorization'),
479
- }))
480
-
481
- const builder = new HonoRPCAppBuilder()
482
- const RPC = Procedures<{ authHeader?: string }, RPCConfig>()
483
-
484
- RPC.Create('GetAuth', { scope: 'get-auth', version: 1 }, async (ctx) => ({
485
- auth: ctx.authHeader,
486
- }))
487
-
488
- builder.register(RPC, factoryContext)
489
- const app = builder.build()
490
-
491
- const res = await app.request('/get-auth/get-auth/1', {
492
- method: 'POST',
493
- headers: {
494
- 'Content-Type': 'application/json',
495
- Authorization: 'Bearer token123',
496
- },
497
- body: JSON.stringify({}),
498
- })
499
-
500
- expect(factoryContext).toHaveBeenCalledTimes(1)
501
- expect(factoryContext.mock.calls[0]![0]).toHaveProperty('req')
502
- const body = await res.json()
503
- expect(body).toEqual({ auth: 'Bearer token123' })
504
- })
505
- })
506
-
507
- // --------------------------------------------------------------------------
508
- // build() Method Tests
509
- // --------------------------------------------------------------------------
510
- describe('build() method', () => {
511
- test('creates POST routes for all procedures', async () => {
512
- const builder = new HonoRPCAppBuilder()
513
- const RPC = Procedures<{}, RPCConfig>()
514
-
515
- RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({ m: 1 }))
516
- RPC.Create('MethodTwo', { scope: 'method-two', version: 2 }, async () => ({ m: 2 }))
517
-
518
- builder.register(RPC, () => ({}))
519
- const app = builder.build()
520
-
521
- const res1 = await app.request('/method-one/method-one/1', {
522
- method: 'POST',
523
- headers: { 'Content-Type': 'application/json' },
524
- body: JSON.stringify({}),
525
- })
526
- const res2 = await app.request('/method-two/method-two/2', {
527
- method: 'POST',
528
- headers: { 'Content-Type': 'application/json' },
529
- body: JSON.stringify({}),
530
- })
531
-
532
- expect(res1.status).toBe(200)
533
- expect(res2.status).toBe(200)
534
- const body1 = await res1.json()
535
- const body2 = await res2.json()
536
- expect(body1).toEqual({ m: 1 })
537
- expect(body2).toEqual({ m: 2 })
538
- })
539
-
540
- test('returns the Hono application', () => {
541
- const builder = new HonoRPCAppBuilder()
542
- const app = builder.build()
543
-
544
- expect(app).toBe(builder.app)
545
- expect(typeof app.request).toBe('function')
546
- })
547
-
548
- test('populates docs array after build', () => {
549
- const builder = new HonoRPCAppBuilder()
550
- const RPC = Procedures<{}, RPCConfig>()
551
-
552
- RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({}))
553
- RPC.Create('MethodTwo', { scope: ['nested', 'method'], version: 2 }, async () => ({}))
554
-
555
- expect(builder.docs).toHaveLength(0)
556
-
557
- builder.register(RPC, () => ({}))
558
- builder.build()
559
-
560
- expect(builder.docs).toHaveLength(2)
561
- expect(builder.docs[0]!.path).toBe('/method-one/method-one/1')
562
- expect(builder.docs[1]!.path).toBe('/nested/method/method-two/2')
563
- })
564
-
565
- test('passes request body to handler as params', async () => {
566
- const builder = new HonoRPCAppBuilder()
567
- const RPC = Procedures<{}, RPCConfig>()
568
-
569
- RPC.Create(
570
- 'Echo',
571
- { scope: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } },
572
- async (ctx, params) => ({ received: params.data })
573
- )
574
-
575
- builder.register(RPC, () => ({}))
576
- const app = builder.build()
577
-
578
- const res = await app.request('/echo/echo/1', {
579
- method: 'POST',
580
- headers: { 'Content-Type': 'application/json' },
581
- body: JSON.stringify({ data: 'test-data' }),
582
- })
583
-
584
- const body = await res.json()
585
- expect(body).toEqual({ received: 'test-data' })
586
- })
587
-
588
- test('GET requests return 404 (RPC uses POST only)', async () => {
589
- const builder = new HonoRPCAppBuilder()
590
- const RPC = Procedures<{}, RPCConfig>()
591
-
592
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
593
-
594
- builder.register(RPC, () => ({}))
595
- const app = builder.build()
596
-
597
- const res = await app.request('/test/test/1')
598
-
599
- expect(res.status).toBe(404)
600
- })
601
-
602
- test('skips streaming procedures and tracks them in skippedProcedures', async () => {
603
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
604
- try {
605
- const builder = new HonoRPCAppBuilder()
606
- const RPC = Procedures<{}, RPCConfig>()
607
-
608
- RPC.Create('RegularProc', { scope: 'test', version: 1 }, async () => ({ ok: true }))
609
- RPC.CreateStream('StreamProc', { scope: 'test', version: 1 }, async function* () {
610
- yield { msg: 'stream' }
611
- })
612
-
613
- builder.register(RPC, () => ({}))
614
- builder.build()
615
-
616
- // Only the regular procedure makes it into docs
617
- expect(builder.docs).toHaveLength(1)
618
- expect(builder.docs[0]!.name).toBe('RegularProc')
619
-
620
- // Stream proc surfaces via skippedProcedures
621
- expect(builder.skippedProcedures).toHaveLength(1)
622
- expect(builder.skippedProcedures[0]!.name).toBe('StreamProc')
623
- expect(builder.skippedProcedures[0]!.reason).toMatch(/HonoStreamAppBuilder/)
624
-
625
- // And we logged a warning so devs aren't blindsided
626
- expect(warnSpy).toHaveBeenCalledWith(
627
- expect.stringContaining('Skipping procedure "StreamProc"')
628
- )
629
- } finally {
630
- warnSpy.mockRestore()
631
- }
632
- })
633
- })
634
-
635
- // --------------------------------------------------------------------------
636
- // Path Generation Tests (makeRPCHttpRoutePath)
637
- // --------------------------------------------------------------------------
638
- describe('makeRPCHttpRoutePath', () => {
639
- let builder: HonoRPCAppBuilder
640
-
641
- beforeEach(() => {
642
- builder = new HonoRPCAppBuilder()
643
- })
644
-
645
- test("simple scope with procedure name: 'users' + 'GetUser' → /users/get-user/1", () => {
646
- const path = builder.makeRPCHttpRoutePath('GetUser', { scope: 'users', version: 1 })
647
- expect(path).toBe('/users/get-user/1')
648
- })
649
-
650
- test("array scope with procedure name: ['users', 'profile'] + 'GetById' → /users/profile/get-by-id/1", () => {
651
- const path = builder.makeRPCHttpRoutePath('GetById', {
652
- scope: ['users', 'profile'],
653
- version: 1,
654
- })
655
- expect(path).toBe('/users/profile/get-by-id/1')
656
- })
657
-
658
- test("camelCase procedure name: 'users' + 'getProfile' → /users/get-profile/1", () => {
659
- const path = builder.makeRPCHttpRoutePath('getProfile', { scope: 'users', version: 1 })
660
- expect(path).toBe('/users/get-profile/1')
661
- })
662
-
663
- test("PascalCase procedure name: 'users' + 'UpdateProfile' → /users/update-profile/1", () => {
664
- const path = builder.makeRPCHttpRoutePath('UpdateProfile', { scope: 'users', version: 1 })
665
- expect(path).toBe('/users/update-profile/1')
666
- })
667
-
668
- test('version number included in path', () => {
669
- const pathV1 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 1 })
670
- const pathV2 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 2 })
671
- const pathV99 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 99 })
672
-
673
- expect(pathV1).toBe('/test/test/1')
674
- expect(pathV2).toBe('/test/test/2')
675
- expect(pathV99).toBe('/test/test/99')
676
- })
677
-
678
- test('handles mixed case in array segments', () => {
679
- const path = builder.makeRPCHttpRoutePath('ListUsers', {
680
- scope: ['UserModule', 'getActiveUsers'],
681
- version: 1,
682
- })
683
- expect(path).toBe('/user-module/get-active-users/list-users/1')
684
- })
685
- })
686
-
687
- // --------------------------------------------------------------------------
688
- // Route Documentation Tests (buildRpcHttpRouteDoc)
689
- // --------------------------------------------------------------------------
690
- describe('buildRpcHttpRouteDoc', () => {
691
- let builder: HonoRPCAppBuilder
692
-
693
- beforeEach(() => {
694
- builder = new HonoRPCAppBuilder()
695
- })
696
-
697
- test('generates complete route documentation', () => {
698
- const paramsSchema = v.object({ id: v.string() })
699
- const returnSchema = v.object({ name: v.string() })
700
-
701
- const RPC = Procedures<{}, RPCConfig>()
702
- RPC.Create(
703
- 'GetUser',
704
- { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
705
- async () => ({ name: 'test' })
706
- )
707
-
708
- builder.register(RPC, () => ({}))
709
- builder.build()
710
-
711
- const doc = builder.docs[0]!
712
- expect(doc.path).toBe('/users/get-user/1')
713
- expect(doc.method).toBe('post')
714
- expect(doc.jsonSchema.body).toBeDefined()
715
- expect(doc.jsonSchema.response).toBeDefined()
716
- })
717
-
718
- test('omits body schema when no params defined', () => {
719
- const RPC = Procedures<{}, RPCConfig>()
720
- RPC.Create('NoParams', { scope: 'no-params', version: 1 }, async () => ({ ok: true }))
721
-
722
- builder.register(RPC, () => ({}))
723
- builder.build()
724
-
725
- const doc = builder.docs[0]!
726
- expect(doc.jsonSchema.body).toBeUndefined()
727
- })
728
-
729
- test('omits response schema when no returnType defined', () => {
730
- const RPC = Procedures<{}, RPCConfig>()
731
- RPC.Create(
732
- 'NoReturn',
733
- { scope: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } },
734
- async () => ({})
735
- )
736
-
737
- builder.register(RPC, () => ({}))
738
- builder.build()
739
-
740
- const doc = builder.docs[0]!
741
- expect(doc.jsonSchema.body).toBeDefined()
742
- expect(doc.jsonSchema.response).toBeUndefined()
743
- })
744
-
745
- test("method is always 'post'", () => {
746
- const RPC = Procedures<{}, RPCConfig>()
747
- RPC.Create('Test1', { scope: 't1', version: 1 }, async () => ({}))
748
- RPC.Create('Test2', { scope: 't2', version: 2 }, async () => ({}))
749
-
750
- builder.register(RPC, () => ({}))
751
- builder.build()
752
-
753
- builder.docs.forEach((doc) => {
754
- expect(doc.method).toBe('post')
755
- })
756
- })
757
- })
758
-
759
- // --------------------------------------------------------------------------
760
- // extendProcedureDoc Tests
761
- // --------------------------------------------------------------------------
762
- describe('extendProcedureDoc', () => {
763
- test('adds custom properties to generated documentation', () => {
764
- const builder = new HonoRPCAppBuilder()
765
- const RPC = Procedures<{}, RPCConfig>()
766
-
767
- RPC.Create('GetUser', { scope: 'users', version: 1 }, async () => ({ name: 'test' }))
768
-
769
- builder.register(
770
- RPC,
771
- () => ({}),
772
- ({ base, procedure }) => ({
773
- summary: `Get user endpoint`,
774
- tags: ['users'],
775
- operationId: procedure.name,
776
- })
777
- )
778
- builder.build()
779
-
780
- const doc = builder.docs[0]!
781
- expect(doc).toHaveProperty('summary', 'Get user endpoint')
782
- expect(doc).toHaveProperty('tags', ['users'])
783
- expect(doc).toHaveProperty('operationId', 'GetUser')
784
- })
785
-
786
- test('receives correct base and procedure parameters', () => {
787
- const extendFn = vi.fn(() => ({}))
788
- const builder = new HonoRPCAppBuilder()
789
- const RPC = Procedures<{}, RPCConfig>()
790
-
791
- const paramsSchema = v.object({ id: v.string() })
792
- RPC.Create(
793
- 'GetItem',
794
- { scope: 'items', version: 2, schema: { params: paramsSchema } },
795
- async () => ({})
796
- )
797
-
798
- builder.register(RPC, () => ({}), extendFn)
799
- builder.build()
800
-
801
- expect(extendFn).toHaveBeenCalledTimes(1)
802
- const callArg = extendFn.mock.calls[0]![0 as any] as any
803
-
804
- // Verify base properties
805
- expect(callArg.base).toHaveProperty('name', 'GetItem')
806
- expect(callArg.base).toHaveProperty('version', 2)
807
- expect(callArg.base).toHaveProperty('scope', 'items')
808
- expect(callArg.base).toHaveProperty('path', '/items/get-item/2')
809
- expect(callArg.base).toHaveProperty('method', 'post')
810
- expect(callArg.base.jsonSchema).toHaveProperty('body')
811
-
812
- // Verify procedure properties
813
- expect(callArg.procedure).toHaveProperty('name', 'GetItem')
814
- expect(callArg.procedure).toHaveProperty('handler')
815
- expect(callArg.procedure.config).toHaveProperty('scope', 'items')
816
- expect(callArg.procedure.config).toHaveProperty('version', 2)
817
- })
818
-
819
- test('base properties take precedence over extended properties', () => {
820
- const builder = new HonoRPCAppBuilder()
821
- const RPC = Procedures<{}, RPCConfig>()
822
-
823
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
824
-
825
- builder.register(
826
- RPC,
827
- () => ({}),
828
- () => ({
829
- name: 'OverriddenName',
830
- path: '/overridden/path',
831
- method: 'get',
832
- customField: 'custom-value',
833
- })
834
- )
835
- builder.build()
836
-
837
- const doc = builder.docs[0]!
838
- // Base properties should NOT be overridden
839
- expect(doc.name).toBe('Test')
840
- expect(doc.path).toBe('/test/test/1')
841
- expect(doc.method).toBe('post')
842
- // Custom field should be present
843
- expect(doc).toHaveProperty('customField', 'custom-value')
844
- })
845
-
846
- test('different factories can have different extendProcedureDoc functions', () => {
847
- const builder = new HonoRPCAppBuilder()
848
-
849
- const PublicRPC = Procedures<{}, RPCConfig>()
850
- const AdminRPC = Procedures<{}, RPCConfig>()
851
-
852
- PublicRPC.Create('GetPublic', { scope: 'public', version: 1 }, async () => ({}))
853
- AdminRPC.Create('GetAdmin', { scope: 'admin', version: 1 }, async () => ({}))
854
-
855
- builder
856
- .register(
857
- PublicRPC,
858
- () => ({}),
859
- () => ({
860
- security: [],
861
- tags: ['public'],
862
- })
863
- )
864
- .register(
865
- AdminRPC,
866
- () => ({}),
867
- () => ({
868
- security: [{ bearerAuth: [] }],
869
- tags: ['admin'],
870
- })
871
- )
872
-
873
- builder.build()
874
-
875
- const publicDoc = builder.docs.find((d) => d.name === 'GetPublic')!
876
- const adminDoc = builder.docs.find((d) => d.name === 'GetAdmin')!
877
-
878
- expect(publicDoc).toHaveProperty('security', [])
879
- expect(publicDoc).toHaveProperty('tags', ['public'])
880
- expect(adminDoc).toHaveProperty('security', [{ bearerAuth: [] }])
881
- expect(adminDoc).toHaveProperty('tags', ['admin'])
882
- })
883
-
884
- test('extendProcedureDoc is called for each procedure in factory', () => {
885
- const extendFn = vi.fn(() => ({ extended: true }))
886
- const builder = new HonoRPCAppBuilder()
887
- const RPC = Procedures<{}, RPCConfig>()
888
-
889
- RPC.Create('Method1', { scope: 'm1', version: 1 }, async () => ({}))
890
- RPC.Create('Method2', { scope: 'm2', version: 1 }, async () => ({}))
891
- RPC.Create('Method3', { scope: 'm3', version: 1 }, async () => ({}))
892
-
893
- builder.register(RPC, () => ({}), extendFn)
894
- builder.build()
895
-
896
- expect(extendFn).toHaveBeenCalledTimes(3)
897
-
898
- const procedureNames = extendFn.mock.calls.map((call: any) => call[0].procedure.name)
899
- expect(procedureNames).toContain('Method1')
900
- expect(procedureNames).toContain('Method2')
901
- expect(procedureNames).toContain('Method3')
902
- })
903
-
904
- test('not providing extendProcedureDoc results in base documentation only', () => {
905
- const builder = new HonoRPCAppBuilder()
906
- const RPC = Procedures<{}, RPCConfig>()
907
-
908
- RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
909
-
910
- builder.register(RPC, () => ({})) // No extendProcedureDoc
911
- builder.build()
912
-
913
- const doc = builder.docs[0]!
914
- expect(doc.name).toBe('Test')
915
- expect(doc.path).toBe('/test/test/1')
916
- expect(doc.method).toBe('post')
917
- // Should not have any extra properties
918
- expect(Object.keys(doc)).toEqual(['kind', 'name', 'version', 'scope', 'path', 'method', 'jsonSchema'])
919
- })
920
-
921
- test('extendProcedureDoc can access procedure config for conditional logic', () => {
922
- const builder = new HonoRPCAppBuilder()
923
- const RPC = Procedures<{}, RPCConfig>()
924
-
925
- RPC.Create('PublicEndpoint', { scope: 'public', version: 1 }, async () => ({}))
926
- RPC.Create('PrivateEndpoint', { scope: 'private', version: 1 }, async () => ({}))
927
-
928
- builder.register(
929
- RPC,
930
- () => ({}),
931
- ({ procedure }) => ({
932
- isPublic: procedure.config.scope === 'public',
933
- description: `This is a ${procedure.config.scope} endpoint`,
934
- })
935
- )
936
- builder.build()
937
-
938
- const publicDoc = builder.docs.find((d) => d.name === 'PublicEndpoint')!
939
- const privateDoc = builder.docs.find((d) => d.name === 'PrivateEndpoint')!
940
-
941
- expect(publicDoc).toHaveProperty('isPublic', true)
942
- expect(publicDoc).toHaveProperty('description', 'This is a public endpoint')
943
- expect(privateDoc).toHaveProperty('isPublic', false)
944
- expect(privateDoc).toHaveProperty('description', 'This is a private endpoint')
945
- })
946
-
947
- test('extendProcedureDoc can use base jsonSchema for OpenAPI-style docs', () => {
948
- const builder = new HonoRPCAppBuilder()
949
- const RPC = Procedures<{}, RPCConfig>()
950
-
951
- const paramsSchema = v.object({ userId: v.string() })
952
- const returnSchema = v.object({ name: v.string(), email: v.string() })
953
-
954
- RPC.Create(
955
- 'GetUser',
956
- { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
957
- async () => ({ name: 'test', email: 'test@example.com' })
958
- )
959
-
960
- builder.register(
961
- RPC,
962
- () => ({}),
963
- ({ base }) => ({
964
- requestBody: base.jsonSchema.body
965
- ? { content: { 'application/json': { schema: base.jsonSchema.body } } }
966
- : undefined,
967
- responses: {
968
- 200: base.jsonSchema.response
969
- ? { content: { 'application/json': { schema: base.jsonSchema.response } } }
970
- : { description: 'Success' },
971
- },
972
- })
973
- )
974
- builder.build()
975
-
976
- const doc = builder.docs[0] as any
977
- expect(doc).toHaveProperty('requestBody')
978
- expect(doc.requestBody).toHaveProperty('content')
979
- expect(doc).toHaveProperty('responses')
980
- expect(doc.responses).toHaveProperty('200')
981
- })
982
- })
983
-
984
- // --------------------------------------------------------------------------
985
- // Integration Test
986
- // --------------------------------------------------------------------------
987
- describe('integration', () => {
988
- test('full workflow with multiple procedure factories and different contexts', async () => {
989
- // Define context types
990
- type PublicContext = { source: 'public' }
991
- type AuthContext = { source: 'auth'; userId: string }
992
-
993
- // Create factories
994
- const PublicRPC = Procedures<PublicContext, RPCConfig>()
995
- const AuthRPC = Procedures<AuthContext, RPCConfig>()
996
-
997
- // Create public procedures
998
- PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
999
- version: '1.0.0',
1000
- }))
1001
-
1002
- PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
1003
- status: 'ok',
1004
- }))
1005
-
1006
- // Create authenticated procedures
1007
- AuthRPC.Create(
1008
- 'GetProfile',
1009
- {
1010
- scope: ['users', 'profile'],
1011
- version: 1,
1012
- schema: { returnType: v.object({ userId: v.string(), source: v.string() }) },
1013
- },
1014
- async (ctx) => ({ userId: ctx.userId, source: ctx.source })
1015
- )
1016
-
1017
- AuthRPC.Create(
1018
- 'UpdateProfile',
1019
- {
1020
- scope: ['users', 'profile'],
1021
- version: 2,
1022
- schema: { params: v.object({ name: v.string() }) },
1023
- },
1024
- async (ctx, params) => ({ userId: ctx.userId, name: params.name })
1025
- )
1026
-
1027
- // Build app with lifecycle hooks
1028
- const events: string[] = []
1029
-
1030
- const builder = new HonoRPCAppBuilder({
1031
- onRequestStart: () => events.push('request-start'),
1032
- onRequestEnd: () => events.push('request-end'),
1033
- onSuccess: (proc) => events.push(`success:${proc.name}`),
1034
- })
1035
-
1036
- builder
1037
- .register(PublicRPC, () => ({ source: 'public' as const }))
1038
- .register(AuthRPC, (c) => ({
1039
- source: 'auth' as const,
1040
- userId: c.req.header('x-user-id') || 'anonymous',
1041
- }))
1042
-
1043
- const app = builder.build()
1044
-
1045
- // Test public endpoints
1046
- const versionRes = await app.request('/system/version/get-version/1', {
1047
- method: 'POST',
1048
- headers: { 'Content-Type': 'application/json' },
1049
- body: JSON.stringify({}),
1050
- })
1051
- expect(versionRes.status).toBe(200)
1052
- const versionBody = await versionRes.json()
1053
- expect(versionBody).toEqual({ version: '1.0.0' })
1054
-
1055
- const healthRes = await app.request('/health/health-check/1', {
1056
- method: 'POST',
1057
- headers: { 'Content-Type': 'application/json' },
1058
- body: JSON.stringify({}),
1059
- })
1060
- expect(healthRes.status).toBe(200)
1061
- const healthBody = await healthRes.json()
1062
- expect(healthBody).toEqual({ status: 'ok' })
1063
-
1064
- // Test authenticated endpoints
1065
- const profileRes = await app.request('/users/profile/get-profile/1', {
1066
- method: 'POST',
1067
- headers: {
1068
- 'Content-Type': 'application/json',
1069
- 'X-User-Id': 'user-123',
1070
- },
1071
- body: JSON.stringify({}),
1072
- })
1073
- expect(profileRes.status).toBe(200)
1074
- const profileBody = await profileRes.json()
1075
- expect(profileBody).toEqual({ userId: 'user-123', source: 'auth' })
1076
-
1077
- const updateRes = await app.request('/users/profile/update-profile/2', {
1078
- method: 'POST',
1079
- headers: {
1080
- 'Content-Type': 'application/json',
1081
- 'X-User-Id': 'user-456',
1082
- },
1083
- body: JSON.stringify({ name: 'John Doe' }),
1084
- })
1085
- expect(updateRes.status).toBe(200)
1086
- const updateBody = await updateRes.json()
1087
- expect(updateBody).toEqual({ userId: 'user-456', name: 'John Doe' })
1088
-
1089
- // Verify documentation
1090
- expect(builder.docs).toHaveLength(4)
1091
-
1092
- const paths = builder.docs.map((d) => d.path)
1093
- expect(paths).toContain('/system/version/get-version/1')
1094
- expect(paths).toContain('/health/health-check/1')
1095
- expect(paths).toContain('/users/profile/get-profile/1')
1096
- expect(paths).toContain('/users/profile/update-profile/2')
1097
-
1098
- // Verify hooks were called
1099
- expect(events).toContain('request-start')
1100
- expect(events).toContain('success:GetVersion')
1101
- expect(events).toContain('success:HealthCheck')
1102
- expect(events).toContain('success:GetProfile')
1103
- expect(events).toContain('success:UpdateProfile')
1104
- expect(events).toContain('request-end')
1105
- })
1106
- })
1107
- })