ts-procedures 5.9.1 → 5.10.2

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 (305) hide show
  1. package/README.md +1 -1
  2. package/agent_config/bin/postinstall.mjs +3 -3
  3. package/agent_config/bin/setup.mjs +22 -11
  4. package/agent_config/claude-code/agents/ts-procedures-architect.md +46 -101
  5. package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +50 -35
  6. package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +6 -5
  7. package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +60 -49
  8. package/agent_config/claude-code/skills/ts-procedures-review/SKILL.md +48 -0
  9. package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +19 -24
  10. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/client.md +115 -0
  11. package/agent_config/lib/install-claude.mjs +35 -87
  12. package/build/src/client/call.d.ts +14 -0
  13. package/build/src/client/call.js +47 -0
  14. package/build/src/client/call.js.map +1 -0
  15. package/build/src/client/call.test.d.ts +1 -0
  16. package/build/src/client/call.test.js +124 -0
  17. package/build/src/client/call.test.js.map +1 -0
  18. package/build/src/client/errors.d.ts +25 -0
  19. package/build/src/client/errors.js +33 -0
  20. package/build/src/client/errors.js.map +1 -0
  21. package/build/src/client/errors.test.d.ts +1 -0
  22. package/build/src/client/errors.test.js +41 -0
  23. package/build/src/client/errors.test.js.map +1 -0
  24. package/build/src/client/fetch-adapter.d.ts +12 -0
  25. package/build/src/client/fetch-adapter.js +156 -0
  26. package/build/src/client/fetch-adapter.js.map +1 -0
  27. package/build/src/client/fetch-adapter.test.d.ts +1 -0
  28. package/build/src/client/fetch-adapter.test.js +271 -0
  29. package/build/src/client/fetch-adapter.test.js.map +1 -0
  30. package/build/src/client/hooks.d.ts +17 -0
  31. package/build/src/client/hooks.js +40 -0
  32. package/build/src/client/hooks.js.map +1 -0
  33. package/build/src/client/hooks.test.d.ts +1 -0
  34. package/build/src/client/hooks.test.js +163 -0
  35. package/build/src/client/hooks.test.js.map +1 -0
  36. package/build/src/client/index.d.ts +22 -0
  37. package/build/src/client/index.js +67 -0
  38. package/build/src/client/index.js.map +1 -0
  39. package/build/src/client/index.test.d.ts +1 -0
  40. package/build/src/client/index.test.js +231 -0
  41. package/build/src/client/index.test.js.map +1 -0
  42. package/build/src/client/request-builder.d.ts +13 -0
  43. package/build/src/client/request-builder.js +53 -0
  44. package/build/src/client/request-builder.js.map +1 -0
  45. package/build/src/client/request-builder.test.d.ts +1 -0
  46. package/build/src/client/request-builder.test.js +160 -0
  47. package/build/src/client/request-builder.test.js.map +1 -0
  48. package/build/src/client/stream.d.ts +27 -0
  49. package/build/src/client/stream.js +118 -0
  50. package/build/src/client/stream.js.map +1 -0
  51. package/build/src/client/stream.test.d.ts +1 -0
  52. package/build/src/client/stream.test.js +228 -0
  53. package/build/src/client/stream.test.js.map +1 -0
  54. package/build/src/client/types.d.ts +78 -0
  55. package/build/src/client/types.js +3 -0
  56. package/build/src/client/types.js.map +1 -0
  57. package/build/src/codegen/bin/cli.d.ts +45 -0
  58. package/build/src/codegen/bin/cli.js +246 -0
  59. package/build/src/codegen/bin/cli.js.map +1 -0
  60. package/build/src/codegen/bin/cli.test.d.ts +1 -0
  61. package/build/src/codegen/bin/cli.test.js +220 -0
  62. package/build/src/codegen/bin/cli.test.js.map +1 -0
  63. package/build/src/codegen/constants.d.ts +1 -0
  64. package/build/src/codegen/constants.js +2 -0
  65. package/build/src/codegen/constants.js.map +1 -0
  66. package/build/src/codegen/e2e.test.d.ts +1 -0
  67. package/build/src/codegen/e2e.test.js +464 -0
  68. package/build/src/codegen/e2e.test.js.map +1 -0
  69. package/build/src/codegen/emit-client-runtime.d.ts +9 -0
  70. package/build/src/codegen/emit-client-runtime.js +99 -0
  71. package/build/src/codegen/emit-client-runtime.js.map +1 -0
  72. package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
  73. package/build/src/codegen/emit-client-runtime.test.js +78 -0
  74. package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
  75. package/build/src/codegen/emit-client-types.d.ts +8 -0
  76. package/build/src/codegen/emit-client-types.js +25 -0
  77. package/build/src/codegen/emit-client-types.js.map +1 -0
  78. package/build/src/codegen/emit-client-types.test.d.ts +1 -0
  79. package/build/src/codegen/emit-client-types.test.js +33 -0
  80. package/build/src/codegen/emit-client-types.test.js.map +1 -0
  81. package/build/src/codegen/emit-errors.d.ts +19 -0
  82. package/build/src/codegen/emit-errors.js +59 -0
  83. package/build/src/codegen/emit-errors.js.map +1 -0
  84. package/build/src/codegen/emit-errors.test.d.ts +1 -0
  85. package/build/src/codegen/emit-errors.test.js +175 -0
  86. package/build/src/codegen/emit-errors.test.js.map +1 -0
  87. package/build/src/codegen/emit-index.d.ts +12 -0
  88. package/build/src/codegen/emit-index.js +41 -0
  89. package/build/src/codegen/emit-index.js.map +1 -0
  90. package/build/src/codegen/emit-index.test.d.ts +1 -0
  91. package/build/src/codegen/emit-index.test.js +106 -0
  92. package/build/src/codegen/emit-index.test.js.map +1 -0
  93. package/build/src/codegen/emit-scope.d.ts +15 -0
  94. package/build/src/codegen/emit-scope.js +299 -0
  95. package/build/src/codegen/emit-scope.js.map +1 -0
  96. package/build/src/codegen/emit-scope.test.d.ts +1 -0
  97. package/build/src/codegen/emit-scope.test.js +559 -0
  98. package/build/src/codegen/emit-scope.test.js.map +1 -0
  99. package/build/src/codegen/emit-types.d.ts +43 -0
  100. package/build/src/codegen/emit-types.js +111 -0
  101. package/build/src/codegen/emit-types.js.map +1 -0
  102. package/build/src/codegen/emit-types.test.d.ts +1 -0
  103. package/build/src/codegen/emit-types.test.js +184 -0
  104. package/build/src/codegen/emit-types.test.js.map +1 -0
  105. package/build/src/codegen/group-routes.d.ts +23 -0
  106. package/build/src/codegen/group-routes.js +46 -0
  107. package/build/src/codegen/group-routes.js.map +1 -0
  108. package/build/src/codegen/group-routes.test.d.ts +1 -0
  109. package/build/src/codegen/group-routes.test.js +131 -0
  110. package/build/src/codegen/group-routes.test.js.map +1 -0
  111. package/build/src/codegen/index.d.ts +15 -0
  112. package/build/src/codegen/index.js +16 -0
  113. package/build/src/codegen/index.js.map +1 -0
  114. package/build/src/codegen/naming.d.ts +7 -0
  115. package/build/src/codegen/naming.js +21 -0
  116. package/build/src/codegen/naming.js.map +1 -0
  117. package/build/src/codegen/naming.test.d.ts +1 -0
  118. package/build/src/codegen/naming.test.js +40 -0
  119. package/build/src/codegen/naming.test.js.map +1 -0
  120. package/build/src/codegen/pipeline.d.ts +17 -0
  121. package/build/src/codegen/pipeline.js +78 -0
  122. package/build/src/codegen/pipeline.js.map +1 -0
  123. package/build/src/codegen/pipeline.test.d.ts +1 -0
  124. package/build/src/codegen/pipeline.test.js +269 -0
  125. package/build/src/codegen/pipeline.test.js.map +1 -0
  126. package/build/src/codegen/resolve-envelope.d.ts +7 -0
  127. package/build/src/codegen/resolve-envelope.js +46 -0
  128. package/build/src/codegen/resolve-envelope.js.map +1 -0
  129. package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
  130. package/build/src/codegen/resolve-envelope.test.js +69 -0
  131. package/build/src/codegen/resolve-envelope.test.js.map +1 -0
  132. package/build/src/errors.d.ts +33 -0
  133. package/build/src/errors.js +91 -0
  134. package/build/src/errors.js.map +1 -0
  135. package/build/src/errors.test.d.ts +1 -0
  136. package/build/src/errors.test.js +122 -0
  137. package/build/src/errors.test.js.map +1 -0
  138. package/build/src/exports.d.ts +7 -0
  139. package/build/src/exports.js +8 -0
  140. package/build/src/exports.js.map +1 -0
  141. package/build/src/implementations/http/doc-registry.d.ts +12 -0
  142. package/build/src/implementations/http/doc-registry.js +114 -0
  143. package/build/src/implementations/http/doc-registry.js.map +1 -0
  144. package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
  145. package/build/src/implementations/http/doc-registry.test.js +347 -0
  146. package/build/src/implementations/http/doc-registry.test.js.map +1 -0
  147. package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
  148. package/build/src/implementations/http/express-rpc/index.js +185 -0
  149. package/build/src/implementations/http/express-rpc/index.js.map +1 -0
  150. package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
  151. package/build/src/implementations/http/express-rpc/index.test.js +684 -0
  152. package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
  153. package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
  154. package/build/src/implementations/http/express-rpc/types.js +2 -0
  155. package/build/src/implementations/http/express-rpc/types.js.map +1 -0
  156. package/build/src/implementations/http/hono-api/index.d.ts +102 -0
  157. package/build/src/implementations/http/hono-api/index.js +341 -0
  158. package/build/src/implementations/http/hono-api/index.js.map +1 -0
  159. package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
  160. package/build/src/implementations/http/hono-api/index.test.js +992 -0
  161. package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
  162. package/build/src/implementations/http/hono-api/types.d.ts +13 -0
  163. package/build/src/implementations/http/hono-api/types.js +2 -0
  164. package/build/src/implementations/http/hono-api/types.js.map +1 -0
  165. package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
  166. package/build/src/implementations/http/hono-rpc/index.js +161 -0
  167. package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
  168. package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
  169. package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
  170. package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
  171. package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
  172. package/build/src/implementations/http/hono-rpc/types.js +2 -0
  173. package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
  174. package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
  175. package/build/src/implementations/http/hono-stream/index.js +309 -0
  176. package/build/src/implementations/http/hono-stream/index.js.map +1 -0
  177. package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
  178. package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
  179. package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
  180. package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
  181. package/build/src/implementations/http/hono-stream/types.js +2 -0
  182. package/build/src/implementations/http/hono-stream/types.js.map +1 -0
  183. package/build/src/implementations/types.d.ts +142 -0
  184. package/build/src/implementations/types.js +2 -0
  185. package/build/src/implementations/types.js.map +1 -0
  186. package/build/src/index.d.ts +165 -0
  187. package/build/src/index.js +253 -0
  188. package/build/src/index.js.map +1 -0
  189. package/build/src/index.test.d.ts +1 -0
  190. package/build/src/index.test.js +890 -0
  191. package/build/src/index.test.js.map +1 -0
  192. package/build/src/schema/compute-schema.d.ts +35 -0
  193. package/build/src/schema/compute-schema.js +41 -0
  194. package/build/src/schema/compute-schema.js.map +1 -0
  195. package/build/src/schema/compute-schema.test.d.ts +1 -0
  196. package/build/src/schema/compute-schema.test.js +107 -0
  197. package/build/src/schema/compute-schema.test.js.map +1 -0
  198. package/build/src/schema/extract-json-schema.d.ts +2 -0
  199. package/build/src/schema/extract-json-schema.js +12 -0
  200. package/build/src/schema/extract-json-schema.js.map +1 -0
  201. package/build/src/schema/extract-json-schema.test.d.ts +1 -0
  202. package/build/src/schema/extract-json-schema.test.js +23 -0
  203. package/build/src/schema/extract-json-schema.test.js.map +1 -0
  204. package/build/src/schema/parser.d.ts +28 -0
  205. package/build/src/schema/parser.js +170 -0
  206. package/build/src/schema/parser.js.map +1 -0
  207. package/build/src/schema/parser.test.d.ts +1 -0
  208. package/build/src/schema/parser.test.js +120 -0
  209. package/build/src/schema/parser.test.js.map +1 -0
  210. package/build/src/schema/resolve-schema-lib.d.ts +12 -0
  211. package/build/src/schema/resolve-schema-lib.js +11 -0
  212. package/build/src/schema/resolve-schema-lib.js.map +1 -0
  213. package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
  214. package/build/src/schema/resolve-schema-lib.test.js +17 -0
  215. package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
  216. package/build/src/schema/types.d.ts +8 -0
  217. package/build/src/schema/types.js +2 -0
  218. package/build/src/schema/types.js.map +1 -0
  219. package/build/src/stack-utils.d.ts +25 -0
  220. package/build/src/stack-utils.js +95 -0
  221. package/build/src/stack-utils.js.map +1 -0
  222. package/build/src/stack-utils.test.d.ts +1 -0
  223. package/build/src/stack-utils.test.js +80 -0
  224. package/build/src/stack-utils.test.js.map +1 -0
  225. package/docs/ai-agent-setup.md +7 -6
  226. package/docs/core.md +5 -9
  227. package/docs/streaming.md +9 -9
  228. package/package.json +2 -13
  229. package/src/client/call.test.ts +162 -0
  230. package/src/client/errors.test.ts +43 -0
  231. package/src/client/fetch-adapter.test.ts +340 -0
  232. package/src/client/hooks.test.ts +191 -0
  233. package/src/client/index.test.ts +290 -0
  234. package/src/client/request-builder.test.ts +184 -0
  235. package/src/client/stream.test.ts +331 -0
  236. package/src/codegen/bin/cli.test.ts +260 -0
  237. package/src/codegen/bin/cli.ts +282 -0
  238. package/src/codegen/constants.ts +1 -0
  239. package/src/codegen/e2e.test.ts +565 -0
  240. package/src/codegen/emit-client-runtime.test.ts +93 -0
  241. package/src/codegen/emit-client-runtime.ts +114 -0
  242. package/src/codegen/emit-client-types.test.ts +39 -0
  243. package/src/codegen/emit-client-types.ts +27 -0
  244. package/src/codegen/emit-errors.test.ts +202 -0
  245. package/src/codegen/emit-errors.ts +80 -0
  246. package/src/codegen/emit-index.test.ts +127 -0
  247. package/src/codegen/emit-index.ts +58 -0
  248. package/src/codegen/emit-scope.test.ts +624 -0
  249. package/src/codegen/emit-scope.ts +389 -0
  250. package/src/codegen/emit-types.test.ts +205 -0
  251. package/src/codegen/emit-types.ts +158 -0
  252. package/src/codegen/group-routes.test.ts +159 -0
  253. package/src/codegen/group-routes.ts +61 -0
  254. package/src/codegen/index.ts +30 -0
  255. package/src/codegen/naming.test.ts +50 -0
  256. package/src/codegen/naming.ts +25 -0
  257. package/src/codegen/pipeline.test.ts +316 -0
  258. package/src/codegen/pipeline.ts +108 -0
  259. package/src/codegen/resolve-envelope.test.ts +76 -0
  260. package/src/codegen/resolve-envelope.ts +61 -0
  261. package/src/errors.test.ts +163 -0
  262. package/src/errors.ts +107 -0
  263. package/src/exports.ts +7 -0
  264. package/src/implementations/http/doc-registry.test.ts +415 -0
  265. package/src/implementations/http/doc-registry.ts +143 -0
  266. package/src/implementations/http/express-rpc/README.md +6 -6
  267. package/src/implementations/http/express-rpc/index.test.ts +957 -0
  268. package/src/implementations/http/express-rpc/index.ts +266 -0
  269. package/src/implementations/http/express-rpc/types.ts +16 -0
  270. package/src/implementations/http/hono-api/index.test.ts +1341 -0
  271. package/src/implementations/http/hono-api/index.ts +463 -0
  272. package/src/implementations/http/hono-api/types.ts +16 -0
  273. package/src/implementations/http/hono-rpc/README.md +6 -6
  274. package/src/implementations/http/hono-rpc/index.test.ts +1075 -0
  275. package/src/implementations/http/hono-rpc/index.ts +238 -0
  276. package/src/implementations/http/hono-rpc/types.ts +16 -0
  277. package/src/implementations/http/hono-stream/README.md +12 -12
  278. package/src/implementations/http/hono-stream/index.test.ts +1768 -0
  279. package/src/implementations/http/hono-stream/index.ts +456 -0
  280. package/src/implementations/http/hono-stream/types.ts +20 -0
  281. package/src/implementations/types.ts +174 -0
  282. package/src/index.test.ts +1185 -0
  283. package/src/index.ts +522 -0
  284. package/src/schema/compute-schema.test.ts +128 -0
  285. package/src/schema/compute-schema.ts +88 -0
  286. package/src/schema/extract-json-schema.test.ts +25 -0
  287. package/src/schema/extract-json-schema.ts +15 -0
  288. package/src/schema/parser.test.ts +182 -0
  289. package/src/schema/parser.ts +215 -0
  290. package/src/schema/resolve-schema-lib.test.ts +19 -0
  291. package/src/schema/resolve-schema-lib.ts +29 -0
  292. package/src/schema/types.ts +20 -0
  293. package/src/stack-utils.test.ts +94 -0
  294. package/src/stack-utils.ts +129 -0
  295. package/agent_config/claude-code/skills/review/SKILL.md +0 -53
  296. package/docs/superpowers/plans/2026-03-30-client-codegen.md +0 -2833
  297. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +0 -632
  298. /package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +0 -0
  299. /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
  300. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
  301. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
  302. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
  303. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
  304. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
  305. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
