ts-procedures 7.1.2 → 7.3.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 (41) hide show
  1. package/README.md +8 -0
  2. package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +35 -0
  3. package/agent_config/claude-code/skills/ts-procedures/api-reference.md +40 -0
  4. package/agent_config/claude-code/skills/ts-procedures/patterns.md +14 -0
  5. package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +1 -0
  6. package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.md +23 -0
  7. package/agent_config/copilot/copilot-instructions.md +6 -0
  8. package/agent_config/cursor/cursorrules +6 -0
  9. package/build/implementations/http/astro/astro-context.d.ts +19 -0
  10. package/build/implementations/http/astro/astro-context.js +28 -0
  11. package/build/implementations/http/astro/astro-context.js.map +1 -0
  12. package/build/implementations/http/astro/create-handler.d.ts +26 -0
  13. package/build/implementations/http/astro/create-handler.js +28 -0
  14. package/build/implementations/http/astro/create-handler.js.map +1 -0
  15. package/build/implementations/http/astro/index.d.ts +3 -0
  16. package/build/implementations/http/astro/index.js +6 -0
  17. package/build/implementations/http/astro/index.js.map +1 -0
  18. package/build/implementations/http/astro/index.test.d.ts +1 -0
  19. package/build/implementations/http/astro/index.test.js +295 -0
  20. package/build/implementations/http/astro/index.test.js.map +1 -0
  21. package/build/implementations/http/astro/rewrite-request.d.ts +13 -0
  22. package/build/implementations/http/astro/rewrite-request.js +32 -0
  23. package/build/implementations/http/astro/rewrite-request.js.map +1 -0
  24. package/build/index.d.ts +10 -0
  25. package/build/index.js +12 -13
  26. package/build/index.js.map +1 -1
  27. package/build/index.test.js +107 -0
  28. package/build/index.test.js.map +1 -1
  29. package/docs/astro-adapter.md +227 -0
  30. package/docs/core.md +19 -0
  31. package/docs/superpowers/plans/2026-05-07-astro-adapter.md +1396 -0
  32. package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +254 -0
  33. package/package.json +8 -2
  34. package/src/implementations/http/astro/README.md +89 -0
  35. package/src/implementations/http/astro/astro-context.ts +34 -0
  36. package/src/implementations/http/astro/create-handler.ts +59 -0
  37. package/src/implementations/http/astro/index.test.ts +350 -0
  38. package/src/implementations/http/astro/index.ts +6 -0
  39. package/src/implementations/http/astro/rewrite-request.ts +31 -0
  40. package/src/index.test.ts +171 -0
  41. package/src/index.ts +27 -15
