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,389 @@
1
+ import type { ScopeGroup } from './group-routes.js'
2
+ import type {
3
+ RPCHttpRouteDoc,
4
+ APIHttpRouteDoc,
5
+ StreamHttpRouteDoc,
6
+ } from '../implementations/types.js'
7
+ import {
8
+ jsonSchemaToTypeString,
9
+ jsonSchemaToTypeBody,
10
+ jsonSchemaToExtractedTypes,
11
+ type AjscOptions,
12
+ type ExtractedTypeOutput,
13
+ } from './emit-types.js'
14
+ import { CODEGEN_HEADER } from './constants.js'
15
+ import { toPascalCase } from './naming.js'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface EmitScopeOptions {
22
+ ajsc?: AjscOptions
23
+ clientImportPath?: string
24
+ namespaceTypes?: boolean
25
+ }
26
+
27
+ interface RouteChunks {
28
+ typeDeclarations: string[]
29
+ callable: string
30
+ hasStream: boolean
31
+ }
32
+
33
+ interface EmitRouteContext {
34
+ ajsc?: AjscOptions
35
+ namespaceTypes: boolean
36
+ scopePascal: string
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Infers the route kind when the `kind` discriminant is missing.
45
+ * This provides backward compatibility with servers running older ts-procedures
46
+ * versions that don't set `kind` on route docs.
47
+ */
48
+ function inferRouteKind(route: Record<string, unknown>): 'rpc' | 'api' | 'stream' {
49
+ if ('streamMode' in route) return 'stream'
50
+ if ('fullPath' in route) return 'api'
51
+ return 'rpc'
52
+ }
53
+
54
+ /**
55
+ * Checks whether a JSON schema looks like an SSE envelope.
56
+ * SSE envelopes have properties: data, event, id (and optionally retry).
57
+ */
58
+ function isSseEnvelope(schema: Record<string, unknown>): boolean {
59
+ const props = schema.properties
60
+ if (props == null || typeof props !== 'object') return false
61
+ const keys = Object.keys(props as Record<string, unknown>)
62
+ return keys.includes('data') && keys.includes('event') && keys.includes('id')
63
+ }
64
+
65
+ /**
66
+ * Unwraps an SSE envelope schema to return the inner `data` property schema.
67
+ * If not an SSE envelope, returns the schema unchanged.
68
+ */
69
+ function unwrapSseEnvelope(
70
+ schema: Record<string, unknown>
71
+ ): Record<string, unknown> {
72
+ if (!isSseEnvelope(schema)) return schema
73
+ const props = schema.properties as Record<string, Record<string, unknown>>
74
+ const dataSchema = props['data']
75
+ return dataSchema ?? schema
76
+ }
77
+
78
+ /**
79
+ * Returns the PascalCase display name for a route, appending `V{version}`
80
+ * when version > 1. Version 1 produces no suffix for backward compatibility.
81
+ */
82
+ function versionedPascal(name: string, version: number | undefined): string {
83
+ const pascal = toPascalCase(name)
84
+ if (version != null && version > 1) return `${pascal}V${version}`
85
+ return pascal
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Shared type formatting helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /** Indent each line of a multi-line string by a given prefix. */
93
+ function indent(text: string, prefix: string): string {
94
+ return text.split('\n').map((line) => (line ? prefix + line : line)).join('\n')
95
+ }
96
+
97
+ interface NamedType {
98
+ /** Short name for namespace mode (e.g., 'Params', 'Response'). */
99
+ shortName: string
100
+ /** Schema to convert, or undefined if not present. */
101
+ schema: Record<string, unknown> | undefined
102
+ }
103
+
104
+ interface FormattedTypes {
105
+ /** Type declarations to add to the file. */
106
+ declarations: string[]
107
+ /** Map of shortName → qualified type reference (for callables). */
108
+ refs: Record<string, string>
109
+ }
110
+
111
+ /**
112
+ * Converts multiple schemas into type declarations and type references.
113
+ * In flat mode: `export type ${routePascal}${shortName} = <body>`
114
+ * In namespace mode: extracted sub-types + named types inside `export namespace ${routePascal} { ... }`
115
+ */
116
+ async function formatTypes(
117
+ routePascal: string,
118
+ types: NamedType[],
119
+ ctx: EmitRouteContext,
120
+ ): Promise<FormattedTypes> {
121
+ const declarations: string[] = []
122
+ const refs: Record<string, string> = {}
123
+
124
+ if (ctx.namespaceTypes) {
125
+ const nsLines: string[] = []
126
+ const seenDeclarations = new Set<string>()
127
+
128
+ for (const { shortName, schema } of types) {
129
+ if (schema == null) continue
130
+
131
+ const result = await jsonSchemaToExtractedTypes(schema, ctx.ajsc)
132
+ if (result == null) continue
133
+
134
+ // Collect extracted sub-types (deduplicate across schemas)
135
+ for (const decl of result.declarations) {
136
+ if (!seenDeclarations.has(decl)) {
137
+ seenDeclarations.add(decl)
138
+ nsLines.push(indent(decl, ' '))
139
+ }
140
+ }
141
+
142
+ nsLines.push(` export type ${shortName} = ${result.body}`)
143
+ refs[shortName] = `${ctx.scopePascal}.${routePascal}.${shortName}`
144
+ }
145
+
146
+ if (nsLines.length > 0) {
147
+ declarations.push(` export namespace ${routePascal} {\n${nsLines.join('\n')}\n }`)
148
+ }
149
+ } else {
150
+ for (const { shortName, schema } of types) {
151
+ if (schema == null) continue
152
+
153
+ const flatName = `${routePascal}${shortName}`
154
+ const body = await jsonSchemaToTypeBody(schema, ctx.ajsc)
155
+ if (body == null) continue
156
+
157
+ declarations.push(`export type ${flatName} = ${body}`)
158
+ refs[shortName] = flatName
159
+ }
160
+ }
161
+
162
+ return { declarations, refs }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Route emitters
167
+ // ---------------------------------------------------------------------------
168
+
169
+ async function emitRpcRoute(route: RPCHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
170
+ const pascal = versionedPascal(route.name, route.version)
171
+
172
+ const { declarations, refs } = await formatTypes(pascal, [
173
+ { shortName: 'Params', schema: route.jsonSchema.body },
174
+ { shortName: 'Response', schema: route.jsonSchema.response },
175
+ ], ctx)
176
+
177
+ const paramsTypeName = refs['Params'] ?? 'unknown'
178
+ const responseTypeName = refs['Response'] ?? 'unknown'
179
+ const scopeStr = Array.isArray(route.scope) ? route.scope.join('-') : route.scope
180
+
181
+ const callable = [
182
+ ` /** ${route.method.toUpperCase()} ${route.path} */`,
183
+ ` ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
184
+ ` return client.call<${responseTypeName}>({`,
185
+ ` name: '${pascal}',`,
186
+ ` scope: '${scopeStr}',`,
187
+ ` path: '${route.path}',`,
188
+ ` method: '${route.method}',`,
189
+ ` kind: 'rpc',`,
190
+ ` params,`,
191
+ ` }, options)`,
192
+ ` },`,
193
+ ].join('\n')
194
+
195
+ return { typeDeclarations: declarations, callable, hasStream: false }
196
+ }
197
+
198
+ async function emitApiRoute(route: APIHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
199
+ const pascal = toPascalCase(route.name)
200
+ const channelKeys = ['pathParams', 'query', 'body', 'headers'] as const
201
+
202
+ // Build channel types + structured Params type
203
+ const channelTypes: NamedType[] = []
204
+ const presentChannels: string[] = []
205
+
206
+ for (const channel of channelKeys) {
207
+ const channelSchema = route.jsonSchema[channel]
208
+ if (channelSchema != null) {
209
+ channelTypes.push({ shortName: toPascalCase(channel), schema: channelSchema })
210
+ presentChannels.push(channel)
211
+ }
212
+ }
213
+
214
+ // Add response
215
+ channelTypes.push({ shortName: 'Response', schema: route.jsonSchema.response })
216
+
217
+ const { declarations, refs } = await formatTypes(pascal, channelTypes, ctx)
218
+
219
+ // Compose structured Params type from channels
220
+ let paramsTypeName = 'unknown'
221
+ if (presentChannels.length > 0) {
222
+ if (ctx.namespaceTypes) {
223
+ const structureFields = presentChannels
224
+ .map((ch) => `${ch}: ${toPascalCase(ch)}`)
225
+ .join('; ')
226
+ // Insert Params type into the namespace block (before closing brace)
227
+ const lastIdx = declarations.length - 1
228
+ if (lastIdx >= 0) {
229
+ const lastDecl = declarations[lastIdx]!
230
+ const closingIdx = lastDecl.lastIndexOf(' }')
231
+ if (closingIdx !== -1) {
232
+ declarations[lastIdx] =
233
+ lastDecl.slice(0, closingIdx) +
234
+ ` export type Params = { ${structureFields} }\n` +
235
+ lastDecl.slice(closingIdx)
236
+ }
237
+ }
238
+ paramsTypeName = `${ctx.scopePascal}.${pascal}.Params`
239
+ } else {
240
+ const structureFields = presentChannels
241
+ .map((ch) => `${ch}: ${refs[toPascalCase(ch)]}`)
242
+ .join('; ')
243
+ declarations.push(`export type ${pascal}Params = { ${structureFields} }`)
244
+ paramsTypeName = `${pascal}Params`
245
+ }
246
+ }
247
+
248
+ const responseTypeName = refs['Response'] ?? 'unknown'
249
+ const scopeStr = route.scope ?? 'default'
250
+
251
+ const callable = [
252
+ ` /** ${route.method.toUpperCase()} ${route.fullPath} */`,
253
+ ` ${route.name}(params: ${paramsTypeName}, options?: ProcedureCallOptions): Promise<${responseTypeName}> {`,
254
+ ` return client.call<${responseTypeName}>({`,
255
+ ` name: '${route.name}',`,
256
+ ` scope: '${scopeStr}',`,
257
+ ` path: '${route.fullPath}',`,
258
+ ` method: '${route.method}',`,
259
+ ` kind: 'api',`,
260
+ ` params,`,
261
+ ` }, options)`,
262
+ ` },`,
263
+ ].join('\n')
264
+
265
+ return { typeDeclarations: declarations, callable, hasStream: false }
266
+ }
267
+
268
+ async function emitStreamRoute(route: StreamHttpRouteDoc, ctx: EmitRouteContext): Promise<RouteChunks> {
269
+ const pascal = versionedPascal(route.name, route.version)
270
+
271
+ // Unwrap SSE envelope from yieldType
272
+ let yieldSchema = route.jsonSchema.yieldType
273
+ if (yieldSchema != null && route.streamMode === 'sse' && isSseEnvelope(yieldSchema)) {
274
+ yieldSchema = unwrapSseEnvelope(yieldSchema)
275
+ }
276
+
277
+ const { declarations, refs } = await formatTypes(pascal, [
278
+ { shortName: 'Params', schema: route.jsonSchema.params },
279
+ { shortName: 'Yield', schema: yieldSchema },
280
+ { shortName: 'Return', schema: route.jsonSchema.returnType },
281
+ ], ctx)
282
+
283
+ const paramsTypeName = refs['Params'] ?? 'unknown'
284
+ const yieldTypeName = refs['Yield'] ?? 'unknown'
285
+ const returnTypeName = refs['Return'] ?? 'void'
286
+ const scopeStr = Array.isArray(route.scope) ? route.scope.join('-') : route.scope
287
+
288
+ const callable = [
289
+ ` /** ${route.methods.map((m) => m.toUpperCase()).join('|')} ${route.path} */`,
290
+ ` ${pascal}(params: ${paramsTypeName}, options?: ProcedureCallOptions): TypedStream<${yieldTypeName}, ${returnTypeName}> {`,
291
+ ` return client.stream<${yieldTypeName}, ${returnTypeName}>({`,
292
+ ` name: '${pascal}',`,
293
+ ` scope: '${scopeStr}',`,
294
+ ` path: '${route.path}',`,
295
+ ` method: '${route.methods[0] ?? 'get'}',`,
296
+ ` kind: 'stream',`,
297
+ ` streamMode: '${route.streamMode}',`,
298
+ ` params,`,
299
+ ` }, options)`,
300
+ ` },`,
301
+ ].join('\n')
302
+
303
+ return { typeDeclarations: declarations, callable, hasStream: true }
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // emitScopeFile
308
+ // ---------------------------------------------------------------------------
309
+
310
+ /**
311
+ * Generates a complete TypeScript scope file for a ScopeGroup.
312
+ *
313
+ * When `namespaceTypes` is true, types are wrapped in nested TypeScript namespaces
314
+ * and ajsc runs with `inlineTypes: false` so formatting options (enumStyle, depluralize,
315
+ * jsdoc, etc.) produce extracted sub-types inside each namespace.
316
+ */
317
+ export async function emitScopeFile(
318
+ group: ScopeGroup,
319
+ options?: EmitScopeOptions,
320
+ ): Promise<string> {
321
+ const { ajsc: ajscOpts, clientImportPath = 'ts-procedures/client', namespaceTypes = false } = options ?? {}
322
+
323
+ const pascal = toPascalCase(group.camelCase)
324
+ const ctx: EmitRouteContext = { ajsc: ajscOpts, namespaceTypes, scopePascal: pascal }
325
+
326
+ const allTypeDeclarations: string[] = []
327
+ const callables: string[] = []
328
+ let hasStream = false
329
+
330
+ for (const route of group.routes) {
331
+ let chunks: RouteChunks
332
+ const kind = route.kind ?? inferRouteKind(route as Record<string, unknown>)
333
+
334
+ try {
335
+ if (kind === 'rpc') {
336
+ chunks = await emitRpcRoute(route as RPCHttpRouteDoc, ctx)
337
+ } else if (kind === 'api') {
338
+ chunks = await emitApiRoute(route as APIHttpRouteDoc, ctx)
339
+ } else if (kind === 'stream') {
340
+ chunks = await emitStreamRoute(route as StreamHttpRouteDoc, ctx)
341
+ } else {
342
+ throw new Error(`Unknown route kind "${kind}"`)
343
+ }
344
+ } catch (err) {
345
+ const msg = err instanceof Error ? err.message : String(err)
346
+ throw new Error(
347
+ `[ts-procedures-codegen] Failed to emit route "${route.name}" (kind: ${kind}, scope: ${group.scopeKey}): ${msg}`
348
+ )
349
+ }
350
+
351
+ allTypeDeclarations.push(...chunks.typeDeclarations)
352
+ callables.push(chunks.callable)
353
+ if (chunks.hasStream) hasStream = true
354
+ }
355
+
356
+ // Build import line
357
+ const clientImports = hasStream
358
+ ? `import type { ClientInstance, ProcedureCallOptions, TypedStream } from '${clientImportPath}'`
359
+ : `import type { ClientInstance, ProcedureCallOptions } from '${clientImportPath}'`
360
+
361
+ let typesBlock: string
362
+ if (namespaceTypes && allTypeDeclarations.length > 0) {
363
+ typesBlock = `export namespace ${pascal} {\n${allTypeDeclarations.join('\n\n')}\n}\n`
364
+ } else {
365
+ typesBlock =
366
+ allTypeDeclarations.length > 0
367
+ ? allTypeDeclarations.join('\n') + '\n'
368
+ : ''
369
+ }
370
+
371
+ const callablesBlock = callables.join('\n\n')
372
+
373
+ return [
374
+ CODEGEN_HEADER,
375
+ clientImports,
376
+ '',
377
+ '// ── Types ────────────────────────────────────────',
378
+ '',
379
+ typesBlock,
380
+ '// ── Callables ────────────────────────────────────',
381
+ '',
382
+ `export function bind${pascal}Scope(client: ClientInstance) {`,
383
+ ' return {',
384
+ callablesBlock,
385
+ ' }',
386
+ '}',
387
+ '',
388
+ ].join('\n')
389
+ }
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { jsonSchemaToTypeString, jsonSchemaToTypeBody, jsonSchemaToExtractedTypes } from './emit-types.js'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Tests
6
+ // ---------------------------------------------------------------------------
7
+
8
+ describe('jsonSchemaToTypeString', () => {
9
+ it('converts a simple object schema with required fields', async () => {
10
+ const schema = {
11
+ type: 'object',
12
+ properties: {
13
+ id: { type: 'string' },
14
+ },
15
+ required: ['id'],
16
+ }
17
+ const result = await jsonSchemaToTypeString('MyType', schema)
18
+ expect(result).toBe('export type MyType = { id: string; }')
19
+ })
20
+
21
+ it('includes optional marker for non-required fields', async () => {
22
+ const schema = {
23
+ type: 'object',
24
+ properties: {
25
+ id: { type: 'string' },
26
+ name: { type: 'string' },
27
+ },
28
+ required: ['id'],
29
+ }
30
+ const result = await jsonSchemaToTypeString('User', schema)
31
+ expect(result).toBe('export type User = { id: string; name?: string; }')
32
+ })
33
+
34
+ it('converts a string enum schema', async () => {
35
+ const schema = {
36
+ type: 'string',
37
+ enum: ['a', 'b'],
38
+ }
39
+ const result = await jsonSchemaToTypeString('Status', schema)
40
+ expect(result).toBe('export type Status = "a" | "b"')
41
+ })
42
+
43
+ it('converts a plain string schema', async () => {
44
+ const schema = { type: 'string' }
45
+ const result = await jsonSchemaToTypeString('Id', schema)
46
+ expect(result).toBe('export type Id = string')
47
+ })
48
+
49
+ it('returns undefined for undefined schema', async () => {
50
+ const result = await jsonSchemaToTypeString('Missing', undefined)
51
+ expect(result).toBeUndefined()
52
+ })
53
+
54
+ it('uses the typeName in the export statement', async () => {
55
+ const schema = { type: 'number' }
56
+ const result = await jsonSchemaToTypeString('Count', schema)
57
+ expect(result).toMatch(/^export type Count = /)
58
+ })
59
+
60
+ it('handles already-clean inline output', async () => {
61
+ // Existing schemas still work — no regression
62
+ const schema = {
63
+ type: 'object',
64
+ properties: { id: { type: 'string' } },
65
+ required: ['id'],
66
+ }
67
+ const result = await jsonSchemaToTypeString('MyType', schema)
68
+ expect(result).toBe('export type MyType = { id: string; }')
69
+ })
70
+
71
+ it('returns undefined for null schema', async () => {
72
+ const result = await jsonSchemaToTypeString('Missing', null as unknown as undefined)
73
+ expect(result).toBeUndefined()
74
+ })
75
+ })
76
+
77
+ describe('jsonSchemaToTypeBody', () => {
78
+ it('returns bare type literal without export wrapper', async () => {
79
+ const schema = {
80
+ type: 'object',
81
+ properties: { id: { type: 'string' } },
82
+ required: ['id'],
83
+ }
84
+ const result = await jsonSchemaToTypeBody(schema)
85
+ expect(result).toBe('{ id: string; }')
86
+ })
87
+
88
+ it('returns undefined for undefined schema', async () => {
89
+ const result = await jsonSchemaToTypeBody(undefined)
90
+ expect(result).toBeUndefined()
91
+ })
92
+
93
+ it('returns plain type for scalar schema', async () => {
94
+ const result = await jsonSchemaToTypeBody({ type: 'string' })
95
+ expect(result).toBe('string')
96
+ })
97
+
98
+ it('returns union for enum schema', async () => {
99
+ const result = await jsonSchemaToTypeBody({ type: 'string', enum: ['a', 'b'] })
100
+ expect(result).toBe('"a" | "b"')
101
+ })
102
+ })
103
+
104
+ describe('jsonSchemaToExtractedTypes', () => {
105
+ it('returns undefined for undefined schema', async () => {
106
+ const result = await jsonSchemaToExtractedTypes(undefined)
107
+ expect(result).toBeUndefined()
108
+ })
109
+
110
+ it('extracts nested object types as declarations', async () => {
111
+ const schema = {
112
+ type: 'object',
113
+ properties: {
114
+ user: {
115
+ type: 'object',
116
+ properties: { name: { type: 'string' } },
117
+ required: ['name'],
118
+ },
119
+ },
120
+ required: ['user'],
121
+ }
122
+ const result = await jsonSchemaToExtractedTypes(schema)
123
+ expect(result).not.toBeUndefined()
124
+ expect(result!.declarations.length).toBeGreaterThan(0)
125
+ expect(result!.declarations.some((d) => d.includes('export type User'))).toBe(true)
126
+ expect(result!.body).toContain('user: User')
127
+ })
128
+
129
+ it('returns empty declarations for flat schemas', async () => {
130
+ const schema = { type: 'string' }
131
+ const result = await jsonSchemaToExtractedTypes(schema)
132
+ expect(result).not.toBeUndefined()
133
+ expect(result!.declarations).toEqual([])
134
+ expect(result!.body).toBe('string')
135
+ })
136
+
137
+ it('extracts enums with enumStyle: enum', async () => {
138
+ const schema = {
139
+ type: 'object',
140
+ properties: {
141
+ status: { type: 'string', enum: ['active', 'pending'] },
142
+ },
143
+ required: ['status'],
144
+ }
145
+ const result = await jsonSchemaToExtractedTypes(schema, { enumStyle: 'enum' })
146
+ expect(result).not.toBeUndefined()
147
+ expect(result!.declarations.some((d) => d.includes('export enum Status'))).toBe(true)
148
+ expect(result!.body).toContain('status: Status')
149
+ })
150
+
151
+ it('strips trailing semicolons from declarations', async () => {
152
+ const schema = {
153
+ type: 'object',
154
+ properties: {
155
+ item: {
156
+ type: 'object',
157
+ properties: { id: { type: 'string' } },
158
+ required: ['id'],
159
+ },
160
+ },
161
+ required: ['item'],
162
+ }
163
+ const result = await jsonSchemaToExtractedTypes(schema)
164
+ expect(result).not.toBeUndefined()
165
+ for (const decl of result!.declarations) {
166
+ expect(decl.trimEnd()).not.toMatch(/;\s*$/)
167
+ }
168
+ })
169
+
170
+ it('body does not contain export type Root prefix', async () => {
171
+ const schema = {
172
+ type: 'object',
173
+ properties: { id: { type: 'string' } },
174
+ required: ['id'],
175
+ }
176
+ const result = await jsonSchemaToExtractedTypes(schema)
177
+ expect(result).not.toBeUndefined()
178
+ expect(result!.body).not.toContain('export type Root')
179
+ expect(result!.body).toContain('id: string')
180
+ })
181
+ })
182
+
183
+ describe('jsonSchemaToTypeString (prefix stripping)', () => {
184
+ it('strips "export type Root = " prefix and does not double-wrap', async () => {
185
+ vi.resetModules()
186
+ vi.doMock('ajsc', () => ({
187
+ TypescriptConverter: class {
188
+ code: string
189
+ constructor(_schema: unknown, _opts: unknown) {
190
+ this.code = '\n\nexport type Root = { id: string; };\n'
191
+ }
192
+ },
193
+ }))
194
+
195
+ const { jsonSchemaToTypeString: fn } = await import('./emit-types.js')
196
+ const result = await fn('MyType', { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] })
197
+ // Should be the clean body, not double-wrapped
198
+ expect(result).toBe('export type MyType = { id: string; }')
199
+ expect(result).not.toContain('export type Root')
200
+ expect(result).not.toContain('export type MyType = export type')
201
+
202
+ vi.doUnmock('ajsc')
203
+ vi.resetModules()
204
+ })
205
+ })