ts-procedures 5.10.0 → 5.12.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 (252) hide show
  1. package/agent_config/bin/postinstall.mjs +3 -3
  2. package/agent_config/bin/setup.mjs +22 -11
  3. package/agent_config/claude-code/agents/ts-procedures-architect.md +2 -2
  4. package/agent_config/claude-code/skills/{guide → ts-procedures}/SKILL.md +1 -1
  5. package/agent_config/claude-code/skills/{guide → ts-procedures}/api-reference.md +11 -8
  6. package/agent_config/claude-code/skills/{guide → ts-procedures}/patterns.md +8 -2
  7. package/agent_config/claude-code/skills/{review → ts-procedures-review}/SKILL.md +3 -3
  8. package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/SKILL.md +2 -2
  9. package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/client.md +4 -4
  10. package/agent_config/copilot/copilot-instructions.md +6 -5
  11. package/agent_config/cursor/cursorrules +6 -5
  12. package/agent_config/lib/install-claude.mjs +35 -87
  13. package/build/codegen/e2e.test.js +21 -14
  14. package/build/codegen/e2e.test.js.map +1 -1
  15. package/build/codegen/emit-index.d.ts +7 -3
  16. package/build/codegen/emit-index.js +33 -20
  17. package/build/codegen/emit-index.js.map +1 -1
  18. package/build/codegen/emit-index.test.js +69 -45
  19. package/build/codegen/emit-index.test.js.map +1 -1
  20. package/build/codegen/pipeline.js +4 -5
  21. package/build/codegen/pipeline.js.map +1 -1
  22. package/build/codegen/pipeline.test.js +4 -4
  23. package/build/codegen/pipeline.test.js.map +1 -1
  24. package/build/src/client/call.d.ts +14 -0
  25. package/build/src/client/call.js +47 -0
  26. package/build/src/client/call.js.map +1 -0
  27. package/build/src/client/call.test.d.ts +1 -0
  28. package/build/src/client/call.test.js +124 -0
  29. package/build/src/client/call.test.js.map +1 -0
  30. package/build/src/client/errors.d.ts +25 -0
  31. package/build/src/client/errors.js +33 -0
  32. package/build/src/client/errors.js.map +1 -0
  33. package/build/src/client/errors.test.d.ts +1 -0
  34. package/build/src/client/errors.test.js +41 -0
  35. package/build/src/client/errors.test.js.map +1 -0
  36. package/build/src/client/fetch-adapter.d.ts +12 -0
  37. package/build/src/client/fetch-adapter.js +156 -0
  38. package/build/src/client/fetch-adapter.js.map +1 -0
  39. package/build/src/client/fetch-adapter.test.d.ts +1 -0
  40. package/build/src/client/fetch-adapter.test.js +271 -0
  41. package/build/src/client/fetch-adapter.test.js.map +1 -0
  42. package/build/src/client/hooks.d.ts +17 -0
  43. package/build/src/client/hooks.js +40 -0
  44. package/build/src/client/hooks.js.map +1 -0
  45. package/build/src/client/hooks.test.d.ts +1 -0
  46. package/build/src/client/hooks.test.js +163 -0
  47. package/build/src/client/hooks.test.js.map +1 -0
  48. package/build/src/client/index.d.ts +22 -0
  49. package/build/src/client/index.js +67 -0
  50. package/build/src/client/index.js.map +1 -0
  51. package/build/src/client/index.test.d.ts +1 -0
  52. package/build/src/client/index.test.js +231 -0
  53. package/build/src/client/index.test.js.map +1 -0
  54. package/build/src/client/request-builder.d.ts +13 -0
  55. package/build/src/client/request-builder.js +53 -0
  56. package/build/src/client/request-builder.js.map +1 -0
  57. package/build/src/client/request-builder.test.d.ts +1 -0
  58. package/build/src/client/request-builder.test.js +160 -0
  59. package/build/src/client/request-builder.test.js.map +1 -0
  60. package/build/src/client/stream.d.ts +27 -0
  61. package/build/src/client/stream.js +118 -0
  62. package/build/src/client/stream.js.map +1 -0
  63. package/build/src/client/stream.test.d.ts +1 -0
  64. package/build/src/client/stream.test.js +228 -0
  65. package/build/src/client/stream.test.js.map +1 -0
  66. package/build/src/client/types.d.ts +78 -0
  67. package/build/src/client/types.js +3 -0
  68. package/build/src/client/types.js.map +1 -0
  69. package/build/src/codegen/bin/cli.d.ts +45 -0
  70. package/build/src/codegen/bin/cli.js +246 -0
  71. package/build/src/codegen/bin/cli.js.map +1 -0
  72. package/build/src/codegen/bin/cli.test.d.ts +1 -0
  73. package/build/src/codegen/bin/cli.test.js +220 -0
  74. package/build/src/codegen/bin/cli.test.js.map +1 -0
  75. package/build/src/codegen/constants.d.ts +1 -0
  76. package/build/src/codegen/constants.js +2 -0
  77. package/build/src/codegen/constants.js.map +1 -0
  78. package/build/src/codegen/e2e.test.d.ts +1 -0
  79. package/build/src/codegen/e2e.test.js +464 -0
  80. package/build/src/codegen/e2e.test.js.map +1 -0
  81. package/build/src/codegen/emit-client-runtime.d.ts +9 -0
  82. package/build/src/codegen/emit-client-runtime.js +99 -0
  83. package/build/src/codegen/emit-client-runtime.js.map +1 -0
  84. package/build/src/codegen/emit-client-runtime.test.d.ts +1 -0
  85. package/build/src/codegen/emit-client-runtime.test.js +78 -0
  86. package/build/src/codegen/emit-client-runtime.test.js.map +1 -0
  87. package/build/src/codegen/emit-client-types.d.ts +8 -0
  88. package/build/src/codegen/emit-client-types.js +25 -0
  89. package/build/src/codegen/emit-client-types.js.map +1 -0
  90. package/build/src/codegen/emit-client-types.test.d.ts +1 -0
  91. package/build/src/codegen/emit-client-types.test.js +33 -0
  92. package/build/src/codegen/emit-client-types.test.js.map +1 -0
  93. package/build/src/codegen/emit-errors.d.ts +19 -0
  94. package/build/src/codegen/emit-errors.js +59 -0
  95. package/build/src/codegen/emit-errors.js.map +1 -0
  96. package/build/src/codegen/emit-errors.test.d.ts +1 -0
  97. package/build/src/codegen/emit-errors.test.js +175 -0
  98. package/build/src/codegen/emit-errors.test.js.map +1 -0
  99. package/build/src/codegen/emit-index.d.ts +12 -0
  100. package/build/src/codegen/emit-index.js +41 -0
  101. package/build/src/codegen/emit-index.js.map +1 -0
  102. package/build/src/codegen/emit-index.test.d.ts +1 -0
  103. package/build/src/codegen/emit-index.test.js +106 -0
  104. package/build/src/codegen/emit-index.test.js.map +1 -0
  105. package/build/src/codegen/emit-scope.d.ts +15 -0
  106. package/build/src/codegen/emit-scope.js +299 -0
  107. package/build/src/codegen/emit-scope.js.map +1 -0
  108. package/build/src/codegen/emit-scope.test.d.ts +1 -0
  109. package/build/src/codegen/emit-scope.test.js +559 -0
  110. package/build/src/codegen/emit-scope.test.js.map +1 -0
  111. package/build/src/codegen/emit-types.d.ts +43 -0
  112. package/build/src/codegen/emit-types.js +111 -0
  113. package/build/src/codegen/emit-types.js.map +1 -0
  114. package/build/src/codegen/emit-types.test.d.ts +1 -0
  115. package/build/src/codegen/emit-types.test.js +184 -0
  116. package/build/src/codegen/emit-types.test.js.map +1 -0
  117. package/build/src/codegen/group-routes.d.ts +23 -0
  118. package/build/src/codegen/group-routes.js +46 -0
  119. package/build/src/codegen/group-routes.js.map +1 -0
  120. package/build/src/codegen/group-routes.test.d.ts +1 -0
  121. package/build/src/codegen/group-routes.test.js +131 -0
  122. package/build/src/codegen/group-routes.test.js.map +1 -0
  123. package/build/src/codegen/index.d.ts +15 -0
  124. package/build/src/codegen/index.js +16 -0
  125. package/build/src/codegen/index.js.map +1 -0
  126. package/build/src/codegen/naming.d.ts +7 -0
  127. package/build/src/codegen/naming.js +21 -0
  128. package/build/src/codegen/naming.js.map +1 -0
  129. package/build/src/codegen/naming.test.d.ts +1 -0
  130. package/build/src/codegen/naming.test.js +40 -0
  131. package/build/src/codegen/naming.test.js.map +1 -0
  132. package/build/src/codegen/pipeline.d.ts +17 -0
  133. package/build/src/codegen/pipeline.js +78 -0
  134. package/build/src/codegen/pipeline.js.map +1 -0
  135. package/build/src/codegen/pipeline.test.d.ts +1 -0
  136. package/build/src/codegen/pipeline.test.js +269 -0
  137. package/build/src/codegen/pipeline.test.js.map +1 -0
  138. package/build/src/codegen/resolve-envelope.d.ts +7 -0
  139. package/build/src/codegen/resolve-envelope.js +46 -0
  140. package/build/src/codegen/resolve-envelope.js.map +1 -0
  141. package/build/src/codegen/resolve-envelope.test.d.ts +1 -0
  142. package/build/src/codegen/resolve-envelope.test.js +69 -0
  143. package/build/src/codegen/resolve-envelope.test.js.map +1 -0
  144. package/build/src/errors.d.ts +33 -0
  145. package/build/src/errors.js +91 -0
  146. package/build/src/errors.js.map +1 -0
  147. package/build/src/errors.test.d.ts +1 -0
  148. package/build/src/errors.test.js +122 -0
  149. package/build/src/errors.test.js.map +1 -0
  150. package/build/src/exports.d.ts +7 -0
  151. package/build/src/exports.js +8 -0
  152. package/build/src/exports.js.map +1 -0
  153. package/build/src/implementations/http/doc-registry.d.ts +12 -0
  154. package/build/src/implementations/http/doc-registry.js +114 -0
  155. package/build/src/implementations/http/doc-registry.js.map +1 -0
  156. package/build/src/implementations/http/doc-registry.test.d.ts +1 -0
  157. package/build/src/implementations/http/doc-registry.test.js +347 -0
  158. package/build/src/implementations/http/doc-registry.test.js.map +1 -0
  159. package/build/src/implementations/http/express-rpc/index.d.ts +94 -0
  160. package/build/src/implementations/http/express-rpc/index.js +185 -0
  161. package/build/src/implementations/http/express-rpc/index.js.map +1 -0
  162. package/build/src/implementations/http/express-rpc/index.test.d.ts +1 -0
  163. package/build/src/implementations/http/express-rpc/index.test.js +684 -0
  164. package/build/src/implementations/http/express-rpc/index.test.js.map +1 -0
  165. package/build/src/implementations/http/express-rpc/types.d.ts +11 -0
  166. package/build/src/implementations/http/express-rpc/types.js +2 -0
  167. package/build/src/implementations/http/express-rpc/types.js.map +1 -0
  168. package/build/src/implementations/http/hono-api/index.d.ts +102 -0
  169. package/build/src/implementations/http/hono-api/index.js +341 -0
  170. package/build/src/implementations/http/hono-api/index.js.map +1 -0
  171. package/build/src/implementations/http/hono-api/index.test.d.ts +1 -0
  172. package/build/src/implementations/http/hono-api/index.test.js +992 -0
  173. package/build/src/implementations/http/hono-api/index.test.js.map +1 -0
  174. package/build/src/implementations/http/hono-api/types.d.ts +13 -0
  175. package/build/src/implementations/http/hono-api/types.js +2 -0
  176. package/build/src/implementations/http/hono-api/types.js.map +1 -0
  177. package/build/src/implementations/http/hono-rpc/index.d.ts +92 -0
  178. package/build/src/implementations/http/hono-rpc/index.js +161 -0
  179. package/build/src/implementations/http/hono-rpc/index.js.map +1 -0
  180. package/build/src/implementations/http/hono-rpc/index.test.d.ts +1 -0
  181. package/build/src/implementations/http/hono-rpc/index.test.js +803 -0
  182. package/build/src/implementations/http/hono-rpc/index.test.js.map +1 -0
  183. package/build/src/implementations/http/hono-rpc/types.d.ts +11 -0
  184. package/build/src/implementations/http/hono-rpc/types.js +2 -0
  185. package/build/src/implementations/http/hono-rpc/types.js.map +1 -0
  186. package/build/src/implementations/http/hono-stream/index.d.ts +120 -0
  187. package/build/src/implementations/http/hono-stream/index.js +309 -0
  188. package/build/src/implementations/http/hono-stream/index.js.map +1 -0
  189. package/build/src/implementations/http/hono-stream/index.test.d.ts +1 -0
  190. package/build/src/implementations/http/hono-stream/index.test.js +1356 -0
  191. package/build/src/implementations/http/hono-stream/index.test.js.map +1 -0
  192. package/build/src/implementations/http/hono-stream/types.d.ts +15 -0
  193. package/build/src/implementations/http/hono-stream/types.js +2 -0
  194. package/build/src/implementations/http/hono-stream/types.js.map +1 -0
  195. package/build/src/implementations/types.d.ts +142 -0
  196. package/build/src/implementations/types.js +2 -0
  197. package/build/src/implementations/types.js.map +1 -0
  198. package/build/src/index.d.ts +165 -0
  199. package/build/src/index.js +253 -0
  200. package/build/src/index.js.map +1 -0
  201. package/build/src/index.test.d.ts +1 -0
  202. package/build/src/index.test.js +890 -0
  203. package/build/src/index.test.js.map +1 -0
  204. package/build/src/schema/compute-schema.d.ts +35 -0
  205. package/build/src/schema/compute-schema.js +41 -0
  206. package/build/src/schema/compute-schema.js.map +1 -0
  207. package/build/src/schema/compute-schema.test.d.ts +1 -0
  208. package/build/src/schema/compute-schema.test.js +107 -0
  209. package/build/src/schema/compute-schema.test.js.map +1 -0
  210. package/build/src/schema/extract-json-schema.d.ts +2 -0
  211. package/build/src/schema/extract-json-schema.js +12 -0
  212. package/build/src/schema/extract-json-schema.js.map +1 -0
  213. package/build/src/schema/extract-json-schema.test.d.ts +1 -0
  214. package/build/src/schema/extract-json-schema.test.js +23 -0
  215. package/build/src/schema/extract-json-schema.test.js.map +1 -0
  216. package/build/src/schema/parser.d.ts +28 -0
  217. package/build/src/schema/parser.js +170 -0
  218. package/build/src/schema/parser.js.map +1 -0
  219. package/build/src/schema/parser.test.d.ts +1 -0
  220. package/build/src/schema/parser.test.js +120 -0
  221. package/build/src/schema/parser.test.js.map +1 -0
  222. package/build/src/schema/resolve-schema-lib.d.ts +12 -0
  223. package/build/src/schema/resolve-schema-lib.js +11 -0
  224. package/build/src/schema/resolve-schema-lib.js.map +1 -0
  225. package/build/src/schema/resolve-schema-lib.test.d.ts +1 -0
  226. package/build/src/schema/resolve-schema-lib.test.js +17 -0
  227. package/build/src/schema/resolve-schema-lib.test.js.map +1 -0
  228. package/build/src/schema/types.d.ts +8 -0
  229. package/build/src/schema/types.js +2 -0
  230. package/build/src/schema/types.js.map +1 -0
  231. package/build/src/stack-utils.d.ts +25 -0
  232. package/build/src/stack-utils.js +95 -0
  233. package/build/src/stack-utils.js.map +1 -0
  234. package/build/src/stack-utils.test.d.ts +1 -0
  235. package/build/src/stack-utils.test.js +80 -0
  236. package/build/src/stack-utils.test.js.map +1 -0
  237. package/docs/ai-agent-setup.md +7 -6
  238. package/docs/client-and-codegen.md +9 -6
  239. package/package.json +1 -1
  240. package/src/codegen/e2e.test.ts +23 -14
  241. package/src/codegen/emit-index.test.ts +72 -45
  242. package/src/codegen/emit-index.ts +43 -20
  243. package/src/codegen/pipeline.test.ts +4 -4
  244. package/src/codegen/pipeline.ts +4 -5
  245. /package/agent_config/claude-code/skills/{guide → ts-procedures}/anti-patterns.md +0 -0
  246. /package/agent_config/claude-code/skills/{review → ts-procedures-review}/checklist.md +0 -0
  247. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/express-rpc.md +0 -0
  248. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-api.md +0 -0
  249. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-rpc.md +0 -0
  250. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/hono-stream.md +0 -0
  251. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/procedure.md +0 -0
  252. /package/agent_config/claude-code/skills/{scaffold → ts-procedures-scaffold}/templates/stream-procedure.md +0 -0