@@ -0,0 +1,227 @@
1
+ # Astro adapter walkthrough
2
+
3
+ This walkthrough builds an Astro app that serves ts-procedures handlers from a single catch-all endpoint.
4
+
5
+ > **Requires SSR.** Your Astro project must use `output: 'server'` or `output: 'hybrid'` with `export const prerender = false` in the catch-all file. Static-prerender mode bypasses live endpoints.
6
+
7
+ ## Project shape
8
+
9
+ ```
10
+ my-astro-app/
11
+ ├── astro.config.mjs
12
+ ├── package.json
13
+ └── src/
14
+ ├── server/
15
+ │ ├── db.ts
16
+ │ ├── procedures/
17
+ │ │ └── users.ts
18
+ │ └── api.ts
19
+ └── pages/
20
+ └── api/
21
+ └── [...rest].ts
22
+ ```
23
+
24
+ ## 1. Define procedures
25
+
26
+ ```ts
27
+ // src/server/procedures/users.ts
28
+ import { Procedures } from 'ts-procedures'
29
+ import type { APIConfig } from 'ts-procedures/http'
30
+ import { Type } from 'typebox'
31
+
32
+ type UserContext = {
33
+ db: { findUser(id: string): Promise<{ id: string; name: string } | null> }
34
+ currentUser: { id: string } | null
35
+ }
36
+
37
+ export const usersAPI = Procedures<UserContext, APIConfig>()
38
+
39
+ usersAPI.Create(
40
+ 'GetUser',
41
+ {
42
+ path: '/users/:id',
43
+ method: 'get',
44
+ scope: 'users', // drives the generated client namespace: api.users.GetUser(...)
45
+ schema: {
46
+ input: { pathParams: Type.Object({ id: Type.String() }) },
47
+ returnType: Type.Object({ id: Type.String(), name: Type.String() }),
48
+ },
49
+ },
50
+ async (ctx, { pathParams }) => {
51
+ const u = await ctx.db.findUser(pathParams.id)
52
+ if (!u) throw new Error('not found')
53
+ return u
54
+ }
55
+ )
56
+ ```
57
+
58
+ ## 2. Build the Hono app once
59
+
60
+ ```ts
61
+ // src/server/api.ts
62
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
63
+ import { getAstroContext } from 'ts-procedures/astro'
64
+ import { usersAPI } from './procedures/users'
65
+ import { db } from './db'
66
+
67
+ export const apiApp = new HonoAPIAppBuilder()
68
+ .register(usersAPI, (c) => {
69
+ const astro = getAstroContext(c)
70
+ return {
71
+ db,
72
+ currentUser: astro.locals.user ?? null,
73
+ }
74
+ })
75
+ .build()
76
+ ```
77
+
78
+ ## 3. Mount the catch-all
79
+
80
+ ```ts
81
+ // src/pages/api/[...rest].ts
82
+ import { createAstroHandler } from 'ts-procedures/astro'
83
+ import { apiApp } from '../../server/api'
84
+
85
+ export const { ALL } = createAstroHandler({
86
+ apps: apiApp,
87
+ pathPrefix: '/api',
88
+ })
89
+ ```
90
+
91
+ That's it — `GET /api/users/123` runs through `usersAPI.GetUser`. The factory closure sees `astro.locals.user` populated from your Astro middleware.
92
+
93
+ ### Troubleshooting: routes 404
94
+
95
+ The most common cause is a `pathPrefix` mismatch. The prefix MUST match the directory the catch-all file lives in:
96
+
97
+ - `src/pages/api/[...rest].ts` → `pathPrefix: '/api'`
98
+ - `src/pages/v1/[...rest].ts` → `pathPrefix: '/v1'`
99
+ - `src/pages/[...rest].ts` (root) → omit `pathPrefix`
100
+
101
+ If you mount at `/api` but forget `pathPrefix`, the inner Hono app sees the full path including `/api/...`, so routes registered as `/users/:id` won't match `/api/users/123`.
102
+
103
+ ## Where do `Astro.locals.user` come from?
104
+
105
+ A typical pattern: an Astro middleware that reads a session cookie and sets `locals.user`:
106
+
107
+ ```ts
108
+ // src/middleware.ts
109
+ import { defineMiddleware } from 'astro:middleware'
110
+ import { db } from './server/db'
111
+
112
+ export const onRequest = defineMiddleware(async (context, next) => {
113
+ const sessionId = context.cookies.get('sid')?.value
114
+ if (sessionId) {
115
+ context.locals.user = await db.findUserBySession(sessionId)
116
+ }
117
+ return next()
118
+ })
119
+ ```
120
+
121
+ The Astro runtime invokes this before your endpoint. The adapter then forwards the same `APIContext` (with `locals.user` populated) into the WeakMap, so `getAstroContext(c)` returns it inside the procedure factory.
122
+
123
+ ## Client codegen — where does it go?
124
+
125
+ The adapter does NOT couple to `DocRegistry`. Wire codegen separately, against the same builders. The cleanest DX is a one-line `npm run gen:api` that emits an envelope from your procedures and runs the codegen CLI in a single step.
126
+
127
+ ### Emit an envelope from your procedures
128
+
129
+ ```ts
130
+ // scripts/build-docs.ts
131
+ import { writeFileSync } from 'node:fs'
132
+ import { DocRegistry } from 'ts-procedures/http-docs'
133
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
134
+ import { usersAPI } from '../src/server/procedures/users'
135
+
136
+ // Use the SAME builders as your runtime app — types can't drift, because
137
+ // both call sites import the exact same `Procedures<...>()` registration.
138
+ const builder = new HonoAPIAppBuilder().register(usersAPI, () => ({} as never))
139
+ builder.build()
140
+
141
+ const envelope = new DocRegistry().from(builder).toEnvelope()
142
+ writeFileSync('envelope.json', JSON.stringify(envelope, null, 2))
143
+ ```
144
+
145
+ ### Wire it as an npm script
146
+
147
+ ```jsonc
148
+ // package.json
149
+ {
150
+ "scripts": {
151
+ "gen:api": "tsx scripts/build-docs.ts && ts-procedures-codegen --file envelope.json --out src/generated/api --service-name Api"
152
+ }
153
+ }
154
+ ```
155
+
156
+ CLI defaults are already optimal: `--self-contained` (no runtime dependency on `ts-procedures/client`), `--namespace-types` (clean `Api.Users.GetUser.Params`), and `--jsdoc` are all on. The default `--service-name` is `Api`, which gives you `createApiClient`, `createApiBindings`, and an `ApiErrors` namespace.
157
+
158
+ > **Tip:** add `src/generated/api` to `.gitignore` and run `npm run gen:api` in CI / pre-commit. The output is a pure function of `envelope.json`.
159
+
160
+ ## Use the typed client in pages
161
+
162
+ Generated client is fully typed end-to-end — params, response, and (via `.safe()`) typed errors.
163
+
164
+ ```ts
165
+ ---
166
+ // src/pages/users/[id].astro
167
+ import { createApiClient } from '../../generated/api'
168
+
169
+ const api = createApiClient({ basePath: '/api' })
170
+
171
+ // Throwing form: typed Response, typed Params (with structured channels)
172
+ const user = await api.users.GetUser({ pathParams: { id: Astro.params.id! } })
173
+
174
+ // Result form: typed errors via the route's declared `errors`
175
+ const result = await api.users.GetUser.safe({ pathParams: { id: Astro.params.id! } })
176
+ if (!result.ok) {
177
+ if (result.kind === 'typed') {
178
+ // result.error is narrowed to the route's declared error union
179
+ return Astro.redirect('/login')
180
+ }
181
+ // result.kind === 'transport' — network/parse/timeout/abort
182
+ throw result.error
183
+ }
184
+ ---
185
+ <h1>{user.name}</h1>
186
+ ```
187
+
188
+ For client-side islands (React/Svelte/Vue components), the same `createApiClient` works in the browser — pass an absolute `basePath` if calling cross-origin, or omit it to use relative paths.
189
+
190
+ ### Per-call options
191
+
192
+ Every callable accepts a second arg with `signal`, `timeout`, `headers`, `basePath`, `meta`, and per-call hooks:
193
+
194
+ ```ts
195
+ const controller = new AbortController()
196
+ await api.users.GetUser(
197
+ { pathParams: { id: '123' } },
198
+ { signal: controller.signal, timeout: 5000, headers: { 'X-Trace-Id': 'abc' } },
199
+ )
200
+ ```
201
+
202
+ `signal`s combine via `AbortSignal.any`; `timeout: 0` disables an inherited default. Set client-wide defaults via `createApiClient({ basePath, defaults: { timeout, headers, meta } })`.
203
+
204
+ ## Multi-app composition
205
+
206
+ For larger projects with separate API/RPC/Stream surfaces, pass an array:
207
+
208
+ ```ts
209
+ export const { ALL } = createAstroHandler({
210
+ apps: [apiApp, rpcApp, streamsApp],
211
+ pathPrefix: '/api',
212
+ })
213
+ ```
214
+
215
+ Order matters — first non-404 wins. See the module README for full dispatch rules.
216
+
217
+ ## DX summary — what makes this streamlined
218
+
219
+ - **One file mounts everything.** `src/pages/api/[...rest].ts` is 4 lines; all routing happens inside the Hono app(s) you already built.
220
+ - **Same builders power runtime AND codegen.** Your procedure registration is the single source of truth — types can't drift between server and client.
221
+ - **`getAstroContext(c)` inside the factory closure.** Full `APIContext` (locals, cookies, redirect, params) is available without leaking Astro into your procedure handlers.
222
+ - **Generated client is self-contained.** No runtime dependency on `ts-procedures/client`; ships as plain `.ts` files you can read and audit.
223
+ - **`.safe()` on every callable.** Opt into `Result<T, TypedErrors>` per call site instead of try/catch — typed against the route's declared `errors`.
224
+ - **Streams just work.** `HonoStreamAppBuilder` returns a `Response` with a `ReadableStream` body; Astro SSR forwards it verbatim, and client disconnects abort `ctx.signal`.
225
+ - **Multi-app composition.** Mix API + RPC + Stream surfaces under one catch-all with first-match dispatch.
226
+
227
+ The single biggest footgun: `pathPrefix` MUST match the catch-all directory. Mismatch produces silent 404s. See the troubleshooting section above.
package/docs/core.md CHANGED
@@ -10,6 +10,7 @@ The `Procedures()` function creates a factory for defining procedures. It accept
10
10
 
