ts-procedures 5.3.0 → 5.4.1

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 (60) hide show
  1. package/README.md +90 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
  4. package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
  5. package/agent_config/claude-code/skills/guide/patterns.md +155 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +22 -0
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
  8. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  9. package/agent_config/copilot/copilot-instructions.md +35 -0
  10. package/agent_config/cursor/cursorrules +35 -0
  11. package/build/implementations/http/hono-api/index.d.ts +102 -0
  12. package/build/implementations/http/hono-api/index.js +339 -0
  13. package/build/implementations/http/hono-api/index.js.map +1 -0
  14. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  15. package/build/implementations/http/hono-api/index.test.js +983 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  17. package/build/implementations/http/hono-api/types.d.ts +13 -0
  18. package/build/implementations/http/hono-api/types.js +2 -0
  19. package/build/implementations/http/hono-api/types.js.map +1 -0
  20. package/build/implementations/types.d.ts +44 -0
  21. package/build/index.d.ts +28 -6
  22. package/build/index.js +28 -0
  23. package/build/index.js.map +1 -1
  24. package/build/schema/compute-schema.d.ts +5 -0
  25. package/build/schema/compute-schema.js +8 -1
  26. package/build/schema/compute-schema.js.map +1 -1
  27. package/build/schema/parser.d.ts +6 -5
  28. package/build/schema/parser.js +54 -0
  29. package/build/schema/parser.js.map +1 -1
  30. package/package.json +8 -4
  31. package/src/errors.test.ts +0 -163
  32. package/src/errors.ts +0 -107
  33. package/src/exports.ts +0 -7
  34. package/src/implementations/http/README.md +0 -217
  35. package/src/implementations/http/express-rpc/README.md +0 -281
  36. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  37. package/src/implementations/http/express-rpc/index.ts +0 -265
  38. package/src/implementations/http/express-rpc/types.ts +0 -16
  39. package/src/implementations/http/hono-rpc/README.md +0 -358
  40. package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
  41. package/src/implementations/http/hono-rpc/index.ts +0 -237
  42. package/src/implementations/http/hono-rpc/types.ts +0 -16
  43. package/src/implementations/http/hono-stream/README.md +0 -526
  44. package/src/implementations/http/hono-stream/index.test.ts +0 -1676
  45. package/src/implementations/http/hono-stream/index.ts +0 -435
  46. package/src/implementations/http/hono-stream/types.ts +0 -29
  47. package/src/implementations/types.ts +0 -75
  48. package/src/index.test.ts +0 -1194
  49. package/src/index.ts +0 -435
  50. package/src/schema/compute-schema.test.ts +0 -128
  51. package/src/schema/compute-schema.ts +0 -67
  52. package/src/schema/extract-json-schema.test.ts +0 -25
  53. package/src/schema/extract-json-schema.ts +0 -15
  54. package/src/schema/parser.test.ts +0 -182
  55. package/src/schema/parser.ts +0 -148
  56. package/src/schema/resolve-schema-lib.test.ts +0 -19
  57. package/src/schema/resolve-schema-lib.ts +0 -29
  58. package/src/schema/types.ts +0 -20
  59. package/src/stack-utils.test.ts +0 -94
  60. package/src/stack-utils.ts +0 -129