@@ -39,76 +39,103 @@ describe('emitIndexFile', () => {
39
39
  expect(output).toContain("import type { ClientInstance } from 'ts-procedures/client'")
40
40
  })
41
41
 
42
- it('imports each bind function using the camelCase scope name', () => {
42
+ it('imports each scope as a namespace using the camelCase scope name', () => {
43
43
  const output = emitIndexFile([usersGroup, billingGroup])
44
- expect(output).toContain("import { bindUsersScope } from './users'")
45
- expect(output).toContain("import { bindBillingScope } from './billing'")
44
+ expect(output).toContain("import * as users from './users'")
45
+ expect(output).toContain("import * as billing from './billing'")
46
46
  })
47
47
 
48
- it('re-exports from each scope file', () => {
48
+ it('does not emit `export *` re-exports', () => {
49
49
  const output = emitIndexFile([usersGroup, billingGroup])
50
- expect(output).toContain("export * from './users'")
51
- expect(output).toContain("export * from './billing'")
50
+ expect(output).not.toContain('export *')
52
51
  })
53
52
 
54
- it('generates createScopeBindings with correct scope property names', () => {
55
- const output = emitIndexFile([usersGroup, billingGroup])
56
- expect(output).toContain('export function createScopeBindings(client: ClientInstance)')
57
- expect(output).toContain('users: bindUsersScope(client)')
58
- expect(output).toContain('billing: bindBillingScope(client)')
53
+ it('uses the scopeKey for import paths and camelCase for the local alias', () => {
54
+ const output = emitIndexFile([adminUsersGroup])
55
+ expect(output).toContain("import * as adminUsers from './admin-users'")
59
56
  })
60
57
 
61
- it('uses camelCase as the property key in createScopeBindings', () => {
62
- const output = emitIndexFile([adminUsersGroup])
63
- expect(output).toContain('adminUsers: bindAdminUsersScope(client)')
58
+ it('generates the factory using the default service name (Api)', () => {
59
+ const output = emitIndexFile([usersGroup, billingGroup])
60
+ expect(output).toContain('export function createApiBindings(client: ClientInstance)')
61
+ expect(output).toContain('users: users.bindUsersScope(client)')
62
+ expect(output).toContain('billing: billing.bindBillingScope(client)')
64
63
  })
65
64
 
66
- it('uses the scopeKey as the import path (not camelCase)', () => {
65
+ it('uses camelCase as the binding property key', () => {
67
66
  const output = emitIndexFile([adminUsersGroup])
68
- expect(output).toContain("import { bindAdminUsersScope } from './admin-users'")
69
- expect(output).toContain("export * from './admin-users'")
67
+ expect(output).toContain('adminUsers: adminUsers.bindAdminUsersScope(client)')
70
68
  })
71
69
 
72
- it('handles a single scope group', () => {
73
- const output = emitIndexFile([usersGroup])
74
- expect(output).toContain('bindUsersScope')
75
- expect(output).toContain('users: bindUsersScope(client)')
70
+ it('places imports before the namespace block before the factory', () => {
71
+ const output = emitIndexFile([usersGroup, billingGroup], { namespaceTypes: true })
72
+ const importIdx = output.indexOf("import * as users")
73
+ const namespaceIdx = output.indexOf('export namespace Api')
74
+ const factoryIdx = output.indexOf('export function createApiBindings')
75
+ expect(importIdx).toBeLessThan(namespaceIdx)
76
+ expect(namespaceIdx).toBeLessThan(factoryIdx)
76
77
  })
77
78
 
78
- it('places imports before re-exports before createScopeBindings', () => {
79
- const output = emitIndexFile([usersGroup, billingGroup])
80
- const importIdx = output.indexOf("import { bindUsersScope }")
81
- const reExportIdx = output.indexOf("export * from './users'")
82
- const bindIdx = output.indexOf('export function createScopeBindings')
83
- expect(importIdx).toBeLessThan(reExportIdx)
84
- expect(reExportIdx).toBeLessThan(bindIdx)
85
- })
79
+ describe('service namespace (namespaceTypes: true)', () => {
80
+ it('emits a service namespace named after the default Api', () => {
81
+ const output = emitIndexFile([usersGroup, billingGroup], { namespaceTypes: true })
82
+ expect(output).toContain('export namespace Api {')
83
+ expect(output).toContain('export import Users = users.Users')
84
+ expect(output).toContain('export import Billing = billing.Billing')
85
+ })
86
86
 
87
- describe('serviceName', () => {
88
- it('uses createScopeBindings when serviceName is not provided', () => {
89
- const output = emitIndexFile([usersGroup])
90
- expect(output).toContain('export function createScopeBindings(client: ClientInstance)')
87
+ it('emits a service namespace named after a provided serviceName', () => {
88
+ const output = emitIndexFile([usersGroup], { namespaceTypes: true, serviceName: 'UsersApi' })
89
+ expect(output).toContain('export namespace UsersApi {')
90
+ expect(output).toContain('export import Users = users.Users')
91
+ expect(output).toContain('export function createUsersApiBindings(client: ClientInstance)')
91
92
  })
92
93
 
93
- it('uses create${ServiceName}Bindings when serviceName is provided', () => {
94
- const output = emitIndexFile([usersGroup], { serviceName: 'Auth' })
95
- expect(output).toContain('export function createAuthBindings(client: ClientInstance)')
96
- expect(output).not.toContain('createScopeBindings')
94
+ it('PascalCases each scope when re-exporting', () => {
95
+ const output = emitIndexFile([adminUsersGroup], { namespaceTypes: true })
96
+ expect(output).toContain('export import AdminUsers = adminUsers.AdminUsers')
97
97
  })
98
98
 
99
- it('PascalCases a kebab-case serviceName', () => {
100
- const output = emitIndexFile([usersGroup], { serviceName: 'auth-service' })
99
+ it('PascalCases a kebab-case serviceName for both namespace and factory', () => {
100
+ const output = emitIndexFile([usersGroup], { namespaceTypes: true, serviceName: 'auth-service' })
101
+ expect(output).toContain('export namespace AuthService {')
101
102
  expect(output).toContain('export function createAuthServiceBindings(client: ClientInstance)')
102
103
  })
103
104
 
104
- it('PascalCases an underscore-separated serviceName', () => {
105
- const output = emitIndexFile([usersGroup], { serviceName: 'users_api' })
106
- expect(output).toContain('export function createUsersApiBindings(client: ClientInstance)')
105
+ it('folds errors into the service namespace as `Errors` when hasErrors is true', () => {
106
+ const output = emitIndexFile([usersGroup], {
107
+ namespaceTypes: true,
108
+ hasErrors: true,
109
+ serviceName: 'UsersApi',
110
+ })
111
+ expect(output).toContain("import * as _errorsModule from './_errors'")
112
+ expect(output).toContain('export import Errors = _errorsModule.UsersApiErrors')
107
113
  })
108
114
 
109
- it('preserves already-PascalCased serviceName', () => {
110
- const output = emitIndexFile([usersGroup, billingGroup], { serviceName: 'UsersApi' })
111
- expect(output).toContain('export function createUsersApiBindings(client: ClientInstance)')
115
+ it('omits the errors import and re-export when hasErrors is false', () => {
116
+ const output = emitIndexFile([usersGroup], { namespaceTypes: true, hasErrors: false })
117
+ expect(output).not.toContain("from './_errors'")
118
+ expect(output).not.toContain('Errors')
119
+ })
120
+ })
121
+
122
+ describe('namespaceTypes: false', () => {
123
+ it('skips the service namespace block entirely', () => {
124
+ const output = emitIndexFile([usersGroup, billingGroup])
125
+ expect(output).not.toContain('export namespace')
126
+ expect(output).not.toContain('export import')
127
+ })
128
+
129
+ it('still imports scopes and emits the factory', () => {
130
+ const output = emitIndexFile([usersGroup, billingGroup])
131
+ expect(output).toContain("import * as users from './users'")
132
+ expect(output).toContain('export function createApiBindings(client: ClientInstance)')
133
+ expect(output).toContain('users: users.bindUsersScope(client)')
134
+ })
135
+
136
+ it('does not import errors even when hasErrors is true', () => {
137
+ const output = emitIndexFile([usersGroup], { hasErrors: true })
138
+ expect(output).not.toContain("from './_errors'")
112
139
  })
113
140
  })
114
141
 
@@ -2,10 +2,7 @@ import type { ScopeGroup } from './group-routes.js'
2
2
  import { CODEGEN_HEADER } from './constants.js'
3
3
  import { toPascalCase } from './naming.js'
4
4
 
5
- /** Derives the bind function name from a camelCase scope identifier. */
6
- function bindFunctionName(camelCase: string): string {
7
- return `bind${toPascalCase(camelCase)}Scope`
8
- }
5
+ const ERRORS_MODULE_ALIAS = '_errorsModule'
9
6
 
10
7
  // ---------------------------------------------------------------------------
11
8
  // emitIndexFile
@@ -14,40 +11,66 @@ function bindFunctionName(camelCase: string): string {
14
11
  export interface EmitIndexOptions {
15
12
  clientImportPath?: string
16
13
  hasErrors?: boolean
14
+ namespaceTypes?: boolean
17
15
  serviceName?: string
18
16
  }
19
17
 
20
18
  /**
21
- * Generates a barrel index file that re-exports all scope files and provides
22
- * a factory function (`createScopeBindings`, or `create${ServiceName}Bindings`
23
- * when `serviceName` is set).
19
+ * Generates a barrel index file that exposes every scope through a single
20
+ * service namespace (named after `serviceName`, defaulting to `Api`) and a
21
+ * `create${ServiceName}Bindings` factory.
22
+ *
23
+ * The service namespace is only emitted when `namespaceTypes` is on, since
24
+ * scope files only export top-level namespaces in that mode.
24
25
  */
25
26
  export function emitIndexFile(groups: ScopeGroup[], options?: EmitIndexOptions): string {
26
- const { clientImportPath = 'ts-procedures/client', hasErrors = false, serviceName } = options ?? {}
27
- const factoryName = serviceName ? `create${toPascalCase(serviceName)}Bindings` : 'createScopeBindings'
28
- const bindImports = groups
29
- .map((g) => `import { ${bindFunctionName(g.camelCase)} } from './${g.scopeKey}'`)
30
- .join('\n')
27
+ const {
28
+ clientImportPath = 'ts-procedures/client',
29
+ hasErrors = false,
30
+ namespaceTypes = false,
31
+ serviceName = 'Api',
32
+ } = options ?? {}
33
+
34
+ const servicePascal = toPascalCase(serviceName)
35
+ const factoryName = `create${servicePascal}Bindings`
36
+ const errorsExportedName = `${servicePascal}Errors`
31
37
 
32
- const scopeReExports = groups
33
- .map((g) => `export * from './${g.scopeKey}'`)
38
+ const scopeImports = groups
39
+ .map((g) => `import * as ${g.camelCase} from './${g.scopeKey}'`)
34
40
  .join('\n')
35
41
 
36
- const errorsReExport = hasErrors ? `export * from './_errors'` : ''
42
+ const errorsImport = namespaceTypes && hasErrors
43
+ ? `import * as ${ERRORS_MODULE_ALIAS} from './_errors'`
44
+ : ''
37
45
 
38
- const reExports = [scopeReExports, errorsReExport].filter(Boolean).join('\n')
46
+ const importsBlock = [scopeImports, errorsImport].filter(Boolean).join('\n')
47
+
48
+ let namespaceBlock = ''
49
+ if (namespaceTypes) {
50
+ const namespaceMembers = groups.map(
51
+ (g) => ` export import ${toPascalCase(g.camelCase)} = ${g.camelCase}.${toPascalCase(g.camelCase)}`,
52
+ )
53
+ if (hasErrors) {
54
+ namespaceMembers.push(` export import Errors = ${ERRORS_MODULE_ALIAS}.${errorsExportedName}`)
55
+ }
56
+ namespaceBlock = [
57
+ `export namespace ${servicePascal} {`,
58
+ namespaceMembers.join('\n'),
59
+ '}',
60
+ '',
61
+ ].join('\n')
62
+ }
39
63
 
40
64
  const scopeBindings = groups
41
- .map((g) => ` ${g.camelCase}: ${bindFunctionName(g.camelCase)}(client),`)
65
+ .map((g) => ` ${g.camelCase}: ${g.camelCase}.bind${toPascalCase(g.camelCase)}Scope(client),`)
42
66
  .join('\n')
43
67
 
44
68
  return [
45
69
  CODEGEN_HEADER,
46
70
  `import type { ClientInstance } from '${clientImportPath}'`,
47
- bindImports,
48
- '',
49
- reExports,
71
+ importsBlock,
50
72
  '',
73
+ namespaceBlock,
51
74
  `export function ${factoryName}(client: ClientInstance) {`,
52
75
  ' return {',
53
76
  scopeBindings,
@@ -114,14 +114,14 @@ describe('generateClient pipeline', () => {
114
114
  expect(content).toContain('CreateInvoice')
115
115
  })
116
116
 
117
- it('index.ts exports from both scope files and has createScopeBindings', async () => {
117
+ it('index.ts imports both scope namespaces and emits the default Api factory', async () => {
118
118
  tmpDir = makeTmpDir()
119
119
  await generateClient({ envelope, outDir: tmpDir })
120
120
 
121
121
  const content = readFileSync(join(tmpDir, 'index.ts'), 'utf-8')
122
- expect(content).toContain("from './users'")
123
- expect(content).toContain("from './billing'")
124
- expect(content).toContain('createScopeBindings')
122
+ expect(content).toContain("import * as users from './users'")
123
+ expect(content).toContain("import * as billing from './billing'")
124
+ expect(content).toContain('createApiBindings')
125
125
  })
126
126
 
127
127
  it('creates outDir if it does not exist', async () => {
@@ -28,10 +28,9 @@ export interface GeneratedFile {
28
28
  }
29
29
 
30
30
  export async function runPipeline(options: PipelineOptions): Promise<GeneratedFile[]> {
31
- const { envelope, outDir, ajsc: ajscOpts, dryRun = false, namespaceTypes = false, selfContained = false, serviceName } = options
32
- if (serviceName != null) {
33
- validateServiceName(serviceName)
34
- }
31
+ const { envelope, outDir, ajsc: ajscOpts, dryRun = false, namespaceTypes = false, selfContained = false } = options
32
+ const serviceName = options.serviceName ?? 'Api'
33
+ validateServiceName(serviceName)
35
34
  const clientImportPath = selfContained ? './_types' : options.clientImportPath
36
35
  if (selfContained && options.clientImportPath != null) {
37
36
  console.warn('[ts-procedures-codegen] --self-contained overrides --client-import-path; using ./_types')
@@ -72,7 +71,7 @@ export async function runPipeline(options: PipelineOptions): Promise<GeneratedFi
72
71
  files.push({ path: join(outDir, '_errors.ts'), code: errorsWithHash })
73
72
  }
74
73
 
75
- const rawIndexCode = emitIndexFile(groupArray, { clientImportPath, hasErrors, serviceName })
74
+ const rawIndexCode = emitIndexFile(groupArray, { clientImportPath, hasErrors, namespaceTypes, serviceName })
76
75
  const indexLines = rawIndexCode.split('\n')
77
76
  indexLines.splice(1, 0, hashComment)
78
77
  const indexCode = indexLines.join('\n')