ts-procedures 5.7.2 → 5.9.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 (50) hide show
  1. package/README.md +7 -1051
  2. package/agent_config/claude-code/skills/guide/api-reference.md +21 -16
  3. package/agent_config/claude-code/skills/guide/patterns.md +3 -1
  4. package/agent_config/copilot/copilot-instructions.md +7 -5
  5. package/agent_config/cursor/cursorrules +7 -5
  6. package/build/codegen/bin/cli.d.ts +2 -0
  7. package/build/codegen/bin/cli.js +21 -10
  8. package/build/codegen/bin/cli.js.map +1 -1
  9. package/build/codegen/bin/cli.test.js +44 -2
  10. package/build/codegen/bin/cli.test.js.map +1 -1
  11. package/build/codegen/emit-errors.d.ts +4 -1
  12. package/build/codegen/emit-errors.js +11 -5
  13. package/build/codegen/emit-errors.js.map +1 -1
  14. package/build/codegen/emit-errors.test.js +37 -0
  15. package/build/codegen/emit-errors.test.js.map +1 -1
  16. package/build/codegen/emit-index.d.ts +3 -1
  17. package/build/codegen/emit-index.js +6 -13
  18. package/build/codegen/emit-index.js.map +1 -1
  19. package/build/codegen/emit-index.test.js +23 -0
  20. package/build/codegen/emit-index.test.js.map +1 -1
  21. package/build/codegen/emit-scope.js +17 -13
  22. package/build/codegen/emit-scope.js.map +1 -1
  23. package/build/codegen/emit-scope.test.js +166 -0
  24. package/build/codegen/emit-scope.test.js.map +1 -1
  25. package/build/codegen/index.d.ts +1 -0
  26. package/build/codegen/index.js +1 -0
  27. package/build/codegen/index.js.map +1 -1
  28. package/build/codegen/naming.d.ts +7 -0
  29. package/build/codegen/naming.js +21 -0
  30. package/build/codegen/naming.js.map +1 -0
  31. package/build/codegen/naming.test.d.ts +1 -0
  32. package/build/codegen/naming.test.js +40 -0
  33. package/build/codegen/naming.test.js.map +1 -0
  34. package/build/codegen/pipeline.d.ts +1 -0
  35. package/build/codegen/pipeline.js +7 -3
  36. package/build/codegen/pipeline.js.map +1 -1
  37. package/build/codegen/pipeline.test.js +60 -0
  38. package/build/codegen/pipeline.test.js.map +1 -1
  39. package/docs/ai-agent-setup.md +61 -0
  40. package/docs/client-and-codegen.md +193 -0
  41. package/docs/core.md +473 -0
  42. package/docs/http-integrations.md +183 -0
  43. package/docs/streaming.md +199 -0
  44. package/docs/superpowers/plans/2026-03-30-client-codegen.md +2833 -0
  45. package/docs/superpowers/specs/2026-03-30-client-codegen-design.md +632 -0
  46. package/package.json +6 -1
  47. package/src/implementations/http/README.md +324 -0
  48. package/src/implementations/http/express-rpc/README.md +281 -0
  49. package/src/implementations/http/hono-rpc/README.md +358 -0
  50. package/src/implementations/http/hono-stream/README.md +525 -0
