ts-procedures 7.1.1 → 7.2.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.
- package/README.md +8 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +36 -0
- package/agent_config/claude-code/skills/ts-procedures/patterns.md +14 -0
- package/agent_config/claude-code/skills/ts-procedures-scaffold/SKILL.md +1 -0
- package/agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.md +23 -0
- package/agent_config/copilot/copilot-instructions.md +4 -0
- package/agent_config/cursor/cursorrules +4 -0
- package/build/codegen/bin/cli.js +0 -0
- package/build/implementations/http/astro/astro-context.d.ts +19 -0
- package/build/implementations/http/astro/astro-context.js +28 -0
- package/build/implementations/http/astro/astro-context.js.map +1 -0
- package/build/implementations/http/astro/create-handler.d.ts +26 -0
- package/build/implementations/http/astro/create-handler.js +28 -0
- package/build/implementations/http/astro/create-handler.js.map +1 -0
- package/build/implementations/http/astro/index.d.ts +3 -0
- package/build/implementations/http/astro/index.js +6 -0
- package/build/implementations/http/astro/index.js.map +1 -0
- package/build/implementations/http/astro/index.test.d.ts +1 -0
- package/build/implementations/http/astro/index.test.js +295 -0
- package/build/implementations/http/astro/index.test.js.map +1 -0
- package/build/implementations/http/astro/rewrite-request.d.ts +13 -0
- package/build/implementations/http/astro/rewrite-request.js +32 -0
- package/build/implementations/http/astro/rewrite-request.js.map +1 -0
- package/docs/astro-adapter.md +227 -0
- package/docs/superpowers/plans/2026-05-07-astro-adapter.md +1396 -0
- package/docs/superpowers/specs/2026-05-07-astro-adapter-design.md +254 -0
- package/package.json +27 -3
- package/src/implementations/http/astro/README.md +89 -0
- package/src/implementations/http/astro/astro-context.ts +34 -0
- package/src/implementations/http/astro/create-handler.ts +59 -0
- package/src/implementations/http/astro/index.test.ts +350 -0
- package/src/implementations/http/astro/index.ts +6 -0
- package/src/implementations/http/astro/rewrite-request.ts +31 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-request.js","sourceRoot":"","sources":["../../../../src/implementations/http/astro/rewrite-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB,EAAE,MAA0B;IACtE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,OAAO,CAAA;IACxC,MAAM,UAAU,GAAG,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IACvD,IAAI,UAAU,KAAK,GAAG;QAAE,OAAO,OAAO,CAAA;IAEtC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAErD,2EAA2E;IAC3E,yEAAyE;IACzE,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IAChD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,GAAG;QAAE,OAAO,IAAI,CAAA;IAE3D,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;IAClD,GAAG,CAAC,QAAQ,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;IAEvC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;AAClC,CAAC"}
|
|
@@ -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.
|