ts-procedures 10.2.0 → 10.2.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.
- package/CHANGELOG.md +18 -0
- package/README.md +9 -6
- package/agent_config/bin/postinstall.mjs +20 -89
- package/agent_config/bin/setup.mjs +51 -264
- package/agent_config/lib/install-skills.mjs +108 -0
- package/build/codegen/bin/cli.js +1 -1
- package/build/codegen/bin/cli.js.map +1 -1
- package/build/codegen/emit/api-route.js +1 -1
- package/build/codegen/emit/api-route.js.map +1 -1
- package/build/codegen/emit/scope-file.js +1 -1
- package/build/codegen/emit/scope-file.js.map +1 -1
- package/build/codegen/emit-errors.integration.test.js +2 -2
- package/build/codegen/emit-errors.integration.test.js.map +1 -1
- package/build/codegen/resolve-envelope.js +1 -1
- package/build/codegen/resolve-envelope.js.map +1 -1
- package/build/codegen/test-helpers/run-tsc.js +1 -1
- package/build/codegen/test-helpers/run-tsc.js.map +1 -1
- package/build/exports.d.ts +1 -1
- package/build/exports.js.map +1 -1
- package/build/exports.surface.test.d.ts +1 -0
- package/build/exports.surface.test.js +132 -0
- package/build/exports.surface.test.js.map +1 -0
- package/docs/ai-agent-setup.md +19 -27
- package/docs/client-error-handling.md +1 -1
- package/docs/http-integrations.md +1 -1
- package/package.json +9 -8
- package/src/codegen/bin/cli.ts +1 -1
- package/src/codegen/emit/api-route.ts +1 -1
- package/src/codegen/emit/scope-file.ts +2 -1
- package/src/codegen/emit-errors.integration.test.ts +2 -2
- package/src/codegen/resolve-envelope.ts +1 -1
- package/src/codegen/test-helpers/run-tsc.ts +1 -0
- package/src/exports.surface.test.ts +134 -0
- package/src/exports.ts +1 -0
- package/agent_config/claude-code/.claude-plugin/plugin.json +0 -5
- package/agent_config/claude-code/agents/ts-procedures-architect.md +0 -138
- package/agent_config/copilot/copilot-instructions.md +0 -521
- package/agent_config/cursor/cursorrules +0 -521
- package/agent_config/lib/install-claude.mjs +0 -57
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/SKILL.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/anti-patterns.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/api-reference.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/checklist.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/patterns.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/client.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/hono.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/procedure.md +0 -0
- /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md +0 -0
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
# ts-procedures Framework Reference
|
|
2
|
-
|
|
3
|
-
You are assisting a developer using **ts-procedures**, a TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition.
|
|
4
|
-
|
|
5
|
-
## Core Flow
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
Procedures<TContext, TExtendedConfig>(options?)
|
|
9
|
-
↓
|
|
10
|
-
Create(name, config, handler) → Standard async procedure (kind: 'rpc')
|
|
11
|
-
CreateStream(name, config, handler) → Streaming async generator (kind: 'rpc-stream')
|
|
12
|
-
CreateHttp(name, config, handler) → REST-style HTTP route (kind: 'http')
|
|
13
|
-
CreateHttpStream(name, config, handler) → REST-style streaming route (kind: 'http-stream')
|
|
14
|
-
↓
|
|
15
|
-
Returns: { [name]: handler, procedure: handler, info: metadata }
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Every registration and every creator's `info` carries the `kind` discriminant — branch on it (there is no `isStream` flag).
|
|
19
|
-
|
|
20
|
-
## Imports
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
// Core
|
|
24
|
-
import { Procedures } from 'ts-procedures'
|
|
25
|
-
import type { Infer, SchemaAdapter, TLocalContext, TStreamContext, TProcedureRegistration, TStreamProcedureRegistration } from 'ts-procedures'
|
|
26
|
-
|
|
27
|
-
// Errors
|
|
28
|
-
import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError, ProcedureRegistrationError } from 'ts-procedures'
|
|
29
|
-
|
|
30
|
-
// HTTP types
|
|
31
|
-
import type { RPCConfig, APIConfig, APIInput, RPCHttpRouteDoc, StreamHttpRouteDoc, StreamMode } from 'ts-procedures/http'
|
|
32
|
-
|
|
33
|
-
// Hono (RPC, streaming, REST — one unified builder)
|
|
34
|
-
import { HonoAppBuilder, sse, defineErrorTaxonomy } from 'ts-procedures/hono'
|
|
35
|
-
|
|
36
|
-
// Doc Registry — compose route docs from multiple builders
|
|
37
|
-
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
38
|
-
import type { DocEnvelope, HeaderDoc, ErrorDoc } from 'ts-procedures/http-docs'
|
|
39
|
-
|
|
40
|
-
// Server toolkit — transport-agnostic building blocks for writing custom adapters (Fastify, Express, ...)
|
|
41
|
-
// Also re-exports DocRegistry, writeDocEnvelope, defineErrorTaxonomy, sse, and the doc builders
|
|
42
|
-
import { dispatchPreStreamError, dispatchMidStreamError, extractReqChannels, buildRpcRouteDoc, SseEventSequencer } from 'ts-procedures/server'
|
|
43
|
-
import type { RequestSource } from 'ts-procedures/server'
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Architecture Rules
|
|
47
|
-
|
|
48
|
-
1. **schema.params is validated at runtime; schema.returnType is documentation only.** Never expect returnType to be validated. Use TypeScript for return type safety.
|
|
49
|
-
|
|
50
|
-
2. **Use ctx.error() for business logic errors.** Never throw raw `Error` instances. `ctx.error(message, meta?)` creates `ProcedureError` with procedure name, metadata, and enhanced stack trace.
|
|
51
|
-
|
|
52
|
-
3. **Pass ctx.signal to all downstream async calls.** HTTP implementations inject AbortSignal automatically. Pass it to fetch, database queries, and other async operations for cancellation support.
|
|
53
|
-
|
|
54
|
-
4. **Stream handlers always have ctx.signal (guaranteed AbortSignal).** Standard handlers get it when HTTP implementations provide it. Check `signal.reason === 'stream-completed'` to distinguish normal completion from client disconnect.
|
|
55
|
-
|
|
56
|
-
5. **Use TypeBox for schemas** (`import { Type } from 'typebox'`). Plain JSON Schema objects are not recognized. TypeBox is the built-in schema library; other libraries plug in via the `SchemaAdapter` interface (`Procedures({ schema: { adapters: [myAdapter] } })` — `{ name, detect, toJsonSchema }`). Custom adapters get runtime validation + docs only; compile-time `Infer<T>` inference is TypeBox-only. Default AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true` — customize per factory via `Procedures({ validation: { ajv } })` (options merged over defaults, or a configured Ajv instance). The bundled TypeBox does **not** export `Type.Composite` — compose schemas with a flat property spread (`Type.Object({ ...Base.properties, name: Type.String() })`), which also keeps the generated JSON Schema a single `object` instead of an `allOf`.
|
|
57
|
-
|
|
58
|
-
6. **Prefer middleware + layered context for access levels.** Use `HonoAppBuilder` `middleware`, `.use()`, or per-`register({ middleware, context })` for tenant/auth. Separate factories are still valid for truly disjoint `TContext` shapes.
|
|
59
|
-
|
|
60
|
-
7. **onCreate callback enables framework integration.** Use for route registration, OpenAPI generation, logging.
|
|
61
|
-
|
|
62
|
-
8. **getProcedures() for introspection.** Returns all registered procedures with metadata.
|
|
63
|
-
|
|
64
|
-
9. **`Procedures({ validation: false })` skips per-call AJV validation** for every procedure on the factory (all four creators, both `schema.params` and `schema.req`). JSON Schema and validators are still computed at registration time (bad schemas still fail fast), so codegen and `info.schema` are unaffected; only the per-call validator runs are bypassed. **Never enable this on a factory whose handlers are reachable from public/untrusted callers** — coerceTypes and removeAdditional also stop running. Reserve for trusted internal factories whose callers are already type-checked at build time. Independent of the per-call `ctx.isPrevalidated` escape hatch used by HTTP builders.
|
|
65
|
-
|
|
66
|
-
10. **`Procedures({ http: { pathPrefix, scope } })` sets factory-level defaults for `CreateHttp` / `CreateHttpStream`.** `pathPrefix` is prepended to every route's `path` at registration time — it becomes part of the route's identity (shows up in `config.path` and route docs), unlike the builder-level `pathPrefix`, which is a deployment mount point. `scope` is the default codegen scope for routes that don't set one; per-route values win.
|
|
67
|
-
|
|
68
|
-
## Procedure Pattern
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
import { Procedures } from 'ts-procedures'
|
|
72
|
-
import { Type } from 'typebox'
|
|
73
|
-
|
|
74
|
-
type AppContext = { userId: string; signal?: AbortSignal }
|
|
75
|
-
|
|
76
|
-
const { Create, CreateStream } = Procedures<AppContext>()
|
|
77
|
-
|
|
78
|
-
// Standard procedure
|
|
79
|
-
const { GetUser } = Create(
|
|
80
|
-
'GetUser',
|
|
81
|
-
{
|
|
82
|
-
description: 'Fetch user by ID',
|
|
83
|
-
schema: {
|
|
84
|
-
params: Type.Object({ id: Type.String() }),
|
|
85
|
-
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
async (ctx, params) => {
|
|
89
|
-
// params.id guaranteed string (AJV validated)
|
|
90
|
-
const user = await fetchUser(params.id, { signal: ctx.signal })
|
|
91
|
-
if (!user) throw ctx.error('Not found', { code: 'NOT_FOUND' })
|
|
92
|
-
return user
|
|
93
|
-
}
|
|
94
|
-
)
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Stream Procedure Pattern
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const { StreamEvents } = CreateStream(
|
|
101
|
-
'StreamEvents',
|
|
102
|
-
{
|
|
103
|
-
schema: {
|
|
104
|
-
params: Type.Object({ channel: Type.String() }),
|
|
105
|
-
yieldType: Type.Object({ type: Type.String(), data: Type.Any() }),
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
async function* (ctx, params) {
|
|
109
|
-
// ctx.signal always present in streams
|
|
110
|
-
while (!ctx.signal.aborted) {
|
|
111
|
-
const event = await pollEvents(params.channel, { signal: ctx.signal })
|
|
112
|
-
yield event
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Hono RPC Pattern
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
import { HonoAppBuilder } from 'ts-procedures/hono'
|
|
122
|
-
|
|
123
|
-
const app = new HonoAppBuilder({ pathPrefix: '/api' })
|
|
124
|
-
.register(RPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
125
|
-
.build()
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## Hono Streaming Pattern
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
import { HonoAppBuilder, sse } from 'ts-procedures/hono'
|
|
132
|
-
|
|
133
|
-
const StreamRPC = Procedures<AppContext, RPCConfig>()
|
|
134
|
-
|
|
135
|
-
StreamRPC.CreateStream('Feed', {
|
|
136
|
-
scope: 'events', version: 1,
|
|
137
|
-
schema: { params: Type.Object({ channel: Type.String() }) },
|
|
138
|
-
}, async function* (ctx, params) {
|
|
139
|
-
while (!ctx.signal.aborted) {
|
|
140
|
-
const event = await poll({ signal: ctx.signal })
|
|
141
|
-
yield sse(event, { event: 'update' })
|
|
142
|
-
}
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const app = new HonoAppBuilder({ stream: { defaultStreamMode: 'sse' } })
|
|
146
|
-
.register(StreamRPC, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
147
|
-
.build()
|
|
148
|
-
// GET|POST /events/feed/1
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
SSE wire behavior is identical for `rpc-stream` and `http-stream` routes (one shared `SseEventSequencer`): `sse(data, { event, id, retry })` metadata is honored on yields and error events of both kinds, and a `null` yield serializes as empty SSE data on both.
|
|
152
|
-
|
|
153
|
-
## Hono API Pattern (REST-style) — `CreateHttp`
|
|
154
|
-
|
|
155
|
-
HTTP routes use `CreateHttp` (not `Create + APIConfig`). Request channels live under `schema.req` (`pathParams`, `query`, `body`, `headers`); response docs under `schema.res`. The factory needs no `APIConfig` type parameter.
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
import { HonoAppBuilder } from 'ts-procedures/hono'
|
|
159
|
-
|
|
160
|
-
const API = Procedures<AppContext>()
|
|
161
|
-
|
|
162
|
-
API.CreateHttp('GetUser', {
|
|
163
|
-
path: '/users/:id', method: 'get',
|
|
164
|
-
schema: {
|
|
165
|
-
req: { pathParams: Type.Object({ id: Type.String() }) },
|
|
166
|
-
res: { body: Type.Object({ id: Type.String(), name: Type.String() }) },
|
|
167
|
-
},
|
|
168
|
-
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
169
|
-
|
|
170
|
-
API.CreateHttp('CreateUser', {
|
|
171
|
-
path: '/users', method: 'post',
|
|
172
|
-
schema: {
|
|
173
|
-
req: { body: Type.Object({ name: Type.String() }) },
|
|
174
|
-
},
|
|
175
|
-
}, async (ctx, { body }) => createUser(body))
|
|
176
|
-
|
|
177
|
-
const app = new HonoAppBuilder({ pathPrefix: '/api' })
|
|
178
|
-
.register(API, (c) => ({ userId: c.req.header('x-user-id') }))
|
|
179
|
-
.build()
|
|
180
|
-
// GET /api/users/:id → 200, POST /api/users → 201
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Streaming HTTP routes use `CreateHttpStream` with `schema.yield` (and optional `schema.res.headers`) — both schemas flow into route docs and generated client types.
|
|
184
|
-
|
|
185
|
-
**v8 → v9 migration:**
|
|
186
|
-
|
|
187
|
-
1. `Procedures({ config: { noRuntimeValidation: true } })` → `Procedures({ validation: false })`
|
|
188
|
-
2. Suretype schemas → TypeBox (or a custom `SchemaAdapter`); `isSuretypeSchema` removed
|
|
189
|
-
3. `registration.isStream` → branch on the `kind` discriminant (`'rpc' | 'rpc-stream' | 'http' | 'http-stream'`)
|
|
190
|
-
4. Duplicate-name registration now throws `ProcedureRegistrationError` instead of a bare `Error` (same message)
|
|
191
|
-
5. `schemaParser` removed — use `computeSchema(name, schema, { adapters, compile })`; `extractJsonSchema(schema, adapters)` takes the adapter list explicitly
|
|
192
|
-
6. Codegen CLI/options/output unchanged — regenerated clients are byte-identical to v8.6.0
|
|
193
|
-
|
|
194
|
-
## Astro adapter
|
|
195
|
-
|
|
196
|
-
Catch-all endpoint pattern. Build Hono apps once with `HonoAppBuilder`, mount via `createAstroHandler` in `src/pages/api/[...rest].ts` with `pathPrefix: '/api'`. Read Astro context inside factory closures with `getAstroContext(c)`. Multi-app dispatch is first-non-404-wins.
|
|
197
|
-
|
|
198
|
-
## Error Handling
|
|
199
|
-
|
|
200
|
-
| Error Class | Trigger | HTTP Status |
|
|
201
|
-
|-------------|---------|-------------|
|
|
202
|
-
| `ProcedureValidationError` | Schema params validation failure | 400 (auto — default taxonomy) |
|
|
203
|
-
| `ProcedureError` | `ctx.error()` or unhandled handler exception | 500 (auto — default taxonomy) |
|
|
204
|
-
| `ProcedureYieldValidationError` | Yield validation failure (validateYields: true) | N/A (mid-stream) |
|
|
205
|
-
| `ProcedureRegistrationError` | Invalid schema or duplicate name at registration | N/A (startup) |
|
|
206
|
-
|
|
207
|
-
Every framework error instance carries a `kind` field (`'procedure' | 'validation' | 'yield-validation' | 'registration'`) as an alternative to `instanceof`.
|
|
208
|
-
|
|
209
|
-
### Error Taxonomy (required for custom errors)
|
|
210
|
-
|
|
211
|
-
`defineErrorTaxonomy` maps error classes to HTTP responses declaratively. Works with `HonoAppBuilder` for `rpc`, `http`, and pre-stream paths (`rpc-stream` / `http-stream`).
|
|
212
|
-
|
|
213
|
-
Do NOT write `onError` `instanceof` ladders — that's anti-pattern #20. Use the taxonomy instead:
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
import { defineErrorTaxonomy } from 'ts-procedures/http-errors'
|
|
217
|
-
|
|
218
|
-
class UseCaseError extends Error {
|
|
219
|
-
constructor(readonly externalMsg: string, readonly internalMsg: string) {
|
|
220
|
-
super(externalMsg); this.name = 'UseCaseError'
|
|
221
|
-
Object.setPrototypeOf(this, UseCaseError.prototype)
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const appErrors = defineErrorTaxonomy({
|
|
226
|
-
// class: entries are topologically sorted (subclasses checked first) automatically.
|
|
227
|
-
// Predicate (match:) entries keep declared order — put narrower predicates first.
|
|
228
|
-
UseCaseError: {
|
|
229
|
-
class: UseCaseError,
|
|
230
|
-
statusCode: 422,
|
|
231
|
-
toResponse: (err) => ({ name: 'UseCaseError', message: err.externalMsg }),
|
|
232
|
-
onCatch: (err) => logger.error(err.internalMsg), // internal logs only
|
|
233
|
-
},
|
|
234
|
-
MongoDuplicateKey: {
|
|
235
|
-
match: (err): err is Error => err instanceof Error && (err as any).code === 11000,
|
|
236
|
-
statusCode: 409,
|
|
237
|
-
toResponse: () => ({ name: 'Conflict', message: 'Resource already exists' }),
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
new HonoAppBuilder({
|
|
242
|
-
errors: appErrors,
|
|
243
|
-
unknownError: { statusCode: 500, toResponse: () => ({ name: 'InternalServerError' }) },
|
|
244
|
-
})
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
Handlers throw the error classes directly — the builder auto-serializes. `onError` is the first-class imperative peer when you want to handle errors in one callback instead (both modes coexist). In v6 `HonoAppBuilder.onPreStreamError` was renamed to `onError`. Cross-cutting `onRequestError` observer fires for every caught error.
|
|
248
|
-
|
|
249
|
-
Custom (non-Hono) adapters consume the same taxonomy via `ts-procedures/server`'s `dispatchPreStreamError` / `dispatchMidStreamError`, which return plain data (`{ type: 'body', statusCode, body } | { type: 'response', response }` / `{ data, sseEvent?, runOnCatch? }`) — your adapter translates that into its framework's response.
|
|
250
|
-
|
|
251
|
-
### Per-route errors (typed)
|
|
252
|
-
|
|
253
|
-
Declare a route's errors via the `errors` field on the `CreateHttp` config. For compile-time typo protection, attach a `satisfies` clause to the array — do NOT pass an explicit generic. `CreateHttp` is generic over `<TName, TReq, TRes, TErrorKey>`, so a single type argument binds `TName` and breaks `schema.req` / `schema.res` inference:
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
257
|
-
|
|
258
|
-
const API = Procedures<Ctx>()
|
|
259
|
-
|
|
260
|
-
API.CreateHttp('GetUser', {
|
|
261
|
-
path: '/users/:id', method: 'get',
|
|
262
|
-
errors: ['UseCaseError'] satisfies (keyof typeof appErrors & string)[],
|
|
263
|
-
schema: { req: { pathParams: Type.Object({ id: Type.String() }) } },
|
|
264
|
-
}, async (ctx, { pathParams }) => fetchUser(pathParams.id))
|
|
265
|
-
|
|
266
|
-
// Seed envelope errors from the taxonomy + framework defaults in one call
|
|
267
|
-
const envelope = new DocRegistry({ errors: appErrors, basePath: '/api' }).from(apiApp).toJSON()
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### Client-side typed catch blocks (via codegen)
|
|
271
|
-
|
|
272
|
-
The generated `_errors.ts` emits real runtime classes extending a shared `${Service}ProcedureError` base. The generated `createApiClient` wires an error registry so non-2xx responses arrive as typed class instances:
|
|
273
|
-
|
|
274
|
-
```typescript
|
|
275
|
-
import { createApiClient, ApiErrors, createFetchAdapter } from './generated'
|
|
276
|
-
|
|
277
|
-
const api = createApiClient({ adapter: createFetchAdapter({ /* ... */ }), basePath: '...' })
|
|
278
|
-
|
|
279
|
-
try {
|
|
280
|
-
await api.users.getUser({ pathParams: { id: 'x' } })
|
|
281
|
-
} catch (err) {
|
|
282
|
-
if (err instanceof ApiErrors.UseCaseError) {
|
|
283
|
-
// err.body typed; err.status, err.procedureName, err.scope available
|
|
284
|
-
} else if (err instanceof ApiErrors.ApiProcedureError) {
|
|
285
|
-
// Catch-all for any service error
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
Typed classes are generated automatically for taxonomy entries declared with just `{ class, statusCode }` (their default `{ name, message }` body is self-describing). Add an explicit `schema` only when you give an entry a custom `toResponse` (shape can't be inferred) or document a raw `ErrorDoc` (via `documentError(...)` / a `config.errors` array) — without a schema those dispatch as the untyped `ClientHttpError`.
|
|
291
|
-
|
|
292
|
-
Per-route error unions: routes with `errors: [...]` get an `Errors` type in their namespace (e.g. `Users.GetUser.Errors = ApiErrors.UseCaseError | ApiErrors.AuthError`).
|
|
293
|
-
|
|
294
|
-
- Generated client error handling: catch the framework classes (`ClientHttpError`, `ClientNetworkError`, `ClientTimeoutError`, `ClientAbortError`), not raw `DOMException`/`TypeError` — the framework normalizes platform errors at the `executeCall` boundary, so raw platform errors no longer reach `catch` blocks after 7.0.0 (original is on `error.cause`). Use the `.safe()` sibling on RPC/API callables for an exhaustive `Result<T, E>` switch (`ok`, `typed`, `http`, `network`, `timeout`, `aborted`, `parse`, `usage`, `unknown`). Streams keep the throwing form — no `.safe()`.
|
|
295
|
-
|
|
296
|
-
## Lifecycle Hook Order
|
|
297
|
-
|
|
298
|
-
### Standard RPC
|
|
299
|
-
```
|
|
300
|
-
onRequestStart → factoryContext() → handler() → onSuccess → onRequestEnd
|
|
301
|
-
→ onError → onRequestEnd
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
### Streaming
|
|
305
|
-
```
|
|
306
|
-
onRequestStart → factoryContext() → validation → onStreamStart → handler yields → onStreamEnd → onRequestEnd
|
|
307
|
-
→ onError (or taxonomy) → onRequestEnd
|
|
308
|
-
→ onMidStreamError → onStreamEnd → onRequestEnd
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
## Decision Framework
|
|
312
|
-
|
|
313
|
-
**Which procedure type?**
|
|
314
|
-
- Single response (RPC) → `Create`
|
|
315
|
-
- Multiple values over time (RPC) → `CreateStream`
|
|
316
|
-
- REST-style route (`path` + `method`) → `CreateHttp`
|
|
317
|
-
- REST-style streaming route → `CreateHttpStream`
|
|
318
|
-
|
|
319
|
-
**Which schema library?**
|
|
320
|
-
- **TypeBox** (`import { Type } from 'typebox'`) — built in, with `Infer<T>` type inference
|
|
321
|
-
- Other libraries → implement `SchemaAdapter` and pass `Procedures({ schema: { adapters: [...] } })` (runtime validation + docs only)
|
|
322
|
-
|
|
323
|
-
**Which HTTP implementation?**
|
|
324
|
-
- Hono (RPC, streams, REST, REST streams) → `HonoAppBuilder`
|
|
325
|
-
- Other frameworks (Fastify, Express, ...) → write a thin adapter on `ts-procedures/server` (doc builders, taxonomy dispatch returning data, `extractReqChannels`/`RequestSource`, `SseEventSequencer`); use the Hono adapter as the blueprint
|
|
326
|
-
|
|
327
|
-
**Stream mode?**
|
|
328
|
-
- Browser EventSource → `'sse'` (default)
|
|
329
|
-
- Simple HTTP client → `'text'` (newline-delimited JSON)
|
|
330
|
-
|
|
331
|
-
## Anti-Patterns — NEVER Do These
|
|
332
|
-
|
|
333
|
-
1. **Never throw raw Error** — use `ctx.error(message, meta?)`
|
|
334
|
-
2. **Never expect returnType runtime validation** — it's docs only
|
|
335
|
-
3. **Never put validation logic in handler** — use `schema.params`
|
|
336
|
-
4. **Never ignore ctx.signal** — pass to all async calls
|
|
337
|
-
5. **Never skip signal.aborted check in stream loops** — causes resource leaks
|
|
338
|
-
6. **Never branch on `isStream`** — removed in v9; use the `kind` discriminant (on registrations and every creator's `info`)
|
|
339
|
-
7. **Never use plain JSON Schema objects** — use TypeBox builders (or register a `SchemaAdapter` for another library)
|
|
340
|
-
8. **Never swallow errors without re-throwing** — hides failures
|
|
341
|
-
9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
|
|
342
|
-
10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
|
|
343
|
-
11. **Never put `schema.req` on `Create`/`CreateStream` or `schema.params` on `CreateHttp`/`CreateHttpStream`** — RPC creators take `schema.params`; HTTP creators take `schema.req` channels
|
|
344
|
-
12. **Never catch raw DOMException/TypeError from generated callables** — catch `ClientTimeoutError`, `ClientAbortError`, `ClientNetworkError` instead; or use `.safe()` for Result-based narrowing
|
|
345
|
-
13. **Never use `Create` for HTTP routes (v8+)** — use `CreateHttp` (unary) or `CreateHttpStream` (streaming); request channels go under `schema.req`; `Procedures<Ctx, APIConfig>()` → `Procedures<Ctx>()`
|
|
346
|
-
|
|
347
|
-
## Testing
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
import { describe, test, expect } from 'vitest'
|
|
351
|
-
import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
352
|
-
|
|
353
|
-
describe('GetUser', () => {
|
|
354
|
-
test('valid params', async () => {
|
|
355
|
-
const result = await GetUser(mockCtx, { id: 'user-1' })
|
|
356
|
-
expect(result.id).toBe('user-1')
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
test('invalid params', async () => {
|
|
360
|
-
await expect(GetUser(mockCtx, {})).rejects.toThrow(ProcedureValidationError)
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
test('business error', async () => {
|
|
364
|
-
await expect(GetUser(mockCtx, { id: 'missing' })).rejects.toThrow(ProcedureError)
|
|
365
|
-
})
|
|
366
|
-
})
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
## HTTP Route Path Format
|
|
370
|
-
|
|
371
|
-
`{pathPrefix}/{scope}/{kebab-case-name}/{version}`
|
|
372
|
-
|
|
373
|
-
- `scope` can be string or string[] (joined as path segments)
|
|
374
|
-
- Procedure name auto-converted to kebab-case
|
|
375
|
-
- Stream routes support both GET (query params) and POST (JSON body)
|
|
376
|
-
|
|
377
|
-
## Client Code Generation
|
|
378
|
-
|
|
379
|
-
Generate type-safe client SDKs from a running DocRegistry endpoint.
|
|
380
|
-
|
|
381
|
-
### Imports
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
// Client Runtime
|
|
385
|
-
import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
386
|
-
import type { ClientAdapter, ClientHooks, TypedStream, ClientInstance } from 'ts-procedures/client'
|
|
387
|
-
|
|
388
|
-
// Code Generation (build-time only)
|
|
389
|
-
import { generateClient } from 'ts-procedures/codegen'
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
### CLI
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
npx ts-procedures-codegen --url http://localhost:3000/docs --out ./src/generated/api
|
|
396
|
-
# Defaults (on): --self-contained --namespace-types --jsdoc --clean-out-dir
|
|
397
|
-
# (--clean-out-dir prunes orphaned generated files signed by ts-procedures-codegen; never deletes hand-written files)
|
|
398
|
-
# Opt-out flags: --no-self-contained --no-namespace-types --no-jsdoc --no-clean-out-dir
|
|
399
|
-
# Optional flags: --depluralize --enum-style union
|
|
400
|
-
# --array-item-naming Item --uncountable-words criteria,alumni
|
|
401
|
-
# --dry-run --watch --client-import-path @my-app/client
|
|
402
|
-
# --config ./codegen.config.json --service-name Auth
|
|
403
|
-
# --check [--check-mode scan|tsc] (self-validate output; exit non-zero if it won't compile)
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
For Kotlin codegen (Android/JVM consumers), see `docs/codegen-kotlin.md` in the ts-procedures repo. The Kotlin target is types-only; consumer apps own HTTP/error handling.
|
|
407
|
-
For Swift codegen (iOS/macOS/Apple-platform consumers), see `docs/codegen-swift.md` in the ts-procedures repo. The Swift target is types-only; consumer apps own HTTP/error handling.
|
|
408
|
-
|
|
409
|
-
Generates one `.ts` file per scope plus a root `index.ts` that imports each scope as a namespace and exports a `create${ServiceName}Bindings(client)` factory AND a `create${ServiceName}Client(config)` convenience factory that pre-wires the error registry (defaults to `createApiBindings` / `createApiClient`; pass `--service-name <Name>` to rename). When namespace mode is on (the default), `index.ts` also wraps every scope namespace in an outer `export namespace ${ServiceName} { ... }` block so types are reachable as `Api.Users.GetUser.Params`, `Api.Errors.UseCaseError`, etc. The errors file (`_errors.ts`) emits runtime error classes extending a shared `${ServiceName}ProcedureError` base, each with `static fromResponse(body, meta)`, plus `${ServiceName}ErrorRegistry` (runtime dispatch map) and `${ServiceName}ProcedureErrorUnion` (type union). Defaults: `ApiErrors`, `ApiProcedureError`, `ApiErrorRegistry`, `ApiProcedureErrorUnion`.
|
|
410
|
-
By default, types are wrapped in nested TS namespaces (`Scope.Route.Params`), JSDoc comments are emitted, and output is self-contained (no runtime dependency on `ts-procedures`). Use `--no-namespace-types` to revert to flat type names (`RouteParams`); this also disables the outer service namespace in `index.ts` and skips importing `_errors` from there.
|
|
411
|
-
The generated client is **compatible with `tsconfig` `verbatimModuleSyntax: true`** (the strictest mode astro/tsconfigs and many monorepos enable). In namespace mode the scope's `bindScope(client)` function is emitted as a member of `export namespace ${Pascal}` so the namespace is value+type — required for the index's `export import` re-exports to compile under verbatim. Most consumers stay on `create${Name}Bindings(client)` / `create${Name}Client(config)` and never touch `bindScope` directly.
|
|
412
|
-
**Strict CLI**: any unknown `--…` flag throws immediately with a `Did you mean …?` suggestion when the typo is close to a known flag (e.g. `--targt` → `--target`, `--service` → `--service-name`). Typos no longer fall through silently.
|
|
413
|
-
**Generation-time guards** (fail fast, non-zero): two routes in a scope whose names PascalCase to the same identifier (e.g. `get-task` + `getTask`), or a scope name colliding with `--service-name` (e.g. scope `godmode` + `--service-name Godmode`), error at generation with the fix to make. Opt into `--check` (or `generateClient({ validate: true })`) to validate the whole emitted client and fail loud if it won't compile — recommended in CI.
|
|
414
|
-
Note: ajsc formatting options (`--enum-style enum`, `--depluralize`, etc.) only take effect in namespace mode (the default). They are ignored with `--no-namespace-types`.
|
|
415
|
-
Supports config file: `ts-procedures-codegen.config.json` (auto-loaded from CWD) or `--config <path>`.
|
|
416
|
-
|
|
417
|
-
### createClient Example
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
import { createClient, createFetchAdapter } from 'ts-procedures/client'
|
|
421
|
-
import { createApiBindings, Api } from './generated/api'
|
|
422
|
-
// With --service-name <Name>: import { create<Name>Bindings, <Name> } from './generated/api'
|
|
423
|
-
|
|
424
|
-
const client = createClient({
|
|
425
|
-
adapter: createFetchAdapter(),
|
|
426
|
-
basePath: 'http://localhost:3000',
|
|
427
|
-
scopes: createApiBindings,
|
|
428
|
-
defaults: { timeout: 30_000, headers: { 'X-Client-Version': '1.0.0' } },
|
|
429
|
-
hooks: {
|
|
430
|
-
onBeforeRequest(ctx) {
|
|
431
|
-
ctx.request.headers = { ...ctx.request.headers, Authorization: `Bearer ${getToken()}` }
|
|
432
|
-
return ctx
|
|
433
|
-
},
|
|
434
|
-
onAfterResponse(ctx) {
|
|
435
|
-
if (ctx.response.status === 401) redirect('/login')
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
// Fully typed — params and response inferred from server schemas; type aliases live under Api.<Scope>.<Route>.
|
|
441
|
-
const user = await client.users.GetUser({ pathParams: { id: '123' } })
|
|
442
|
-
|
|
443
|
-
// Per-call options — timeout, signal, headers, basePath, and hooks all share one bag
|
|
444
|
-
await client.users.GetUser(
|
|
445
|
-
{ pathParams: { id: '123' } },
|
|
446
|
-
{
|
|
447
|
-
timeout: 5000,
|
|
448
|
-
headers: { 'X-Request-Id': crypto.randomUUID() },
|
|
449
|
-
onAfterResponse(ctx) {
|
|
450
|
-
console.log(ctx.response.headers['x-rate-limit-remaining'])
|
|
451
|
-
},
|
|
452
|
-
},
|
|
453
|
-
)
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Per-Call Options & Defaults
|
|
457
|
-
|
|
458
|
-
```typescript
|
|
459
|
-
interface ProcedureCallDefaults {
|
|
460
|
-
signal?: AbortSignal // cancellation (combined with per-call via AbortSignal.any)
|
|
461
|
-
timeout?: number // ms — per-call timeout:0 disables inherited default
|
|
462
|
-
headers?: Record<string, string> // merged, per-call keys win
|
|
463
|
-
basePath?: string // per-call > default > config.basePath
|
|
464
|
-
meta?: RequestMeta // typed per-request metadata (declaration-mergeable)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
interface ProcedureCallOptions extends ProcedureCallDefaults, ClientHooks {}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
- Defaults are set via `config.defaults`; per-call options are passed as the 2nd argument to any generated callable.
|
|
471
|
-
- Header precedence (low → high): adapter config < `defaults.headers` < per-call `headers` < route-declared `schema.req.headers` < `onBeforeRequest` mutations.
|
|
472
|
-
|
|
473
|
-
### Typed RequestMeta (Declaration Merging)
|
|
474
|
-
|
|
475
|
-
Augment `RequestMeta` to type `meta` end-to-end (options, hooks, adapter):
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
// For code-generated self-contained clients
|
|
479
|
-
declare module './generated/_types' {
|
|
480
|
-
interface RequestMeta {
|
|
481
|
-
traceId: string
|
|
482
|
-
priority?: 'high' | 'low'
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Or for direct ts-procedures/client usage
|
|
487
|
-
declare module 'ts-procedures/client' {
|
|
488
|
-
interface RequestMeta { traceId: string }
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
await client.users.GetUser(
|
|
492
|
-
{ pathParams: { id: '1' } },
|
|
493
|
-
{ meta: { traceId: 'req-abc', priority: 'high' } }, // fully typed
|
|
494
|
-
)
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
### Hook Types
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
interface ClientHooks {
|
|
501
|
-
onBeforeRequest?: (ctx: BeforeRequestContext) => BeforeRequestContext | Promise<BeforeRequestContext>
|
|
502
|
-
onAfterResponse?: (ctx: AfterResponseContext) => void | Promise<void>
|
|
503
|
-
onError?: (ctx: ErrorContext) => void | Promise<void>
|
|
504
|
-
}
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
### TypedStream Usage
|
|
508
|
-
|
|
509
|
-
```typescript
|
|
510
|
-
const stream = client.events.WatchNotifications({ filter: 'all' })
|
|
511
|
-
|
|
512
|
-
for await (const event of stream) {
|
|
513
|
-
console.log(event) // Typed as WatchNotificationsYield
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const result = await stream.result // Typed as WatchNotificationsReturn
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
- `TypedStream<TYield, TReturn>` extends `AsyncIterable<TYield>`
|
|
520
|
-
- `stream.result` resolves with the final return value sent as `event: 'return'` SSE message
|
|
521
|
-
- Stream procedures that return `void` have `result` resolve to `undefined`
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { cpSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join, dirname } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const SOURCE_DIR = join(__dirname, '..', 'claude-code');
|
|
8
|
-
|
|
9
|
-
const SKILL_NAMES = ['ts-procedures'];
|
|
10
|
-
const AGENT_FILES = ['ts-procedures-architect.md'];
|
|
11
|
-
|
|
12
|
-
function listFilesRecursive(dir) {
|
|
13
|
-
const out = [];
|
|
14
|
-
for (const entry of readdirSync(dir)) {
|
|
15
|
-
const full = join(dir, entry);
|
|
16
|
-
if (statSync(full).isDirectory()) {
|
|
17
|
-
for (const nested of listFilesRecursive(full)) out.push(join(entry, nested));
|
|
18
|
-
} else {
|
|
19
|
-
out.push(entry);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Install Claude Code integration files into a project's .claude/ directory.
|
|
27
|
-
*
|
|
28
|
-
* Creates:
|
|
29
|
-
* .claude/skills/ts-procedures/ — Unified skill: framework reference plus
|
|
30
|
-
* `review`/`scaffold` $ARGUMENTS modes (checklist +
|
|
31
|
-
* templates bundled) and Kotlin/Swift codegen reference
|
|
32
|
-
* .claude/agents/ts-procedures-architect.md — Architecture planning agent
|
|
33
|
-
*
|
|
34
|
-
* @param {string} projectRoot — Absolute path to the consuming project's root
|
|
35
|
-
* @returns {{ files: string[] }} — List of created/updated file paths (relative to projectRoot)
|
|
36
|
-
*/
|
|
37
|
-
export function installClaude(projectRoot) {
|
|
38
|
-
const claudeDir = join(projectRoot, '.claude');
|
|
39
|
-
const skillsDest = join(claudeDir, 'skills');
|
|
40
|
-
const agentsDest = join(claudeDir, 'agents');
|
|
41
|
-
mkdirSync(skillsDest, { recursive: true });
|
|
42
|
-
mkdirSync(agentsDest, { recursive: true });
|
|
43
|
-
|
|
44
|
-
const files = [];
|
|
45
|
-
for (const skill of SKILL_NAMES) {
|
|
46
|
-
const src = join(SOURCE_DIR, 'skills', skill);
|
|
47
|
-
cpSync(src, join(skillsDest, skill), { recursive: true, force: true });
|
|
48
|
-
for (const rel of listFilesRecursive(src)) {
|
|
49
|
-
files.push(`.claude/skills/${skill}/${rel}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
for (const agent of AGENT_FILES) {
|
|
53
|
-
cpSync(join(SOURCE_DIR, 'agents', agent), join(agentsDest, agent), { force: true });
|
|
54
|
-
files.push(`.claude/agents/${agent}`);
|
|
55
|
-
}
|
|
56
|
-
return { files };
|
|
57
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md
RENAMED
|
File without changes
|