@@ -1,435 +0,0 @@
1
- import { Hono, Context } from 'hono'
2
- import { streamSSE, streamText } from 'hono/streaming'
3
- import { kebabCase } from 'es-toolkit/string'
4
- import { castArray } from 'es-toolkit/compat'
5
- import { TStreamProcedureRegistration } from '../../../index.js'
6
- import { ExtractConfig, ExtractContext, ProceduresFactory, RPCConfig } from '../../types.js'
7
- import { HonoStreamFactoryItem, StreamHttpRouteDoc, StreamMode } from './types.js'
8
- import { ProcedureValidationError } from '../../../errors.js'
9
-
10
- export type { StreamHttpRouteDoc, StreamMode }
11
-
12
- export type SSEOptions = {
13
- event?: string
14
- id?: string
15
- retry?: number
16
- }
17
-
18
- const sseMetadata = new WeakMap<object, SSEOptions>()
19
-
20
- export function sse<T extends object>(data: T, options?: SSEOptions): T {
21
- sseMetadata.set(data, options ?? {})
22
- return data
23
- }
24
-
25
- function getSSEMeta(value: unknown): SSEOptions | undefined {
26
- if (typeof value === 'object' && value !== null) {
27
- return sseMetadata.get(value)
28
- }
29
- return undefined
30
- }
31
-
32
- /**
33
- * Result from onMidStreamError callback.
34
- * @property data - The data to write as the SSE `data:` field content (should match yieldType schema)
35
- * @property closeStream - Whether to close the stream after writing (defaults to true)
36
- */
37
- export type MidStreamErrorResult<TErrorData = unknown> = {
38
- data: TErrorData
39
- closeStream?: boolean
40
- }
41
-
42
- export type HonoStreamAppBuilderConfig<TErrorData = unknown> = {
43
- /**
44
- * An existing Hono application instance to use.
45
- * If not provided, a new instance will be created.
46
- */
47
- app?: Hono
48
- /** Optional path prefix for all stream routes. */
49
- pathPrefix?: string
50
- /** Default stream mode for all routes. Defaults to 'sse'. */
51
- defaultStreamMode?: StreamMode
52
- onRequestStart?: (c: Context) => void
53
- onRequestEnd?: (c: Context) => void
54
- onStreamStart?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
55
- onStreamEnd?: (procedure: TStreamProcedureRegistration, c: Context, streamMode: StreamMode) => void
56
- /**
57
- * Called for errors BEFORE streaming starts (validation, auth, context resolution).
58
- * Return value IS used as the HTTP response.
59
- */
60
- onPreStreamError?: (
61
- procedure: TStreamProcedureRegistration,
62
- c: Context,
63
- error: ProcedureValidationError | Error
64
- ) => Response | Promise<Response>
65
- /**
66
- * Called for errors DURING streaming (generator throws).
67
- * Return value is written to the stream as a yield.
68
- * Should return a value matching your yieldType schema (e.g., error variant of a union).
69
- * Return undefined to use default behavior (writes { error: message }).
70
- *
71
- * Use sse() to attach SSE metadata (event, id, retry) to the error data object.
72
- *
73
- * @returns { data, closeStream? } - data to yield, whether to close after (default true)
74
- */
75
- onMidStreamError?: (
76
- procedure: TStreamProcedureRegistration,
77
- c: Context,
78
- error: Error
79
- ) => MidStreamErrorResult<TErrorData> | undefined
80
- }
81
-
82
- /**
83
- * Builder class for creating a Hono application with streaming RPC routes.
84
- *
85
- * Usage:
86
- * const StreamRPC = Procedures<StreamContext, RPCConfig>()
87
- *
88
- * const streamApp = new HonoStreamAppBuilder()
89
- * .register(StreamRPC, (c): Promise<StreamContext> => { /* context resolution logic * / })
90
- * .build();
91
- *
92
- * const app = streamApp.app; // Hono application
93
- * const docs = streamApp.docs; // Stream route documentation
94
- */
95
- export class HonoStreamAppBuilder<TErrorData = unknown> {
96
- /**
97
- * Constructor for HonoStreamAppBuilder.
98
- */
99
- constructor(readonly config?: HonoStreamAppBuilderConfig<TErrorData>) {
100
- if (config?.app) {
101
- this._app = config.app
102
- }
103
-
104
- if (config?.onRequestStart) {
105
- this._app.use('*', async (c, next) => {
106
- config.onRequestStart!(c)
107
- await next()
108
- })
109
- }
110
- }
111
-
112
- /**
113
- * Generates the stream route path based on the RPC configuration.
114
- */
115
- static makeStreamHttpRoutePath({
116
- name,
117
- config,
118
- prefix,
119
- }: {
120
- name: string
121
- prefix?: string
122
- config: RPCConfig
123
- }) {
124
- const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
125
-
126
- return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
127
- }
128
-
129
- /**
130
- * Instance method wrapper for makeStreamHttpRoutePath that uses the builder's pathPrefix.
131
- */
132
- makeStreamHttpRoutePath(name: string, config: RPCConfig): string {
133
- return HonoStreamAppBuilder.makeStreamHttpRoutePath({
134
- name,
135
- config,
136
- prefix: this.config?.pathPrefix,
137
- })
138
- }
139
-
140
- private factories: HonoStreamFactoryItem<any>[] = []
141
-
142
- private _app: Hono = new Hono()
143
- private _docs: (StreamHttpRouteDoc & object)[] = []
144
-
145
- get app(): Hono {
146
- return this._app
147
- }
148
-
149
- get docs(): StreamHttpRouteDoc[] {
150
- return this._docs
151
- }
152
-
153
- /**
154
- * Registers a procedure factory with its context.
155
- * Only streaming procedures (created with CreateStream) will be registered.
156
- */
157
- register<TFactory extends ProceduresFactory>(
158
- factory: TFactory,
159
- factoryContext:
160
- | ExtractContext<TFactory>
161
- | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
162
- options?: {
163
- streamMode?: StreamMode
164
- extendProcedureDoc?: (params: {
165
- base: StreamHttpRouteDoc
166
- procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
167
- }) => Record<string, any>
168
- }
169
- ): this {
170
- this.factories.push({
171
- factory,
172
- factoryContext,
173
- streamMode: options?.streamMode,
174
- extendProcedureDoc: options?.extendProcedureDoc,
175
- } as HonoStreamFactoryItem<any>)
176
- return this
177
- }
178
-
179
- /**
180
- * Creates a route handler for streaming procedures.
181
- */
182
- private createStreamHandler(
183
- procedure: TStreamProcedureRegistration,
184
- factoryContext: HonoStreamFactoryItem['factoryContext'],
185
- streamMode: StreamMode
186
- ) {
187
- return async (c: Context) => {
188
- try {
189
- const context =
190
- typeof factoryContext === 'function' ? await factoryContext(c) : factoryContext
191
-
192
- // GET: query params, POST: JSON body
193
- const params =
194
- c.req.method === 'GET'
195
- ? Object.fromEntries(new URL(c.req.url).searchParams)
196
- : await c.req.json().catch(() => ({}))
197
-
198
- // Validate params BEFORE starting the stream
199
- if (procedure.config.validation?.params) {
200
- const { errors } = procedure.config.validation.params(params)
201
- if (errors) {
202
- const error = new ProcedureValidationError(
203
- procedure.name,
204
- `Validation error for ${procedure.name}`,
205
- errors
206
- )
207
- // Use onPreStreamError if provided
208
- if (this.config?.onPreStreamError) {
209
- return this.config.onPreStreamError(procedure, c, error)
210
- }
211
- return c.json({ error: error.message }, 400)
212
- }
213
- }
214
-
215
- if (this.config?.onStreamStart) {
216
- this.config.onStreamStart(procedure, c, streamMode)
217
- }
218
-
219
- if (streamMode === 'sse') {
220
- return this.handleSSEStream(procedure, context, params, c)
221
- } else {
222
- return this.handleTextStream(procedure, context, params, c)
223
- }
224
- } catch (error) {
225
- // Use onPreStreamError for context resolution errors
226
- if (this.config?.onPreStreamError) {
227
- return this.config.onPreStreamError(procedure, c, error as Error)
228
- }
229
- return c.json({ error: (error as Error).message }, 500)
230
- }
231
- }
232
- }
233
-
234
- /**
235
- * Handles SSE streaming mode.
236
- */
237
- private handleSSEStream(
238
- procedure: TStreamProcedureRegistration,
239
- context: any,
240
- params: any,
241
- c: Context
242
- ) {
243
- return streamSSE(c, async (stream) => {
244
- // Pass isPrevalidated: true since we already validated params in createStreamHandler
245
- const generator = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: true }, params)
246
-
247
- stream.onAbort(async () => {
248
- await generator.return(undefined)
249
- })
250
-
251
- let eventId = 0
252
- try {
253
- for await (const value of generator) {
254
- const currentId = eventId++
255
- const meta = getSSEMeta(value)
256
-
257
- const data =
258
- typeof value === 'string'
259
- ? value
260
- : value != null
261
- ? JSON.stringify(value)
262
- : ''
263
-
264
- await stream.writeSSE({
265
- data,
266
- event: meta?.event ?? procedure.name,
267
- id: meta?.id ?? String(currentId),
268
- ...(meta?.retry !== undefined && { retry: meta.retry }),
269
- })
270
- }
271
- } catch (error) {
272
- // Get error yield value from callback (onMidStreamError)
273
- let errorResult: MidStreamErrorResult<TErrorData> | undefined
274
-
275
- if (this.config?.onMidStreamError) {
276
- errorResult = this.config.onMidStreamError(procedure, c, error as Error)
277
- }
278
-
279
- // Write error value to stream
280
- const errorData = errorResult?.data ?? { error: (error as Error).message }
281
- const sseMeta = getSSEMeta(errorData)
282
-
283
- await stream.writeSSE({
284
- data: typeof errorData === 'string' ? errorData : JSON.stringify(errorData),
285
- event: sseMeta?.event ?? (errorResult?.data !== undefined ? procedure.name : 'error'),
286
- id: sseMeta?.id ?? String(eventId++),
287
- ...(sseMeta?.retry !== undefined && { retry: sseMeta.retry }),
288
- })
289
-
290
- // closeStream defaults to true if not specified
291
- // (stream closes naturally after this handler completes)
292
- } finally {
293
- if (this.config?.onStreamEnd) {
294
- this.config.onStreamEnd(procedure, c, 'sse')
295
- }
296
- if (this.config?.onRequestEnd) {
297
- this.config.onRequestEnd(c)
298
- }
299
- }
300
- })
301
- }
302
-
303
- /**
304
- * Handles text streaming mode.
305
- */
306
- private handleTextStream(
307
- procedure: TStreamProcedureRegistration,
308
- context: any,
309
- params: any,
310
- c: Context
311
- ) {
312
- return streamText(c, async (stream) => {
313
- // Pass isPrevalidated: true since we already validated params in createStreamHandler
314
- const generator = procedure.handler({ ...context, signal: c.req.raw.signal, isPrevalidated: true }, params)
315
-
316
- stream.onAbort(async () => {
317
- await generator.return(undefined)
318
- })
319
-
320
- try {
321
- for await (const value of generator) {
322
- await stream.writeln(JSON.stringify(value))
323
- }
324
- } catch (error) {
325
- // Get error yield value from callback (onMidStreamError)
326
- let errorResult: MidStreamErrorResult<TErrorData> | undefined
327
-
328
- if (this.config?.onMidStreamError) {
329
- errorResult = this.config.onMidStreamError(procedure, c, error as Error)
330
- }
331
-
332
- // Write error value to stream
333
- const errorData = errorResult?.data ?? { error: (error as Error).message }
334
- await stream.writeln(JSON.stringify(errorData))
335
- } finally {
336
- if (this.config?.onStreamEnd) {
337
- this.config.onStreamEnd(procedure, c, 'text')
338
- }
339
- if (this.config?.onRequestEnd) {
340
- this.config.onRequestEnd(c)
341
- }
342
- }
343
- })
344
- }
345
-
346
- /**
347
- * Builds and returns the Hono application with registered streaming routes.
348
- */
349
- build(): Hono {
350
- this.factories.forEach(({ factory, factoryContext, streamMode, extendProcedureDoc }) => {
351
- const mode = streamMode ?? this.config?.defaultStreamMode ?? 'sse'
352
-
353
- factory
354
- .getProcedures()
355
- .filter(
356
- (p: { isStream?: boolean }): p is TStreamProcedureRegistration => p.isStream === true
357
- )
358
- .forEach((procedure: TStreamProcedureRegistration<any, RPCConfig>) => {
359
- const route = this.buildStreamHttpRouteDoc(procedure, mode, extendProcedureDoc)
360
-
361
- this._docs.push(route)
362
-
363
- const handler = this.createStreamHandler(procedure, factoryContext, mode)
364
-
365
- // Register both GET and POST handlers
366
- this._app.get(route.path, handler)
367
- this._app.post(route.path, handler)
368
- })
369
- })
370
-
371
- return this._app
372
- }
373
-
374
- /**
375
- * Generates the Stream HTTP route documentation for the given procedure.
376
- */
377
- private buildStreamHttpRouteDoc(
378
- procedure: TStreamProcedureRegistration<any, RPCConfig>,
379
- streamMode: StreamMode,
380
- extendProcedureDoc?: HonoStreamFactoryItem['extendProcedureDoc']
381
- ): StreamHttpRouteDoc {
382
- const { config } = procedure
383
- const path = HonoStreamAppBuilder.makeStreamHttpRoutePath({
384
- name: procedure.name,
385
- config,
386
- prefix: this.config?.pathPrefix,
387
- })
388
- const methods = ['get', 'post'] as const
389
- const jsonSchema: { params?: Record<string, unknown>; yieldType?: Record<string, unknown>; returnType?: Record<string, unknown> } = {}
390
-
391
- if (config.schema?.params) {
392
- jsonSchema.params = config.schema.params
393
- }
394
- if (streamMode === 'sse') {
395
- jsonSchema.yieldType = {
396
- type: 'object',
397
- description: 'SSE message envelope. The data field contains the procedure yield value.',
398
- required: ['data', 'event', 'id'],
399
- properties: {
400
- data: config.schema?.yieldType ?? {},
401
- event: { type: 'string' },
402
- id: { type: 'string' },
403
- retry: { type: 'number' },
404
- },
405
- }
406
- } else if (config.schema?.yieldType) {
407
- // Text mode: pass through as-is
408
- jsonSchema.yieldType = config.schema.yieldType
409
- }
410
- if (config.schema?.returnType) {
411
- jsonSchema.returnType = config.schema.returnType
412
- }
413
-
414
- const base: StreamHttpRouteDoc = {
415
- name: procedure.name,
416
- version: config.version,
417
- scope: config.scope,
418
- path,
419
- methods: [...methods],
420
- streamMode,
421
- jsonSchema,
422
- }
423
-
424
- let extendedDoc: object = {}
425
-
426
- if (extendProcedureDoc) {
427
- extendedDoc = extendProcedureDoc({ base, procedure })
428
- }
429
-
430
- return {
431
- ...extendedDoc,
432
- ...base,
433
- }
434
- }
435
- }
@@ -1,29 +0,0 @@
1
- import { Context } from 'hono'
2
- import { TStreamProcedureRegistration } from '../../../index.js'
3
- import { ExtractConfig, ExtractContext, RPCConfig } from '../../types.js'
4
-
5
- export type StreamMode = 'sse' | 'text'
6
-
7
- export interface StreamHttpRouteDoc extends RPCConfig {
8
- name: string
9
- path: string
10
- methods: ('get' | 'post')[]
11
- streamMode: StreamMode
12
- jsonSchema: {
13
- params?: Record<string, unknown>
14
- yieldType?: Record<string, unknown>
15
- returnType?: Record<string, unknown>
16
- }
17
- }
18
-
19
- export type HonoStreamFactoryItem<TFactory = any> = {
20
- factory: TFactory
21
- factoryContext:
22
- | ExtractContext<TFactory>
23
- | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
24
- streamMode?: StreamMode
25
- extendProcedureDoc?: (params: {
26
- base: StreamHttpRouteDoc
27
- procedure: TStreamProcedureRegistration<any, ExtractConfig<TFactory>>
28
- }) => Record<string, any>
29
- }
@@ -1,75 +0,0 @@
1
- import { Procedures } from '../index.js'
2
-
3
- export interface RPCConfig {
4
- // Scope or scopes (scope segments) required to access the RPC
5
- scope: string | string[]
6
- version: number
7
- }
8
-
9
- export type FactoryItem<C> = {
10
- factory: ReturnType<typeof Procedures<C, RPCConfig>>
11
- factoryContext: (req: Request) => C
12
- }
13
-
14
- export interface RPCHttpRouteDoc extends RPCConfig {
15
- name: string // procedure name
16
- path: string
17
- method: 'post'
18
- jsonSchema: {
19
- body?: Record<string, unknown>
20
- response?: Record<string, unknown>
21
- }
22
- }
23
-
24
- export type StreamMode = 'sse' | 'text'
25
-
26
- export interface StreamHttpRouteDoc extends RPCConfig {
27
- name: string // procedure name
28
- path: string
29
- methods: ('get' | 'post')[]
30
- streamMode: StreamMode
31
- jsonSchema: {
32
- params?: Record<string, unknown> // Query params (GET) or body (POST)
33
- yieldType?: Record<string, unknown> // Schema for each streamed value
34
- returnType?: Record<string, unknown> // Final return (optional)
35
- }
36
- }
37
-
38
- // ================
39
- // Utility types
40
- // ================
41
-
42
- /**
43
- * Extracts the TContext type from a Procedures factory return type.
44
- * Uses the first parameter of the handler function to infer the context type.
45
- */
46
- export type ExtractContext<TFactory> = TFactory extends {
47
- getProcedures: () => Array<{ handler: (ctx: infer TContext, ...args: any[]) => any }>
48
- }
49
- ? TContext
50
- : never
51
-
52
- /**
53
- * Extracts the TConfig type from a Procedures factory return type.
54
- * Uses the config property of the procedure registration to infer the config type.
55
- */
56
- export type ExtractConfig<TFactory> = TFactory extends {
57
- getProcedures: () => Array<{ config: infer TConfig }>
58
- }
59
- ? TConfig
60
- : never
61
-
62
- /**
63
- * Minimal structural type for a Procedures factory.
64
- * Uses explicit `any` types to avoid variance issues with generic constraints.
65
- */
66
- export type ProceduresFactory = {
67
- getProcedures: () => Array<{
68
- name: string
69
- isStream?: boolean
70
- config: any
71
- handler: (ctx: any, params?: any) => Promise<any> | AsyncGenerator<any, any, unknown>
72
- }>
73
- Create: (...args: any[]) => any
74
- CreateStream?: (...args: any[]) => any
75
- }