@@ -0,0 +1,163 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import {
3
+ ProcedureError,
4
+ ProcedureValidationError,
5
+ ProcedureRegistrationError,
6
+ } from './errors.js'
7
+ import { DefinitionInfo } from './stack-utils.js'
8
+
9
+ describe('Error Classes', () => {
10
+ test('ProcedureError has correct properties', () => {
11
+ const err = new ProcedureError('TestProc', 'Something failed', { code: 123 })
12
+
13
+ expect(err.name).toBe('ProcedureError')
14
+ expect(err.procedureName).toBe('TestProc')
15
+ expect(err.message).toBe('Something failed')
16
+ expect(err.meta).toEqual({ code: 123 })
17
+ expect(err instanceof Error).toBe(true)
18
+ })
19
+
20
+ test('ProcedureValidationError extends ProcedureError', () => {
21
+ const err = new ProcedureValidationError('TestProc', 'Validation failed', [])
22
+
23
+ expect(err instanceof ProcedureError).toBe(true)
24
+ expect(err instanceof Error).toBe(true)
25
+ expect(err.name).toBe('ProcedureValidationError')
26
+ })
27
+
28
+ test('ProcedureRegistrationError extends ProcedureError', () => {
29
+ const err = new ProcedureRegistrationError('TestProc', 'Registration failed')
30
+
31
+ expect(err instanceof ProcedureError).toBe(true)
32
+ expect(err instanceof Error).toBe(true)
33
+ expect(err.name).toBe('ProcedureRegistrationError')
34
+ expect(err.procedureName).toBe('TestProc')
35
+ })
36
+
37
+ test('ProcedureError supports cause property', () => {
38
+ const cause = new Error('Original error')
39
+ const err = new ProcedureError('TestProc', 'Wrapped error')
40
+ err.cause = cause
41
+
42
+ expect(err.cause).toBe(cause)
43
+ })
44
+
45
+ test('All error types have consistent procedureName property', () => {
46
+ const baseErr = new ProcedureError('Proc1', 'message')
47
+ const validationErr = new ProcedureValidationError('Proc2', 'message', [])
48
+ const registrationErr = new ProcedureRegistrationError('Proc3', 'message')
49
+
50
+ expect(baseErr.procedureName).toBe('Proc1')
51
+ expect(validationErr.procedureName).toBe('Proc2')
52
+ expect(registrationErr.procedureName).toBe('Proc3')
53
+ })
54
+ })
55
+
56
+ describe('Error Classes - Definition Info', () => {
57
+ const mockDefinitionInfo: DefinitionInfo = {
58
+ definedAt: {
59
+ file: '/app/procedures/user.ts',
60
+ line: 25,
61
+ column: 3,
62
+ raw: 'at Object.<anonymous> (/app/procedures/user.ts:25:3)',
63
+ },
64
+ definitionStack: 'Error\n at Object.<anonymous> (/app/procedures/user.ts:25:3)',
65
+ }
66
+
67
+ test('ProcedureError includes definedAt when provided', () => {
68
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
69
+
70
+ expect(err.definedAt).toBeDefined()
71
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
72
+ expect(err.definedAt?.line).toBe(25)
73
+ expect(err.definedAt?.column).toBe(3)
74
+ })
75
+
76
+ test('ProcedureError includes definitionStack when provided', () => {
77
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
78
+
79
+ expect(err.definitionStack).toBeDefined()
80
+ expect(err.definitionStack).toContain('/app/procedures/user.ts')
81
+ })
82
+
83
+ test('ProcedureError works without definitionInfo (backward compat)', () => {
84
+ const err = new ProcedureError('TestProc', 'Something failed', { code: 123 })
85
+
86
+ expect(err.definedAt).toBeUndefined()
87
+ expect(err.definitionStack).toBeUndefined()
88
+ expect(err.procedureName).toBe('TestProc')
89
+ expect(err.message).toBe('Something failed')
90
+ expect(err.meta).toEqual({ code: 123 })
91
+ })
92
+
93
+ test('ProcedureValidationError includes definedAt when provided', () => {
94
+ const err = new ProcedureValidationError('TestProc', 'Validation failed', [], mockDefinitionInfo)
95
+
96
+ expect(err.definedAt).toBeDefined()
97
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
98
+ expect(err.definedAt?.line).toBe(25)
99
+ })
100
+
101
+ test('ProcedureValidationError works without definitionInfo (backward compat)', () => {
102
+ const err = new ProcedureValidationError('TestProc', 'Validation failed', [])
103
+
104
+ expect(err.definedAt).toBeUndefined()
105
+ expect(err.definitionStack).toBeUndefined()
106
+ expect(err.name).toBe('ProcedureValidationError')
107
+ })
108
+
109
+ test('ProcedureRegistrationError includes definedAt when provided', () => {
110
+ const err = new ProcedureRegistrationError('TestProc', 'Registration failed', mockDefinitionInfo)
111
+
112
+ expect(err.definedAt).toBeDefined()
113
+ expect(err.definedAt?.file).toBe('/app/procedures/user.ts')
114
+ })
115
+
116
+ test('ProcedureRegistrationError works without definitionInfo (backward compat)', () => {
117
+ const err = new ProcedureRegistrationError('TestProc', 'Registration failed')
118
+
119
+ expect(err.definedAt).toBeUndefined()
120
+ expect(err.definitionStack).toBeUndefined()
121
+ expect(err.name).toBe('ProcedureRegistrationError')
122
+ })
123
+
124
+ test('getDefinitionLocation returns formatted location string', () => {
125
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
126
+
127
+ const location = err.getDefinitionLocation()
128
+
129
+ expect(location).toBe('/app/procedures/user.ts:25:3')
130
+ })
131
+
132
+ test('getDefinitionLocation returns undefined when no definedAt', () => {
133
+ const err = new ProcedureError('TestProc', 'Something failed')
134
+
135
+ const location = err.getDefinitionLocation()
136
+
137
+ expect(location).toBeUndefined()
138
+ })
139
+
140
+ test('enhanced stack contains definition location', () => {
141
+ const err = new ProcedureError('TestProc', 'Something failed', undefined, mockDefinitionInfo)
142
+
143
+ expect(err.stack).toContain('--- Procedure "TestProc" defined at ---')
144
+ expect(err.stack).toContain('/app/procedures/user.ts:25:3')
145
+ })
146
+
147
+ test('stack is not modified when no definitionInfo', () => {
148
+ const err = new ProcedureError('TestProc', 'Something failed')
149
+
150
+ expect(err.stack).toBeDefined()
151
+ expect(err.stack).not.toContain('--- Procedure')
152
+ })
153
+
154
+ test('all error types enhance stack with definition location', () => {
155
+ const baseErr = new ProcedureError('Proc1', 'message', undefined, mockDefinitionInfo)
156
+ const validationErr = new ProcedureValidationError('Proc2', 'message', [], mockDefinitionInfo)
157
+ const registrationErr = new ProcedureRegistrationError('Proc3', 'message', mockDefinitionInfo)
158
+
159
+ expect(baseErr.stack).toContain('--- Procedure "Proc1" defined at ---')
160
+ expect(validationErr.stack).toContain('--- Procedure "Proc2" defined at ---')
161
+ expect(registrationErr.stack).toContain('--- Procedure "Proc3" defined at ---')
162
+ })
163
+ })
package/src/errors.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { TSchemaValidationError } from './schema/parser.js'
2
+ import { DefinitionInfo, DefinitionLocation, formatDefinitionInfo } from './stack-utils.js'
3
+ import { kebabCase } from 'es-toolkit/string'
4
+
5
+ export class ProcedureError extends Error {
6
+ cause?: unknown
7
+ readonly definedAt?: DefinitionLocation
8
+ readonly definitionStack?: string
9
+
10
+ constructor(
11
+ readonly procedureName: string,
12
+ readonly message: string,
13
+ readonly meta?: object,
14
+ // Used for error stack trace details
15
+ definitionInfo?: DefinitionInfo
16
+ ) {
17
+ super(message)
18
+ this.name = 'ProcedureError'
19
+
20
+ if (definitionInfo) {
21
+ this.definedAt = definitionInfo.definedAt
22
+ this.definitionStack = definitionInfo.definitionStack
23
+ this.enhanceStack()
24
+ }
25
+
26
+ // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
27
+ Object.setPrototypeOf(this, ProcedureError.prototype)
28
+ }
29
+
30
+ /**
31
+ * Returns a formatted string showing where the procedure was defined.
32
+ */
33
+ getDefinitionLocation(): string | undefined {
34
+ if (!this.definedAt) {
35
+ return undefined
36
+ }
37
+ return `${this.definedAt.file}:${this.definedAt.line}:${this.definedAt.column}`
38
+ }
39
+
40
+ /**
41
+ * Enhances the error stack with definition location information.
42
+ */
43
+ private enhanceStack(): void {
44
+ if (!this.stack || !this.definedAt) {
45
+ return
46
+ }
47
+
48
+ const definitionSection = formatDefinitionInfo(
49
+ { definedAt: this.definedAt, definitionStack: this.definitionStack },
50
+ this.procedureName
51
+ )
52
+
53
+ if (definitionSection) {
54
+ this.stack = this.stack + definitionSection
55
+ }
56
+ }
57
+ }
58
+
59
+ export class ProcedureValidationError extends ProcedureError {
60
+ constructor(
61
+ readonly procedureName: string,
62
+ message: string,
63
+ readonly errors?: TSchemaValidationError[],
64
+ // Used for error stack trace details
65
+ definitionInfo?: DefinitionInfo
66
+ ) {
67
+ const readableErrors = errors
68
+ ?.map((err) => `- ${kebabCase(err.instancePath).replace('-', '.')} ${err.message}`)
69
+ .join(', ')
70
+ super(procedureName, message + ' ' + readableErrors, { errors }, definitionInfo)
71
+ this.name = 'ProcedureValidationError'
72
+
73
+ // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
74
+ Object.setPrototypeOf(this, ProcedureValidationError.prototype)
75
+ }
76
+ }
77
+
78
+ export class ProcedureRegistrationError extends ProcedureError {
79
+ constructor(
80
+ readonly procedureName: string,
81
+ message: string,
82
+ // Used for error stack trace details
83
+ definitionInfo?: DefinitionInfo
84
+ ) {
85
+ super(procedureName, message, undefined, definitionInfo)
86
+ this.name = 'ProcedureRegistrationError'
87
+
88
+ // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
89
+ Object.setPrototypeOf(this, ProcedureRegistrationError.prototype)
90
+ }
91
+ }
92
+
93
+ export class ProcedureYieldValidationError extends ProcedureError {
94
+ constructor(
95
+ readonly procedureName: string,
96
+ message: string,
97
+ readonly errors?: TSchemaValidationError[],
98
+ // Used for error stack trace details
99
+ definitionInfo?: DefinitionInfo
100
+ ) {
101
+ super(procedureName, message, undefined, definitionInfo)
102
+ this.name = 'ProcedureYieldValidationError'
103
+
104
+ // https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
105
+ Object.setPrototypeOf(this, ProcedureYieldValidationError.prototype)
106
+ }
107
+ }
package/src/exports.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './index.js'
2
+ export * from './errors.js'
3
+ export * from './stack-utils.js'
4
+ export * from './schema/extract-json-schema.js'
5
+ export * from './schema/parser.js'
6
+ export * from './schema/resolve-schema-lib.js'
7
+ export * from './schema/types.js'
@@ -0,0 +1,415 @@
1
+ import { describe, expect, it, test } from 'vitest'
2
+ import { v } from 'suretype'
3
+ import { Procedures } from '../../index.js'
4
+ import { HonoRPCAppBuilder } from './hono-rpc/index.js'
5
+ import { DocRegistry } from './doc-registry.js'
6
+ import type {
7
+ AnyHttpRouteDoc,
8
+ RPCHttpRouteDoc,
9
+ RPCConfig,
10
+ APIHttpRouteDoc,
11
+ StreamHttpRouteDoc,
12
+ DocSource,
13
+ DocEnvelope,
14
+ } from '../types.js'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers — minimal doc fixtures
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const rpcDoc: RPCHttpRouteDoc = {
21
+ kind: 'rpc',
22
+ name: 'Echo',
23
+ path: '/echo/1',
24
+ method: 'post',
25
+ scope: 'echo',
26
+ version: 1,
27
+ jsonSchema: { body: { type: 'object' } },
28
+ }
29
+
30
+ const apiDoc: APIHttpRouteDoc = {
31
+ kind: 'api',
32
+ name: 'GetUser',
33
+ path: '/users/:id',
34
+ method: 'get',
35
+ fullPath: '/api/users/:id',
36
+ jsonSchema: { pathParams: { type: 'object' } },
37
+ }
38
+
39
+ const streamDoc: StreamHttpRouteDoc = {
40
+ kind: 'stream',
41
+ name: 'Feed',
42
+ path: '/feed/1',
43
+ methods: ['get'],
44
+ streamMode: 'sse',
45
+ scope: 'feed',
46
+ version: 1,
47
+ jsonSchema: { yieldType: { type: 'object' } },
48
+ }
49
+
50
+ function makeSource<T extends AnyHttpRouteDoc>(docs: T[]): DocSource<T> {
51
+ return { docs }
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Tests
56
+ // ---------------------------------------------------------------------------
57
+
58
+ describe('DocRegistry', () => {
59
+ // --------------------------------------------------------------------------
60
+ // Constructor
61
+ // --------------------------------------------------------------------------
62
+ describe('constructor', () => {
63
+ test('uses defaults when no config provided', () => {
64
+ const registry = new DocRegistry()
65
+ const out = registry.toJSON()
66
+ expect(out.basePath).toBe('')
67
+ expect(out.headers).toEqual([])
68
+ expect(out.errors).toEqual([])
69
+ })
70
+
71
+ test('accepts partial config', () => {
72
+ const registry = new DocRegistry({ basePath: '/v1' })
73
+ const out = registry.toJSON()
74
+ expect(out.basePath).toBe('/v1')
75
+ expect(out.headers).toEqual([])
76
+ expect(out.errors).toEqual([])
77
+ })
78
+
79
+ test('accepts full config', () => {
80
+ const headers = [{ name: 'Authorization', description: 'Bearer token', required: true }]
81
+ const errors = [{ name: 'Unauthorized', statusCode: 401, description: 'Missing token' }]
82
+ const registry = new DocRegistry({ basePath: '/api', headers, errors })
83
+ const out = registry.toJSON()
84
+ expect(out.basePath).toBe('/api')
85
+ expect(out.headers).toEqual(headers)
86
+ expect(out.errors).toEqual(errors)
87
+ })
88
+ })
89
+
90
+ // --------------------------------------------------------------------------
91
+ // from()
92
+ // --------------------------------------------------------------------------
93
+ describe('from()', () => {
94
+ test('returns this for chaining', () => {
95
+ const registry = new DocRegistry()
96
+ const result = registry.from(makeSource([rpcDoc]))
97
+ expect(result).toBe(registry)
98
+ })
99
+
100
+ test('accepts RPC builder source', () => {
101
+ const registry = new DocRegistry().from(makeSource([rpcDoc]))
102
+ expect(registry.toJSON().routes).toEqual([rpcDoc])
103
+ })
104
+
105
+ test('accepts API builder source', () => {
106
+ const registry = new DocRegistry().from(makeSource([apiDoc]))
107
+ expect(registry.toJSON().routes).toEqual([apiDoc])
108
+ })
109
+
110
+ test('accepts Stream builder source', () => {
111
+ const registry = new DocRegistry().from(makeSource([streamDoc]))
112
+ expect(registry.toJSON().routes).toEqual([streamDoc])
113
+ })
114
+
115
+ test('accepts plain { docs } object', () => {
116
+ const plain = { docs: [rpcDoc, apiDoc] }
117
+ const registry = new DocRegistry().from(plain)
118
+ expect(registry.toJSON().routes).toHaveLength(2)
119
+ })
120
+
121
+ test('builder not yet built returns empty routes', () => {
122
+ const emptySource = makeSource<RPCHttpRouteDoc>([])
123
+ const registry = new DocRegistry().from(emptySource)
124
+ expect(registry.toJSON().routes).toEqual([])
125
+ })
126
+
127
+ test('same builder twice duplicates routes', () => {
128
+ const source = makeSource([rpcDoc])
129
+ const registry = new DocRegistry().from(source).from(source)
130
+ expect(registry.toJSON().routes).toHaveLength(2)
131
+ expect(registry.toJSON().routes[0]).toEqual(registry.toJSON().routes[1])
132
+ })
133
+
134
+ test('reads docs lazily at toJSON() time', () => {
135
+ const mutableDocs: RPCHttpRouteDoc[] = []
136
+ const source: DocSource<RPCHttpRouteDoc> = { get docs() { return mutableDocs } }
137
+ const registry = new DocRegistry().from(source)
138
+
139
+ expect(registry.toJSON().routes).toHaveLength(0)
140
+
141
+ mutableDocs.push(rpcDoc)
142
+ expect(registry.toJSON().routes).toHaveLength(1)
143
+ })
144
+ })
145
+
146
+ // --------------------------------------------------------------------------
147
+ // toJSON()
148
+ // --------------------------------------------------------------------------
149
+ describe('toJSON()', () => {
150
+ test('returns correct DocEnvelope shape', () => {
151
+ const registry = new DocRegistry({ basePath: '/api' })
152
+ const out = registry.toJSON()
153
+ expect(out).toHaveProperty('basePath')
154
+ expect(out).toHaveProperty('headers')
155
+ expect(out).toHaveProperty('errors')
156
+ expect(out).toHaveProperty('routes')
157
+ })
158
+
159
+ test('collects routes from all sources', () => {
160
+ const registry = new DocRegistry()
161
+ .from(makeSource([rpcDoc]))
162
+ .from(makeSource([apiDoc]))
163
+ .from(makeSource([streamDoc]))
164
+
165
+ const out = registry.toJSON()
166
+ expect(out.routes).toHaveLength(3)
167
+ expect(out.routes).toEqual([rpcDoc, apiDoc, streamDoc])
168
+ })
169
+
170
+ test('empty when no sources', () => {
171
+ const out = new DocRegistry().toJSON()
172
+ expect(out.routes).toEqual([])
173
+ })
174
+
175
+ test('headers and errors are copies', () => {
176
+ const headers = [{ name: 'X-Custom' }]
177
+ const errors = [{ name: 'E', statusCode: 500, description: 'd' }]
178
+ const registry = new DocRegistry({ headers, errors })
179
+ const out = registry.toJSON()
180
+ expect(out.headers).toEqual(headers)
181
+ expect(out.headers).not.toBe(headers)
182
+ expect(out.errors).toEqual(errors)
183
+ expect(out.errors).not.toBe(errors)
184
+ })
185
+ })
186
+
187
+ // --------------------------------------------------------------------------
188
+ // toJSON() filter
189
+ // --------------------------------------------------------------------------
190
+ describe('toJSON() filter', () => {
191
+ test('excludes routes by name', () => {
192
+ const registry = new DocRegistry()
193
+ .from(makeSource([rpcDoc]))
194
+ .from(makeSource([apiDoc]))
195
+
196
+ const out = registry.toJSON({ filter: (r) => r.name !== 'Echo' })
197
+ expect(out.routes).toHaveLength(1)
198
+ expect(out.routes[0]!.name).toBe('GetUser')
199
+ })
200
+
201
+ test('excludes routes by type field', () => {
202
+ const registry = new DocRegistry()
203
+ .from(makeSource([rpcDoc]))
204
+ .from(makeSource([apiDoc]))
205
+
206
+ const out = registry.toJSON({
207
+ filter: (r) => 'method' in r && (r as RPCHttpRouteDoc).method === 'post' && 'scope' in r,
208
+ })
209
+ expect(out.routes).toHaveLength(1)
210
+ expect(out.routes[0]!.name).toBe('Echo')
211
+ })
212
+
213
+ test('all filtered returns empty routes array', () => {
214
+ const registry = new DocRegistry().from(makeSource([rpcDoc]))
215
+ const out = registry.toJSON({ filter: () => false })
216
+ expect(out.routes).toEqual([])
217
+ })
218
+ })
219
+
220
+ // --------------------------------------------------------------------------
221
+ // toJSON() transform
222
+ // --------------------------------------------------------------------------
223
+ describe('toJSON() transform', () => {
224
+ test('adds fields', () => {
225
+ const registry = new DocRegistry({ basePath: '/api' }).from(makeSource([rpcDoc]))
226
+ const out = registry.toJSON({
227
+ transform: (envelope) => ({ ...envelope, version: '2.0' }),
228
+ })
229
+ expect(out.version).toBe('2.0')
230
+ expect(out.basePath).toBe('/api')
231
+ })
232
+
233
+ test('reshapes envelope', () => {
234
+ const registry = new DocRegistry().from(makeSource([rpcDoc]))
235
+ const out = registry.toJSON({
236
+ transform: (envelope) => ({ count: envelope.routes.length }),
237
+ })
238
+ expect(out).toEqual({ count: 1 })
239
+ })
240
+
241
+ test('runs after filter', () => {
242
+ const registry = new DocRegistry()
243
+ .from(makeSource([rpcDoc]))
244
+ .from(makeSource([apiDoc]))
245
+
246
+ const out = registry.toJSON({
247
+ filter: (r) => r.name === 'Echo',
248
+ transform: (envelope) => ({ ...envelope, filteredCount: envelope.routes.length }),
249
+ })
250
+ expect(out.filteredCount).toBe(1)
251
+ expect(out.routes).toHaveLength(1)
252
+ })
253
+ })
254
+
255
+ // --------------------------------------------------------------------------
256
+ // defaultErrors()
257
+ // --------------------------------------------------------------------------
258
+ describe('defaultErrors()', () => {
259
+ test('returns 4 entries', () => {
260
+ const errors = DocRegistry.defaultErrors()
261
+ expect(errors).toHaveLength(4)
262
+ })
263
+
264
+ test('has correct error names', () => {
265
+ const names = DocRegistry.defaultErrors().map((e) => e.name)
266
+ expect(names).toEqual([
267
+ 'ProcedureError',
268
+ 'ProcedureValidationError',
269
+ 'ProcedureYieldValidationError',
270
+ 'ProcedureRegistrationError',
271
+ ])
272
+ })
273
+
274
+ test('has correct status codes', () => {
275
+ const errors = DocRegistry.defaultErrors()
276
+ expect(errors[0]!.statusCode).toBe(500) // ProcedureError
277
+ expect(errors[1]!.statusCode).toBe(400) // ProcedureValidationError
278
+ expect(errors[2]!.statusCode).toBe(500) // ProcedureYieldValidationError
279
+ expect(errors[3]!.statusCode).toBe(500) // ProcedureRegistrationError
280
+ })
281
+
282
+ test('each entry has schema', () => {
283
+ for (const err of DocRegistry.defaultErrors()) {
284
+ expect(err.schema).toBeDefined()
285
+ expect(err.schema!.type).toBe('object')
286
+ }
287
+ })
288
+
289
+ test('returns a new array each call', () => {
290
+ const a = DocRegistry.defaultErrors()
291
+ const b = DocRegistry.defaultErrors()
292
+ expect(a).not.toBe(b)
293
+ expect(a).toEqual(b)
294
+ })
295
+ })
296
+
297
+ // --------------------------------------------------------------------------
298
+ // kind discriminant
299
+ // --------------------------------------------------------------------------
300
+ describe('kind discriminant', () => {
301
+ it('rpcDoc fixture requires kind field', () => {
302
+ const doc = rpcDoc
303
+ expect(doc.kind).toBe('rpc')
304
+ })
305
+
306
+ it('apiDoc fixture requires kind field', () => {
307
+ const doc = apiDoc
308
+ expect(doc.kind).toBe('api')
309
+ })
310
+
311
+ it('streamDoc fixture requires kind field', () => {
312
+ const doc = streamDoc
313
+ expect(doc.kind).toBe('stream')
314
+ })
315
+
316
+ it('narrows AnyHttpRouteDoc union via kind', () => {
317
+ const routes: AnyHttpRouteDoc[] = [rpcDoc, apiDoc, streamDoc]
318
+ for (const route of routes) {
319
+ expect(route.kind).toBeDefined()
320
+ }
321
+ })
322
+ })
323
+
324
+ // --------------------------------------------------------------------------
325
+ // Integration
326
+ // --------------------------------------------------------------------------
327
+ describe('integration', () => {
328
+ test('multiple builder types composed', () => {
329
+ const registry = new DocRegistry({
330
+ basePath: '/api',
331
+ headers: [{ name: 'Authorization', description: 'Bearer token', required: false }],
332
+ errors: DocRegistry.defaultErrors(),
333
+ })
334
+ .from(makeSource([rpcDoc]))
335
+ .from(makeSource([apiDoc]))
336
+ .from(makeSource([streamDoc]))
337
+
338
+ const out = registry.toJSON()
339
+ expect(out.basePath).toBe('/api')
340
+ expect(out.headers).toHaveLength(1)
341
+ expect(out.errors).toHaveLength(4)
342
+ expect(out.routes).toHaveLength(3)
343
+ })
344
+
345
+ test('filter + transform combined', () => {
346
+ const registry = new DocRegistry({
347
+ basePath: '/api',
348
+ errors: DocRegistry.defaultErrors(),
349
+ })
350
+ .from(makeSource([rpcDoc]))
351
+ .from(makeSource([apiDoc]))
352
+ .from(makeSource([streamDoc]))
353
+
354
+ const out = registry.toJSON({
355
+ filter: (r) => r.name !== 'Feed',
356
+ transform: (envelope) => ({
357
+ ...envelope,
358
+ generatedAt: '2026-01-01',
359
+ }),
360
+ })
361
+
362
+ expect(out.routes).toHaveLength(2)
363
+ expect(out.routes.map((r: AnyHttpRouteDoc) => r.name)).toEqual(['Echo', 'GetUser'])
364
+ expect(out.generatedAt).toBe('2026-01-01')
365
+ })
366
+
367
+ test('output is JSON.stringify-safe', () => {
368
+ const registry = new DocRegistry({
369
+ basePath: '/api',
370
+ headers: [{ name: 'X-Request-Id', example: 'abc-123' }],
371
+ errors: DocRegistry.defaultErrors(),
372
+ })
373
+ .from(makeSource([rpcDoc]))
374
+ .from(makeSource([apiDoc]))
375
+
376
+ const out = registry.toJSON()
377
+ const str = JSON.stringify(out)
378
+ expect(() => JSON.parse(str)).not.toThrow()
379
+ expect(JSON.parse(str)).toEqual(out)
380
+ })
381
+
382
+ test('JSON.stringify(registry) produces default envelope', () => {
383
+ const registry = new DocRegistry({ basePath: '/api' })
384
+ .from(makeSource([rpcDoc]))
385
+
386
+ const str = JSON.stringify(registry)
387
+ const parsed = JSON.parse(str)
388
+ expect(parsed.basePath).toBe('/api')
389
+ expect(parsed.routes).toHaveLength(1)
390
+ expect(parsed.routes[0].name).toBe('Echo')
391
+ })
392
+
393
+ test('accepts a real HonoRPCAppBuilder as source', () => {
394
+ const RPC = Procedures<{ userId: string }, RPCConfig>()
395
+
396
+ RPC.Create(
397
+ 'Ping',
398
+ { scope: 'ping', version: 1, schema: { params: v.object({ msg: v.string() }) } },
399
+ async (_ctx, params) => params,
400
+ )
401
+
402
+ const builder = new HonoRPCAppBuilder()
403
+ builder.register(RPC, () => ({ userId: '123' }))
404
+ builder.build()
405
+
406
+ const registry = new DocRegistry({ basePath: '/rpc' }).from(builder)
407
+ const out = registry.toJSON()
408
+
409
+ expect(out.basePath).toBe('/rpc')
410
+ expect(out.routes).toHaveLength(1)
411
+ expect(out.routes[0]!.name).toBe('Ping')
412
+ expect(out.routes[0]!).toHaveProperty('jsonSchema')
413
+ })
414
+ })
415
+ })