11
11
  ```typescript
12
12
  Procedures<TContext, TExtendedConfig>(builder?: {
13
+ config?: { noRuntimeValidation?: true }
13
14
  onCreate?: (procedure: TProcedureRegistration<TContext, TExtendedConfig>) => void
14
15
  })
15
16
  ```
@@ -18,6 +19,7 @@ Procedures<TContext, TExtendedConfig>(builder?: {
18
19
  |-----------|----------------------------------------------------------------------------|
19
20
  | `TContext` | The base context type passed to all handlers as the first parameter |
20
21
  | `TExtendedConfig` | Additional configuration properties for all procedures `config` properties |
22
+ | `builder.config.noRuntimeValidation` | When `true`, every procedure created by this factory skips AJV validation of `schema.params` and `schema.input` at call time. Applies to both `Create` and `CreateStream`. Default: validation runs. |
21
23
  | `builder.onCreate` | Optional callback invoked when each procedure is registered (runtime) |
22
24
 
23
25
  ## Create Function
@@ -221,6 +223,22 @@ AJV is configured with:
221
223
 
222
224
  **Note:** `schema.params` is validated at runtime. `schema.returnType` is for documentation/introspection only.
223
225
 
226
+ ### Disabling Runtime Validation
227
+
228
+ For trusted internal callers — for example, a back-of-house factory whose inputs are already type-checked at build time and whose handlers are never invoked from public HTTP — pass `config.noRuntimeValidation: true` when constructing the factory:
229
+
230
+ ```typescript
231
+ const { Create, CreateStream } = Procedures({
232
+ config: { noRuntimeValidation: true },
233
+ })
234
+ ```
235
+
236
+ When set, every procedure registered by this factory skips AJV validation of `schema.params` and `schema.input` on every call. JSON Schema is still computed at registration time (so `info.schema` and codegen are unaffected) — only the per-call validator runs are bypassed.
237
+
238
+ **Use sparingly.** Do not enable this for procedures that accept input from public clients, untyped JSON bodies, or anything you do not control end-to-end. The flag is shaped as `noRuntimeValidation?: true` (only `true` is accepted) to make the opt-out explicit at the call site.
239
+
240
+ This is independent of the per-call `ctx.isPrevalidated` escape hatch used by HTTP builders that have already validated upstream — both paths short-circuit the same validators.
241
+
224
242
  ## Error Handling
225
243
 
226
244
  ### Using ctx.error()
@@ -419,6 +437,7 @@ describe('GetUser', () => {
419
437
  Creates a procedure factory.
420
438
 
421
439
  **Parameters:**
440
+ - `builder.config.noRuntimeValidation` - When `true`, every procedure registered through this factory skips AJV validation of `schema.params` and `schema.input` at call time. Default: validation runs. See [Disabling Runtime Validation](#disabling-runtime-validation).
422
441
  - `builder.onCreate` - Callback invoked when each procedure is registered
423
442
 
424
443
  **Returns:**