@@ -0,0 +1,358 @@
1
+ # HonoRPCAppBuilder
2
+
3
+ Hono integration for `ts-procedures` that creates type-safe RPC endpoints as POST routes. Works with Bun, Deno, Cloudflare Workers, and Node.js.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install ts-procedures hono
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { Hono } from 'hono'
15
+ import { Procedures } from 'ts-procedures'
16
+ import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
17
+ import { v } from 'suretype'
18
+
19
+ // Define your context type
20
+ type AppContext = { userId: string }
21
+
22
+ // Create a procedure factory
23
+ const RPC = Procedures<AppContext, RPCConfig>()
24
+
25
+ // Define procedures
26
+ RPC.Create(
27
+ 'GetUser',
28
+ {
29
+ scope: ['users', 'profile'],
30
+ version: 1,
31
+ schema: {
32
+ params: v.object({ id: v.string() }),
33
+ returnType: v.object({ id: v.string(), name: v.string() }),
34
+ },
35
+ },
36
+ async (ctx, params) => {
37
+ return { id: params.id, name: 'John Doe' }
38
+ }
39
+ )
40
+
41
+ // Build the Hono app
42
+ const builder = new HonoRPCAppBuilder({ pathPrefix: '/rpc' }).register(RPC, (c) => ({
43
+ userId: c.req.header('x-user-id') || 'anonymous',
44
+ }))
45
+
46
+ const app = builder.build()
47
+
48
+ // Bun
49
+ export default app
50
+
51
+ // Node.js
52
+ // import { serve } from '@hono/node-server'
53
+ // serve(app)
54
+
55
+ // POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
56
+ ```
57
+
58
+ ## Configuration
59
+
60
+ ```typescript
61
+ type HonoRPCAppBuilderConfig = {
62
+ app?: Hono // Existing Hono app (optional)
63
+ pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
64
+ onRequestStart?: (c: Context) => void
65
+ onRequestEnd?: (c: Context) => void
66
+ onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
67
+ error?: (
68
+ procedure: TProcedureRegistration,
69
+ c: Context,
70
+ error: Error
71
+ ) => Response | Promise<Response>
72
+ }
73
+ ```
74
+
75
+ | Option | Type | Description |
76
+ | ---------------- | ---------------------------- | ------------------------------------------------- |
77
+ | `app` | `Hono` | Use existing Hono app instead of creating new one |
78
+ | `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
79
+ | `onRequestStart` | `(c) => void` | Called at start of each request |
80
+ | `onRequestEnd` | `(c) => void` | Called after handler completes |
81
+ | `onSuccess` | `(proc, c) => void` | Called on successful handler execution |
82
+ | `error` | `(proc, c, err) => Response` | Custom error handler (must return Response) |
83
+
84
+ ## Context Resolution
85
+
86
+ The context resolver receives the Hono `Context` object:
87
+
88
+ ```typescript
89
+ builder.register(RPC, (c: Context) => ({
90
+ userId: c.req.header('x-user-id') || 'anonymous',
91
+ userAgent: c.req.header('user-agent'),
92
+ ip: c.req.raw.headers.get('cf-connecting-ip'), // Cloudflare
93
+ }))
94
+
95
+ // Async context resolution
96
+ builder.register(RPC, async (c) => {
97
+ const token = c.req.header('authorization')?.replace('Bearer ', '')
98
+ const user = await verifyToken(token)
99
+ return { userId: user.id, roles: user.roles }
100
+ })
101
+ ```
102
+
103
+ ## Extending Procedure Documentation
104
+
105
+ The `register` method accepts an optional third parameter `extendProcedureDoc` that allows you to add custom fields to each procedure's documentation. This is useful for adding metadata like descriptions, tags, or custom fields for API documentation generators.
106
+
107
+ ```typescript
108
+ // Example of a factory extending the procedure config:
109
+ type ExtendedRPCConfig = {
110
+ description: string
111
+ tags: string[]
112
+ }
113
+
114
+ builder.register(RPC, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }), {
115
+ extendProcedureDoc: ({ base, procedure }, { base: RPCHttpRouteDoc, procedure }) =>
116
+ ({
117
+ description: `Procedure: ${procedure.name}`,
118
+ tags: Array.isArray(procedure.config.scope)
119
+ ? procedure.config.scope
120
+ : [procedure.config.scope],
121
+ }),
122
+ })
123
+
124
+ // Access extended docs after build()
125
+ const app = builder.build()
126
+ console.log(builder.docs) // Each doc now includes description and tags
127
+ ```
128
+
129
+ The `extendProcedureDoc` callback receives:
130
+
131
+ | Parameter | Type | Description |
132
+ | ----------- | ------------------------ | ------------------------------------------------------------------------------------------ |
133
+ | `base` | `RPCHttpRouteDoc` | The base documentation with `name`, `path`, `method`, `scope`, `version`, and `jsonSchema` |
134
+ | `procedure` | `TProcedureRegistration` | The full procedure registration including `name`, `config`, and `handler` |
135
+
136
+ This allows you to derive documentation fields from procedure config or add static metadata per factory registration.
137
+
138
+ ## Abort Signal
139
+
140
+ `HonoRPCAppBuilder` automatically injects the HTTP request's `AbortSignal` (`c.req.raw.signal`) into the handler context. When a client disconnects mid-request, `ctx.signal` aborts, cancelling any signal-aware operations:
141
+
142
+ ```typescript
143
+ RPC.Create(
144
+ 'SlowQuery',
145
+ { scope: 'data', version: 1 },
146
+ async (ctx, params) => {
147
+ // Automatically cancelled if client disconnects
148
+ const response = await fetch('https://slow-api.example.com/data', {
149
+ signal: ctx.signal,
150
+ })
151
+ return response.json()
152
+ }
153
+ )
154
+ ```
155
+
156
+ To use `ctx.signal` with type safety, include `signal: AbortSignal` in your context type:
157
+
158
+ ```typescript
159
+ type AppContext = { userId: string; signal: AbortSignal }
160
+ const RPC = Procedures<AppContext, RPCConfig>()
161
+ ```
162
+
163
+ ## Error Handling
164
+
165
+ Custom error handler receives the procedure, context, and error. **Must return a Response:**
166
+
167
+ ```typescript
168
+ const builder = new HonoRPCAppBuilder({
169
+ onError: (procedure, c, error) => {
170
+ console.error(`Error in ${procedure.name}:`, error)
171
+
172
+ if (error instanceof ValidationError) {
173
+ return c.json({ error: error.message, code: 'VALIDATION_ERROR' }, 400)
174
+ }
175
+
176
+ if (error instanceof AuthError) {
177
+ return c.json({ error: 'Unauthorized', code: 'AUTH_ERROR' }, 401)
178
+ }
179
+
180
+ return c.json({ error: 'Internal server error' }, 500)
181
+ },
182
+ })
183
+ ```
184
+
185
+ **Default error handling:** Returns `{ error: message }` with status 500.
186
+
187
+ ## Using Existing Hono App
188
+
189
+ You can add RPC routes to an existing Hono application:
190
+
191
+ ```typescript
192
+ const app = new Hono()
193
+
194
+ // Add custom middleware and routes
195
+ app.use('*', cors())
196
+ app.get('/custom', (c) => c.json({ custom: true }))
197
+
198
+ const builder = new HonoRPCAppBuilder({ app }).register(RPC, contextResolver)
199
+
200
+ builder.build() // Adds RPC routes to existing app
201
+ ```
202
+
203
+ ## Runtime Compatibility
204
+
205
+ HonoRPCAppBuilder works across all Hono-supported runtimes:
206
+
207
+ ### Bun
208
+
209
+ ```typescript
210
+ const app = builder.build()
211
+ export default app
212
+ ```
213
+
214
+ ### Node.js
215
+
216
+ ```typescript
217
+ import { serve } from '@hono/node-server'
218
+
219
+ const app = builder.build()
220
+ serve(app)
221
+ ```
222
+
223
+ ### Deno
224
+
225
+ ```typescript
226
+ import { serve } from 'https://deno.land/std/http/server.ts'
227
+
228
+ const app = builder.build()
229
+ serve(app.fetch)
230
+ ```
231
+
232
+ ### Cloudflare Workers
233
+
234
+ ```typescript
235
+ const app = builder.build()
236
+ export default app
237
+ ```
238
+
239
+ ## API Reference
240
+
241
+ ### Constructor
242
+
243
+ ```typescript
244
+ new HonoRPCAppBuilder(config?: HonoRPCAppBuilderConfig)
245
+ ```
246
+
247
+ ### Methods
248
+
249
+ | Method | Signature | Description |
250
+ | ---------------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
251
+ | `register` | `register<T>(factory, context, options?): this` | Register procedure factory with context and optional doc extension |
252
+ | `build` | `build(): Hono` | Build routes and return app |
253
+ | `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
254
+
255
+ ### Static Methods
256
+
257
+ | Method | Signature | Description |
258
+ | ---------------------- | --------------------------------------------------------- | -------------------------------------- |
259
+ | `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
260
+
261
+ ### Properties
262
+
263
+ | Property | Type | Description |
264
+ | -------- | ------------------------- | ------------------------------------- |
265
+ | `app` | `Hono` | The Hono application instance |
266
+ | `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
267
+ | `config` | `HonoRPCAppBuilderConfig` | The configuration object |
268
+
269
+ ## TypeScript Types
270
+
271
+ ```typescript
272
+ import {
273
+ HonoRPCAppBuilder,
274
+ HonoRPCAppBuilderConfig,
275
+ RPCConfig,
276
+ RPCHttpRouteDoc,
277
+ } from 'ts-procedures/hono-rpc'
278
+ ```
279
+
280
+ ## Full Example
281
+
282
+ ```typescript
283
+ import { Hono, Context } from 'hono'
284
+ import { Procedures } from 'ts-procedures'
285
+ import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
286
+ import { v } from 'suretype'
287
+
288
+ // Context types
289
+ type PublicContext = { source: 'public' }
290
+ type AuthContext = { source: 'auth'; userId: string }
291
+
292
+ // Create factories
293
+ const PublicRPC = Procedures<PublicContext, RPCConfig>()
294
+ const AuthRPC = Procedures<AuthContext, RPCConfig>()
295
+
296
+ // Public procedures
297
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
298
+ status: 'ok',
299
+ }))
300
+
301
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
302
+ version: '1.0.0',
303
+ }))
304
+
305
+ // Authenticated procedures
306
+ AuthRPC.Create(
307
+ 'GetProfile',
308
+ {
309
+ scope: ['users', 'profile'],
310
+ version: 1,
311
+ schema: { returnType: v.object({ userId: v.string() }) },
312
+ },
313
+ async (ctx) => ({ userId: ctx.userId })
314
+ )
315
+
316
+ AuthRPC.Create(
317
+ 'UpdateProfile',
318
+ {
319
+ scope: ['users', 'profile'],
320
+ version: 2,
321
+ schema: { params: v.object({ name: v.string() }) },
322
+ },
323
+ async (ctx, params) => ({ userId: ctx.userId, name: params.name })
324
+ )
325
+
326
+ // Build app
327
+ const builder = new HonoRPCAppBuilder({
328
+ pathPrefix: '/rpc',
329
+ onRequestStart: (c) => console.log(`→ ${c.req.method} ${c.req.path}`),
330
+ onRequestEnd: (c) => console.log(`← completed`),
331
+ onSuccess: (proc) => console.log(`✓ ${proc.name}`),
332
+ onError: (proc, c, err) => {
333
+ console.error(`✗ ${proc.name}:`, err.message)
334
+ return c.json({ error: err.message }, 500)
335
+ },
336
+ })
337
+
338
+ builder
339
+ .register(PublicRPC, () => ({ source: 'public' as const }))
340
+ .register(AuthRPC, (c) => ({
341
+ source: 'auth' as const,
342
+ userId: c.req.header('x-user-id') || 'anonymous',
343
+ }))
344
+
345
+ const app = builder.build()
346
+
347
+ // Generated routes:
348
+ // POST /rpc/health/1
349
+ // POST /rpc/system/version/get-version/1
350
+ // POST /rpc/users/profile/get-user/1
351
+ // POST /rpc/users/profile/get-user/2
352
+
353
+ console.log(
354
+ 'Routes:',
355
+ builder.docs.map((d) => d.path)
356
+ )
357
+ export default app
358
+ ```