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,254 @@
|
|
|
1
|
+
# Astro adapter for ts-procedures
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-07
|
|
4
|
+
**Status:** Design — pending implementation
|
|
5
|
+
**Subpath export:** `ts-procedures/astro`
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
Downstream developers using Astro want to host ts-procedures handlers inside Astro server endpoints (`src/pages/**/*.ts`). Today they must hand-roll the bridge: take an Astro `APIRoute`, convert its `APIContext` into a `Request`, find a way to dispatch through their procedure factories, and translate the result back. Astro's filesystem-as-router model also resists the existing `register(...).build()` pattern, which produces one app for many routes.
|
|
10
|
+
|
|
11
|
+
The goal is a "drop in the entry point" adapter: a single catch-all Astro file delegates every procedure call to one or more already-built ts-procedures apps, with full access to Astro's per-request data (`locals`, `cookies`, `redirect`) inside procedure handlers.
|
|
12
|
+
|
|
13
|
+
## Non-goals
|
|
14
|
+
|
|
15
|
+
- Express RPC support. Express uses Node `req`/`res`, not Web `Request`/`Response`. A bridge is non-trivial (body streams, hijacked sockets, `res.flushHeaders`); defer until requested.
|
|
16
|
+
- Native Astro builders. Mirroring `HonoAPIAppBuilder` as `AstroAPIAppBuilder` would duplicate path-matching, schema validation, error taxonomy, and DocRegistry plumbing. The catch-all pattern covers the request without that cost.
|
|
17
|
+
- Per-procedure file scaffolding (one Astro file per procedure). Out of scope for v1.
|
|
18
|
+
- DocRegistry / codegen integration. Users keep wiring `DocRegistry` against the same builders they pass to the adapter; the adapter returns no `.docs`.
|
|
19
|
+
- `dispatch: 'merge'` strategy. YAGNI — users who want one Hono app can pre-merge with `app.route(...)` themselves.
|
|
20
|
+
|
|
21
|
+
## Approach
|
|
22
|
+
|
|
23
|
+
The adapter is a thin Web-fetch delegator. It accepts already-built Hono apps (from any of `HonoAPIAppBuilder`, `HonoRPCAppBuilder`, `HonoStreamAppBuilder`) and exposes Astro `APIRoute` exports the developer re-exports from a catch-all file.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Browser → /api/users/123
|
|
27
|
+
↓
|
|
28
|
+
Astro invokes ALL({ request, locals, cookies, params, redirect, url, ... })
|
|
29
|
+
↓
|
|
30
|
+
Adapter:
|
|
31
|
+
1. Strip pathPrefix from request URL (if configured) → `rewritten`
|
|
32
|
+
(when no prefix configured, `rewritten` IS `request`)
|
|
33
|
+
2. astroContextMap.set(rewritten, apiContext) — WeakMap stash
|
|
34
|
+
3. For each app in order: response = await app.fetch(rewritten)
|
|
35
|
+
— first response with status !== 404 is returned (a 500 from
|
|
36
|
+
app A stops dispatch; it is treated as a real answer, not a
|
|
37
|
+
miss). Only a literal 404 falls through to the next app.
|
|
38
|
+
4. All apps 404 → new Response(null, { status: 404 })
|
|
39
|
+
↓
|
|
40
|
+
Inside any factory-context closure:
|
|
41
|
+
getAstroContext(c) → reads c.req.raw → WeakMap lookup → APIContext
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The WeakMap is keyed by the `Request` object that Hono ends up seeing — i.e., the post-rewrite Request when `pathPrefix` is configured, and the original Request when it isn't. Hono exposes that same Request on `c.req.raw`, which is what `getAstroContext` reads. Stashing happens AFTER the rewrite so the key matches what Hono observes. Entries clear when the Request is GC'd — no manual cleanup, no leak.
|
|
45
|
+
|
|
46
|
+
## Public API
|
|
47
|
+
|
|
48
|
+
Two exports:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import type { Hono, Context as HonoContext } from 'hono'
|
|
52
|
+
import type { APIContext, APIRoute } from 'astro'
|
|
53
|
+
|
|
54
|
+
export type AstroAdapterConfig = {
|
|
55
|
+
/** One or more built Hono apps. Order matters in 'first-match' dispatch. */
|
|
56
|
+
apps: Hono | Hono[]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Path prefix matching the Astro catch-all mount point.
|
|
60
|
+
* For src/pages/api/[...rest].ts use '/api'. The adapter strips this
|
|
61
|
+
* before delegating, so Hono routes do NOT need to repeat the prefix.
|
|
62
|
+
*
|
|
63
|
+
* Normalization: leading and trailing slashes are optional; '/api',
|
|
64
|
+
* 'api', and '/api/' are equivalent.
|
|
65
|
+
*/
|
|
66
|
+
pathPrefix?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type AstroHandlers = {
|
|
70
|
+
ALL: APIRoute
|
|
71
|
+
GET: APIRoute
|
|
72
|
+
POST: APIRoute
|
|
73
|
+
PUT: APIRoute
|
|
74
|
+
PATCH: APIRoute
|
|
75
|
+
DELETE: APIRoute
|
|
76
|
+
HEAD: APIRoute
|
|
77
|
+
OPTIONS: APIRoute
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createAstroHandler(config: AstroAdapterConfig): AstroHandlers
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read Astro's APIContext from inside a Hono factory-context closure.
|
|
84
|
+
* Throws if called outside an Astro request scope (e.g., if the Hono app
|
|
85
|
+
* is also being served outside the adapter and the closure ran there).
|
|
86
|
+
*/
|
|
87
|
+
export function getAstroContext(c: HonoContext): APIContext
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`createAstroHandler` returns every method handler so the developer can destructure whichever they want. Most consumers use `ALL`. Per-method exports support cases where Astro's behavior diverges by method (e.g., `HEAD` auto-derived from `GET`).
|
|
91
|
+
|
|
92
|
+
## Developer-facing usage
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// src/server/procedures/users.ts
|
|
96
|
+
import { Procedures } from 'ts-procedures'
|
|
97
|
+
import { Type } from 'typebox'
|
|
98
|
+
|
|
99
|
+
export const usersAPI = Procedures<{ db: Db; user: User | null }, APIConfig>()
|
|
100
|
+
|
|
101
|
+
usersAPI.Create('GetUser', {
|
|
102
|
+
path: '/users/:id',
|
|
103
|
+
method: 'get',
|
|
104
|
+
schema: {
|
|
105
|
+
input: { pathParams: Type.Object({ id: Type.String() }) },
|
|
106
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
107
|
+
},
|
|
108
|
+
}, async (ctx, { pathParams }) => {
|
|
109
|
+
return ctx.db.users.findOne(pathParams.id)
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// src/server/api.ts
|
|
115
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
116
|
+
import { getAstroContext } from 'ts-procedures/astro'
|
|
117
|
+
import { usersAPI } from './procedures/users'
|
|
118
|
+
import { db } from './db'
|
|
119
|
+
|
|
120
|
+
export const apiApp = new HonoAPIAppBuilder()
|
|
121
|
+
.register(usersAPI, (c) => {
|
|
122
|
+
const astro = getAstroContext(c)
|
|
123
|
+
return { db, user: astro.locals.user ?? null }
|
|
124
|
+
})
|
|
125
|
+
.build()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// src/pages/api/[...rest].ts
|
|
130
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
131
|
+
import { apiApp } from '../../server/api'
|
|
132
|
+
|
|
133
|
+
export const { ALL } = createAstroHandler({
|
|
134
|
+
apps: apiApp,
|
|
135
|
+
pathPrefix: '/api',
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Multi-app variant:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export const { ALL } = createAstroHandler({
|
|
143
|
+
apps: [apiApp, rpcApp, streamsApp],
|
|
144
|
+
pathPrefix: '/api',
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Path-prefix rewrite
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
function stripPrefix(request: Request, prefix: string | undefined): Request | null {
|
|
152
|
+
if (!prefix) return request
|
|
153
|
+
const url = new URL(request.url)
|
|
154
|
+
const norm = '/' + prefix.replace(/^\/|\/$/g, '') // → '/api'
|
|
155
|
+
if (norm === '/') return request
|
|
156
|
+
if (!url.pathname.startsWith(norm)) return null // 404 fast-path
|
|
157
|
+
const rest = url.pathname.slice(norm.length)
|
|
158
|
+
url.pathname = rest === '' ? '/' : rest
|
|
159
|
+
return new Request(url, request) // copies method/headers/body/signal/duplex
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`new Request(url, request)` per the Web spec init pattern preserves method, headers, body stream, abort signal, and `duplex` mode. Body streams pass through; client disconnect aborts continue to fire on `c.req.raw.signal` inside Hono.
|
|
164
|
+
|
|
165
|
+
Edge cases:
|
|
166
|
+
- `pathPrefix` undefined → no rewrite
|
|
167
|
+
- `pathPrefix` normalizes to `'/'` → no rewrite (root mount)
|
|
168
|
+
- Request path doesn't start with the normalized prefix → adapter returns 404 without invoking any app
|
|
169
|
+
- Exact prefix match (`/api` with request `/api`) → rewrites to `/`
|
|
170
|
+
|
|
171
|
+
## Streams & abort signals
|
|
172
|
+
|
|
173
|
+
Hono stream builders return `Response` with a `ReadableStream` body. Astro SSR forwards that body verbatim — no special handling.
|
|
174
|
+
|
|
175
|
+
`request.signal` flow on client disconnect:
|
|
176
|
+
```
|
|
177
|
+
Browser closes EventSource
|
|
178
|
+
→ Astro aborts request.signal
|
|
179
|
+
→ rewriteRequest preserves signal via new Request(url, request)
|
|
180
|
+
→ Hono's c.req.raw.signal === request.signal
|
|
181
|
+
→ Stream builder's ctx.signal fires (reason 'client-disconnect')
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Same path the existing Hono stream tests cover; one new integration test wires Astro→Hono-stream end-to-end.
|
|
185
|
+
|
|
186
|
+
## Error handling
|
|
187
|
+
|
|
188
|
+
The adapter performs no error handling of its own. Errors flow through whatever the underlying Hono builder is configured with — taxonomy, `onError`, `onRequestError`. The adapter returns whatever Response Hono produced.
|
|
189
|
+
|
|
190
|
+
The only Response the adapter constructs itself: a stock `new Response(null, { status: 404 })` returned when (a) the path-prefix rewrite fails (request path outside the mount), or (b) every registered app returned 404.
|
|
191
|
+
|
|
192
|
+
If `getAstroContext(c)` is called outside an Astro request (the Hono app is also being served via a non-Astro path and the closure ran there), it throws with a clear message. This propagates into Hono's normal error path and through the configured taxonomy.
|
|
193
|
+
|
|
194
|
+
## File layout
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
src/implementations/http/astro/
|
|
198
|
+
├── README.md
|
|
199
|
+
├── index.ts # public exports
|
|
200
|
+
├── create-handler.ts # createAstroHandler, multi-app dispatch
|
|
201
|
+
├── astro-context.ts # WeakMap + getAstroContext
|
|
202
|
+
├── rewrite-request.ts # stripPrefix
|
|
203
|
+
└── index.test.ts # integration tests vs real Hono builders
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Tests
|
|
207
|
+
|
|
208
|
+
Integration-style, real builders, simulated `Request`:
|
|
209
|
+
|
|
210
|
+
1. Single Hono API app — GET and POST roundtrip, request/response bodies intact, headers preserved
|
|
211
|
+
2. Path prefix normalization: `/api`, `api`, `/api/`, exact-match → root, missing prefix → 404 short-circuit, no prefix configured, query string preserved through the rewrite (`/api/users?limit=10` → inner app sees `/users?limit=10`)
|
|
212
|
+
3. `getAstroContext(c)` returns the same `APIContext` object passed to `ALL`
|
|
213
|
+
4. Multi-app first-match: app A returns 404 → app B handles → app B's response returned
|
|
214
|
+
5. Multi-app non-404 short-circuit: app A returns 500 → app B is NOT invoked; app A's 500 is returned
|
|
215
|
+
6. All-404 fallback returns adapter's 404 (status 404, empty body), not Hono's
|
|
216
|
+
7. Stream pass-through: `HonoStreamAppBuilder` + generator yielding 3 events; verify all 3 reach the consumer through the adapter
|
|
217
|
+
8. Abort signal flow: simulate client disconnect by aborting the incoming `Request` mid-stream; assert `ctx.signal.aborted === true` inside the handler with `signal.reason !== 'stream-completed'`
|
|
218
|
+
9. `getAstroContext` outside Astro scope throws a clear error
|
|
219
|
+
|
|
220
|
+
## Package wiring
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
// package.json (additions)
|
|
224
|
+
"exports": {
|
|
225
|
+
"./astro": {
|
|
226
|
+
"types": "./build/implementations/http/astro/index.d.ts",
|
|
227
|
+
"import": "./build/implementations/http/astro/index.js"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"optionalDependencies": {
|
|
231
|
+
"ajsc": "...",
|
|
232
|
+
"express": "...",
|
|
233
|
+
"hono": "...",
|
|
234
|
+
"astro": "^5.0.0"
|
|
235
|
+
},
|
|
236
|
+
"devDependencies": {
|
|
237
|
+
"...": "...",
|
|
238
|
+
"astro": "^5.0.0"
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Matching the existing pattern for `hono` and `express`: listed as `optionalDependencies` so it isn't pulled into non-Astro deployments, and as `devDependencies` so types resolve at build time. Astro is imported via `import type` only — zero runtime dependency on the published package.
|
|
243
|
+
|
|
244
|
+
## Agent config / docs
|
|
245
|
+
|
|
246
|
+
Mirror the pattern used when other builders shipped:
|
|
247
|
+
- New `docs/astro-adapter.md` end-to-end walkthrough
|
|
248
|
+
- Update `agent_config/claude-code/skills/ts-procedures/api-reference.md` and `patterns.md` to teach downstream Claude/Cursor/Copilot rules the Astro path
|
|
249
|
+
- New `agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.ts` template
|
|
250
|
+
- `src/implementations/http/astro/README.md` with usage and the prefix-semantics table
|
|
251
|
+
|
|
252
|
+
## Open questions
|
|
253
|
+
|
|
254
|
+
None at design time. Ready for implementation planning.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-procedures",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
|
|
5
5
|
"main": "build/exports.js",
|
|
6
6
|
"types": "build/exports.d.ts",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./build/exports.d.ts",
|
|
24
24
|
"import": "./build/exports.js"
|
|
25
25
|
},
|
|
26
|
+
"./astro": {
|
|
27
|
+
"types": "./build/implementations/http/astro/index.d.ts",
|
|
28
|
+
"import": "./build/implementations/http/astro/index.js"
|
|
29
|
+
},
|
|
26
30
|
"./http": {
|
|
27
31
|
"types": "./build/implementations/types.d.ts"
|
|
28
32
|
},
|
|
@@ -75,10 +79,29 @@
|
|
|
75
79
|
"validation",
|
|
76
80
|
"procedures",
|
|
77
81
|
"api",
|
|
78
|
-
"framework"
|
|
82
|
+
"framework",
|
|
83
|
+
"hono",
|
|
84
|
+
"express",
|
|
85
|
+
"http",
|
|
86
|
+
"rest",
|
|
87
|
+
"schema",
|
|
88
|
+
"json-schema",
|
|
89
|
+
"typebox",
|
|
90
|
+
"ajv",
|
|
91
|
+
"schema-validation",
|
|
92
|
+
"type-inference",
|
|
93
|
+
"codegen",
|
|
94
|
+
"code-generation",
|
|
95
|
+
"client-generation",
|
|
96
|
+
"streaming",
|
|
97
|
+
"sse",
|
|
98
|
+
"server-sent-events",
|
|
99
|
+
"endpoint",
|
|
100
|
+
"trpc"
|
|
79
101
|
],
|
|
80
102
|
"optionalDependencies": {
|
|
81
|
-
"ajsc": "7.2.0",
|
|
103
|
+
"ajsc": "^7.2.0",
|
|
104
|
+
"astro": "6.x.x || 7.x.x",
|
|
82
105
|
"express": "^5.2.1",
|
|
83
106
|
"hono": "^4.7.4"
|
|
84
107
|
},
|
|
@@ -93,6 +116,7 @@
|
|
|
93
116
|
"@eslint/js": "^9.17.0",
|
|
94
117
|
"@types/express": "^5.0.6",
|
|
95
118
|
"@types/supertest": "^7.2.0",
|
|
119
|
+
"astro": "^6.3.1",
|
|
96
120
|
"eslint": "^9.17.0",
|
|
97
121
|
"express": "^5.2.1",
|
|
98
122
|
"hono": "^4.7.4",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `ts-procedures/astro`
|
|
2
|
+
|
|
3
|
+
Drop one or more pre-built Hono apps (from `HonoAPIAppBuilder`, `HonoRPCAppBuilder`, or `HonoStreamAppBuilder`) into a single Astro catch-all endpoint. Procedure handlers can read Astro's per-request data (`locals`, `cookies`, `redirect`, `params`) inside their factory-context closures.
|
|
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
|
+
## Install
|
|
8
|
+
|
|
9
|
+
The adapter is part of `ts-procedures`. Astro is an optional peer; install it in your Astro app:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install ts-procedures hono astro
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// src/server/api.ts
|
|
19
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
20
|
+
import { getAstroContext } from 'ts-procedures/astro'
|
|
21
|
+
import { usersAPI } from './procedures/users'
|
|
22
|
+
import { db } from './db'
|
|
23
|
+
|
|
24
|
+
export const apiApp = new HonoAPIAppBuilder()
|
|
25
|
+
.register(usersAPI, (c) => {
|
|
26
|
+
const astro = getAstroContext(c)
|
|
27
|
+
return { db, user: astro.locals.user ?? null }
|
|
28
|
+
})
|
|
29
|
+
.build()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// src/pages/api/[...rest].ts
|
|
34
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
35
|
+
import { apiApp } from '../../server/api'
|
|
36
|
+
|
|
37
|
+
export const { ALL } = createAstroHandler({
|
|
38
|
+
apps: apiApp,
|
|
39
|
+
pathPrefix: '/api',
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Config reference
|
|
44
|
+
|
|
45
|
+
| Option | Type | Description |
|
|
46
|
+
| ------------ | --------------------- | -------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `apps` | `Hono \| Hono[]` | One or more built Hono apps. Order matters in first-match dispatch. |
|
|
48
|
+
| `pathPrefix` | `string \| undefined` | Path prefix matching the Astro catch-all mount point. `/api`, `api`, `/api/` are equivalent. |
|
|
49
|
+
|
|
50
|
+
## Path prefix semantics
|
|
51
|
+
|
|
52
|
+
The catch-all file is at `src/pages/api/[...rest].ts`, so Astro receives requests with paths like `/api/users/123`. The Hono builder routes you registered probably use just `/users/:id`. Configuring `pathPrefix: '/api'` strips the prefix before delegating — your Hono routes don't need to repeat the prefix.
|
|
53
|
+
|
|
54
|
+
| Request path | `pathPrefix` | Inner Hono sees |
|
|
55
|
+
| ---------------- | ------------ | --------------------- |
|
|
56
|
+
| `/api/users/123` | `'/api'` | `/users/123` |
|
|
57
|
+
| `/api` | `'/api'` | `/` |
|
|
58
|
+
| `/somewhere` | `'/api'` | (404, no app invoked) |
|
|
59
|
+
| `/users/123` | `undefined` | `/users/123` |
|
|
60
|
+
|
|
61
|
+
The prefix match is segment-aware: `/apikey/secret` does NOT match `pathPrefix: '/api'` and will return 404.
|
|
62
|
+
|
|
63
|
+
**Diagnostic:** if your routes 404 unexpectedly, the most common cause is a `pathPrefix` mismatch. The prefix MUST match the directory the catch-all file lives in. For `src/pages/api/[...rest].ts` use `pathPrefix: '/api'`; for `src/pages/v1/[...rest].ts` use `pathPrefix: '/v1'`.
|
|
64
|
+
|
|
65
|
+
## Multi-app dispatch
|
|
66
|
+
|
|
67
|
+
Pass an array. The adapter tries each app in order:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export const { ALL } = createAstroHandler({
|
|
71
|
+
apps: [apiApp, rpcApp, streamsApp],
|
|
72
|
+
pathPrefix: '/api',
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Dispatch rules:
|
|
77
|
+
- The first response with `status !== 404` is returned.
|
|
78
|
+
- A `500` from app A stops dispatch (it is treated as a real answer, not a miss).
|
|
79
|
+
- Only literal 404s fall through to the next app.
|
|
80
|
+
- All apps 404 → adapter returns its own `Response(null, { status: 404 })`.
|
|
81
|
+
|
|
82
|
+
## Streams
|
|
83
|
+
|
|
84
|
+
`HonoStreamAppBuilder` returns a `Response` with a `ReadableStream` body. Astro SSR forwards that body verbatim — no additional configuration. Client disconnects abort `ctx.signal` inside the stream handler.
|
|
85
|
+
|
|
86
|
+
## What's NOT included
|
|
87
|
+
|
|
88
|
+
- Express RPC support (Express uses Node `req`/`res`, not Web Fetch).
|
|
89
|
+
- DocRegistry coupling — wire `DocRegistry` against the same builders separately for client codegen.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { APIContext } from 'astro'
|
|
2
|
+
import type { Context as HonoContext } from 'hono'
|
|
3
|
+
|
|
4
|
+
const astroContextMap = new WeakMap<Request, APIContext>()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal — stash the Astro APIContext for the Request that Hono will receive.
|
|
8
|
+
*
|
|
9
|
+
* The key MUST be the post-rewrite Request (the one passed to `app.fetch`),
|
|
10
|
+
* because that is what Hono exposes via `c.req.raw` and what `getAstroContext`
|
|
11
|
+
* reads back. When no `pathPrefix` rewrite happens, this is the same Request
|
|
12
|
+
* that Astro originally invoked the adapter with.
|
|
13
|
+
*/
|
|
14
|
+
export function setAstroContext(request: Request, apiContext: APIContext): void {
|
|
15
|
+
astroContextMap.set(request, apiContext)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reads Astro's APIContext from inside a Hono factory-context closure.
|
|
20
|
+
*
|
|
21
|
+
* Throws when the request is not in scope — typically because the Hono app
|
|
22
|
+
* is being served outside the Astro adapter (e.g., bound to a Node listener
|
|
23
|
+
* or via a different adapter) and the closure was invoked there.
|
|
24
|
+
*/
|
|
25
|
+
export function getAstroContext(c: HonoContext): APIContext {
|
|
26
|
+
const ctx = astroContextMap.get(c.req.raw)
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'getAstroContext was called outside an Astro request scope. ' +
|
|
30
|
+
'Ensure this Hono app is being served via createAstroHandler when this closure runs.'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return ctx
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Hono } from 'hono'
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
3
|
+
import { setAstroContext } from './astro-context.js'
|
|
4
|
+
import { stripPrefix } from './rewrite-request.js'
|
|
5
|
+
|
|
6
|
+
export type AstroAdapterConfig = {
|
|
7
|
+
/** One or more built Hono apps. Order matters in first-match dispatch. */
|
|
8
|
+
apps: Hono | Hono[]
|
|
9
|
+
/**
|
|
10
|
+
* Path prefix matching the Astro catch-all mount point.
|
|
11
|
+
* For src/pages/api/[...rest].ts use '/api'. The adapter strips this
|
|
12
|
+
* before delegating, so Hono routes do NOT need to repeat the prefix.
|
|
13
|
+
*
|
|
14
|
+
* Normalization: leading and trailing slashes are optional; '/api',
|
|
15
|
+
* 'api', and '/api/' are equivalent. Undefined or '/' means no rewrite.
|
|
16
|
+
*/
|
|
17
|
+
pathPrefix?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type AstroHandlers = {
|
|
21
|
+
ALL: APIRoute
|
|
22
|
+
GET: APIRoute
|
|
23
|
+
POST: APIRoute
|
|
24
|
+
PUT: APIRoute
|
|
25
|
+
PATCH: APIRoute
|
|
26
|
+
DELETE: APIRoute
|
|
27
|
+
HEAD: APIRoute
|
|
28
|
+
OPTIONS: APIRoute
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const
|
|
32
|
+
|
|
33
|
+
export function createAstroHandler(config: AstroAdapterConfig): AstroHandlers {
|
|
34
|
+
const apps = Array.isArray(config.apps) ? config.apps : [config.apps]
|
|
35
|
+
|
|
36
|
+
if (apps.length === 0) {
|
|
37
|
+
throw new Error('createAstroHandler: `apps` must contain at least one Hono app.')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ALL: APIRoute = async (apiContext) => {
|
|
41
|
+
const rewritten = stripPrefix(apiContext.request, config.pathPrefix)
|
|
42
|
+
if (rewritten === null) {
|
|
43
|
+
return new Response(null, { status: 404 })
|
|
44
|
+
}
|
|
45
|
+
setAstroContext(rewritten, apiContext)
|
|
46
|
+
|
|
47
|
+
for (const app of apps) {
|
|
48
|
+
const res = await app.fetch(rewritten)
|
|
49
|
+
if (res.status !== 404) return res
|
|
50
|
+
}
|
|
51
|
+
return new Response(null, { status: 404 })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handlers = { ALL } as AstroHandlers
|
|
55
|
+
for (const method of HTTP_METHODS) {
|
|
56
|
+
handlers[method] = ALL
|
|
57
|
+
}
|
|
58
|
+
return handlers
|
|
59
|
+
}
|