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.
- package/README.md +8 -0
- package/agent_config/claude-code/skills/ts-procedures/anti-patterns.md +35 -0
- package/agent_config/claude-code/skills/ts-procedures/api-reference.md +40 -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 +6 -0
- package/agent_config/cursor/cursorrules +6 -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/build/index.d.ts +10 -0
- package/build/index.js +12 -13
- package/build/index.js.map +1 -1
- package/build/index.test.js +107 -0
- package/build/index.test.js.map +1 -1
- package/docs/astro-adapter.md +227 -0
- package/docs/core.md +19 -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 +8 -2
- 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
- package/src/index.test.ts +171 -0
- package/src/index.ts +27 -15
|
@@ -0,0 +1,1396 @@
|
|
|
1
|
+
# Astro Adapter Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Ship a new `ts-procedures/astro` subpath that lets a downstream developer drop one or more pre-built Hono apps (from `HonoAPIAppBuilder`, `HonoRPCAppBuilder`, `HonoStreamAppBuilder`) into a single catch-all Astro endpoint file (`src/pages/api/[...rest].ts`) and have every procedure call route correctly with full access to Astro's per-request data inside factory-context closures.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Web-fetch delegator — accept already-built Hono apps via `app.fetch(request)`. The adapter (a) optionally strips a configured `pathPrefix` from the incoming `Request` URL, (b) stashes Astro's `APIContext` in a `WeakMap` keyed by the rewritten Request, (c) dispatches across the registered apps with first-non-404-wins, (d) returns the response. A second exported helper, `getAstroContext(c)`, lets factory closures read the stashed `APIContext` from the Hono `Context` (via `c.req.raw`). No Hono builder is modified; Astro is types-only and listed under `optionalDependencies`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (ESM, NodeNext), vitest, Hono (already a dev/optional dep), Astro 5 (new `optionalDependencies` + `devDependencies` entry, `import type` only).
|
|
10
|
+
|
|
11
|
+
**Source spec:** `docs/superpowers/specs/2026-05-07-astro-adapter-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
**New files:**
|
|
18
|
+
- `src/implementations/http/astro/astro-context.ts` — module-private `WeakMap<Request, APIContext>`, internal `setAstroContext(req, ctx)`, public `getAstroContext(c)` reading via `c.req.raw`. Throws `Error` when the request is not in scope.
|
|
19
|
+
- `src/implementations/http/astro/rewrite-request.ts` — pure function `stripPrefix(request, prefix)`: returns the original request when no prefix, the rewritten request when prefix matches, or `null` (404 short-circuit) when it doesn't match.
|
|
20
|
+
- `src/implementations/http/astro/create-handler.ts` — `createAstroHandler({ apps, pathPrefix })` returning the per-method `APIRoute` map. Internal multi-app dispatch with first-non-404 short-circuit.
|
|
21
|
+
- `src/implementations/http/astro/index.ts` — public barrel: re-exports `createAstroHandler`, `getAstroContext`, and the `AstroAdapterConfig` type.
|
|
22
|
+
- `src/implementations/http/astro/index.test.ts` — vitest suite with all unit + integration tests in one file (matches the convention used by `hono-api/index.test.ts`).
|
|
23
|
+
- `src/implementations/http/astro/README.md` — usage docs, prefix semantics, examples.
|
|
24
|
+
- `docs/astro-adapter.md` — end-to-end walkthrough.
|
|
25
|
+
- `agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.ts` — scaffold template.
|
|
26
|
+
|
|
27
|
+
**Modified files:**
|
|
28
|
+
- `package.json` — add `./astro` to `exports`, add `astro` to `optionalDependencies` and `devDependencies`.
|
|
29
|
+
- `agent_config/claude-code/skills/ts-procedures/api-reference.md` — add Astro adapter section.
|
|
30
|
+
- `agent_config/claude-code/skills/ts-procedures/patterns.md` — add Astro pattern.
|
|
31
|
+
- `agent_config/copilot/copilot-instructions.md` and `agent_config/cursor/cursorrules` — add the same Astro pattern inside `<!-- BEGIN/END ts-procedures -->` markers (these two files are kept identical per CLAUDE.md).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Task 1: Module skeleton + package.json wiring
|
|
36
|
+
|
|
37
|
+
**Files:**
|
|
38
|
+
- Create: `src/implementations/http/astro/index.ts`
|
|
39
|
+
- Create: `src/implementations/http/astro/astro-context.ts`
|
|
40
|
+
- Create: `src/implementations/http/astro/rewrite-request.ts`
|
|
41
|
+
- Create: `src/implementations/http/astro/create-handler.ts`
|
|
42
|
+
- Create: `src/implementations/http/astro/index.test.ts`
|
|
43
|
+
- Modify: `package.json`
|
|
44
|
+
|
|
45
|
+
- [ ] **Step 1: Install Astro as a devDependency**
|
|
46
|
+
|
|
47
|
+
Run from the project root:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install --save-dev --save-optional astro@^5.0.0
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Expected: `astro` appears in both `devDependencies` and `optionalDependencies`. (npm will write `optionalDependencies` because of `--save-optional`; verify it also lands in `devDependencies` — if not, manually add it to `devDependencies` so types resolve at build time.)
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 2: Verify package.json now has astro in both sections**
|
|
56
|
+
|
|
57
|
+
Open `package.json` and confirm both:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
"optionalDependencies": {
|
|
61
|
+
"ajsc": "7.2.0",
|
|
62
|
+
"astro": "^5.0.0",
|
|
63
|
+
"express": "^5.2.1",
|
|
64
|
+
"hono": "^4.7.4"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"...": "...",
|
|
68
|
+
"astro": "^5.0.0"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If `astro` is missing from `devDependencies`, add it manually with the same version range.
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: Add the `./astro` subpath export**
|
|
75
|
+
|
|
76
|
+
In `package.json`, add to the `exports` object (alphabetical placement — between `.` and `./client` is fine):
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
"./astro": {
|
|
80
|
+
"types": "./build/implementations/http/astro/index.d.ts",
|
|
81
|
+
"import": "./build/implementations/http/astro/index.js"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 4: Create empty source files**
|
|
86
|
+
|
|
87
|
+
Create the following with minimal placeholder content so TypeScript resolves them:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// src/implementations/http/astro/astro-context.ts
|
|
91
|
+
export {}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// src/implementations/http/astro/rewrite-request.ts
|
|
96
|
+
export {}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// src/implementations/http/astro/create-handler.ts
|
|
101
|
+
export {}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// src/implementations/http/astro/index.ts
|
|
106
|
+
export {}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// src/implementations/http/astro/index.test.ts
|
|
111
|
+
import { describe } from 'vitest'
|
|
112
|
+
describe.skip('astro adapter — placeholder', () => {})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- [ ] **Step 5: Run the build and tests to confirm the skeleton compiles**
|
|
116
|
+
|
|
117
|
+
Run:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run build && npm test -- src/implementations/http/astro
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Expected: build succeeds, the placeholder describe is skipped, test run passes with 0 failures.
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 6: Commit**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git add package.json package-lock.json src/implementations/http/astro/
|
|
129
|
+
git commit -m "feat(astro): scaffold ts-procedures/astro subpath"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Task 2: stripPrefix helper
|
|
135
|
+
|
|
136
|
+
**Files:**
|
|
137
|
+
- Modify: `src/implementations/http/astro/rewrite-request.ts`
|
|
138
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
139
|
+
|
|
140
|
+
- [ ] **Step 1: Write the failing tests**
|
|
141
|
+
|
|
142
|
+
Replace the contents of `src/implementations/http/astro/index.test.ts` with:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { describe, expect, test } from 'vitest'
|
|
146
|
+
import { stripPrefix } from './rewrite-request.js'
|
|
147
|
+
|
|
148
|
+
describe('stripPrefix', () => {
|
|
149
|
+
test('returns original request when prefix is undefined', () => {
|
|
150
|
+
const req = new Request('https://example.test/api/users/1')
|
|
151
|
+
expect(stripPrefix(req, undefined)).toBe(req)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('returns original request when normalized prefix is "/"', () => {
|
|
155
|
+
const req = new Request('https://example.test/users/1')
|
|
156
|
+
expect(stripPrefix(req, '/')).toBe(req)
|
|
157
|
+
expect(stripPrefix(req, '')).toBe(req)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('strips a leading-slash prefix', () => {
|
|
161
|
+
const req = new Request('https://example.test/api/users/1')
|
|
162
|
+
const out = stripPrefix(req, '/api')!
|
|
163
|
+
expect(new URL(out.url).pathname).toBe('/users/1')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('strips a no-slash prefix (normalized)', () => {
|
|
167
|
+
const req = new Request('https://example.test/api/users/1')
|
|
168
|
+
const out = stripPrefix(req, 'api')!
|
|
169
|
+
expect(new URL(out.url).pathname).toBe('/users/1')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('strips a trailing-slash prefix (normalized)', () => {
|
|
173
|
+
const req = new Request('https://example.test/api/users/1')
|
|
174
|
+
const out = stripPrefix(req, '/api/')!
|
|
175
|
+
expect(new URL(out.url).pathname).toBe('/users/1')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('exact-match prefix becomes "/"', () => {
|
|
179
|
+
const req = new Request('https://example.test/api')
|
|
180
|
+
const out = stripPrefix(req, '/api')!
|
|
181
|
+
expect(new URL(out.url).pathname).toBe('/')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('returns null when path does not start with prefix (404 short-circuit)', () => {
|
|
185
|
+
const req = new Request('https://example.test/other/users/1')
|
|
186
|
+
expect(stripPrefix(req, '/api')).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('returns null when path shares prefix characters but not a segment boundary', () => {
|
|
190
|
+
// /apikey starts with /api but is not under /api — must NOT rewrite.
|
|
191
|
+
expect(stripPrefix(new Request('https://example.test/apikey/secret'), '/api')).toBeNull()
|
|
192
|
+
expect(stripPrefix(new Request('https://example.test/api-v2/x'), '/api')).toBeNull()
|
|
193
|
+
expect(stripPrefix(new Request('https://example.test/api_internal'), '/api')).toBeNull()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('preserves the query string through the rewrite', () => {
|
|
197
|
+
const req = new Request('https://example.test/api/users?limit=10&tag=a&tag=b')
|
|
198
|
+
const out = stripPrefix(req, '/api')!
|
|
199
|
+
const url = new URL(out.url)
|
|
200
|
+
expect(url.pathname).toBe('/users')
|
|
201
|
+
expect(url.search).toBe('?limit=10&tag=a&tag=b')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('preserves the request method', () => {
|
|
205
|
+
const req = new Request('https://example.test/api/users', { method: 'POST' })
|
|
206
|
+
const out = stripPrefix(req, '/api')!
|
|
207
|
+
expect(out.method).toBe('POST')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('preserves headers', () => {
|
|
211
|
+
const req = new Request('https://example.test/api/users', {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'X-Trace-Id': 'abc-123', 'Content-Type': 'application/json' },
|
|
214
|
+
})
|
|
215
|
+
const out = stripPrefix(req, '/api')!
|
|
216
|
+
expect(out.headers.get('X-Trace-Id')).toBe('abc-123')
|
|
217
|
+
expect(out.headers.get('Content-Type')).toBe('application/json')
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 2: Run the tests to verify they fail**
|
|
223
|
+
|
|
224
|
+
Run:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npm test -- src/implementations/http/astro
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Expected: all `stripPrefix` tests fail with `stripPrefix is not a function` or import error.
|
|
231
|
+
|
|
232
|
+
- [ ] **Step 3: Implement `stripPrefix`**
|
|
233
|
+
|
|
234
|
+
Replace `src/implementations/http/astro/rewrite-request.ts` with:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
/**
|
|
238
|
+
* Returns a Request with `prefix` stripped from the URL pathname.
|
|
239
|
+
*
|
|
240
|
+
* - `prefix` undefined or normalizing to '/' → returns the original request unchanged.
|
|
241
|
+
* - Path does not start with the normalized prefix at a segment boundary → returns `null`
|
|
242
|
+
* (caller short-circuits to 404). E.g. with prefix '/api', the request '/apikey'
|
|
243
|
+
* returns null because 'key' is not preceded by a path separator.
|
|
244
|
+
* - Otherwise → returns a new Request with the rewritten URL and method/headers/body/signal/duplex
|
|
245
|
+
* preserved via `new Request(url, init)`.
|
|
246
|
+
*
|
|
247
|
+
* Normalization: leading and trailing slashes are optional. '/api', 'api', and '/api/' are equivalent.
|
|
248
|
+
*/
|
|
249
|
+
export function stripPrefix(request: Request, prefix: string | undefined): Request | null {
|
|
250
|
+
if (prefix === undefined) return request
|
|
251
|
+
const normalized = '/' + prefix.replace(/^\/|\/$/g, '')
|
|
252
|
+
if (normalized === '/') return request
|
|
253
|
+
|
|
254
|
+
const url = new URL(request.url)
|
|
255
|
+
if (!url.pathname.startsWith(normalized)) return null
|
|
256
|
+
|
|
257
|
+
// Segment-boundary guard: the character right after the prefix in the path
|
|
258
|
+
// MUST be either undefined (exact match) or '/' (real segment boundary).
|
|
259
|
+
// Without this, '/apikey' would falsely match prefix '/api'.
|
|
260
|
+
const nextChar = url.pathname[normalized.length]
|
|
261
|
+
if (nextChar !== undefined && nextChar !== '/') return null
|
|
262
|
+
|
|
263
|
+
const rest = url.pathname.slice(normalized.length)
|
|
264
|
+
url.pathname = rest === '' ? '/' : rest
|
|
265
|
+
|
|
266
|
+
return new Request(url, request)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- [ ] **Step 4: Run the tests to verify they pass**
|
|
271
|
+
|
|
272
|
+
Run:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
npm test -- src/implementations/http/astro
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Expected: all 11 `stripPrefix` tests pass.
|
|
279
|
+
|
|
280
|
+
- [ ] **Step 5: Commit**
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
git add src/implementations/http/astro/rewrite-request.ts src/implementations/http/astro/index.test.ts
|
|
284
|
+
git commit -m "feat(astro): add stripPrefix URL-rewrite helper"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Task 3: astro-context (WeakMap + getAstroContext)
|
|
290
|
+
|
|
291
|
+
**Files:**
|
|
292
|
+
- Modify: `src/implementations/http/astro/astro-context.ts`
|
|
293
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
294
|
+
|
|
295
|
+
- [ ] **Step 1: Write the failing tests**
|
|
296
|
+
|
|
297
|
+
Append to `src/implementations/http/astro/index.test.ts` (keep the existing `stripPrefix` block above):
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import { Hono } from 'hono'
|
|
301
|
+
import { setAstroContext, getAstroContext } from './astro-context.js'
|
|
302
|
+
|
|
303
|
+
// Minimal stand-in for Astro's APIContext for unit tests.
|
|
304
|
+
function fakeApiContext(overrides: Record<string, unknown> = {}) {
|
|
305
|
+
return {
|
|
306
|
+
locals: { user: { id: 'u-1' } },
|
|
307
|
+
cookies: {},
|
|
308
|
+
params: {},
|
|
309
|
+
request: new Request('https://example.test/'),
|
|
310
|
+
url: new URL('https://example.test/'),
|
|
311
|
+
redirect: () => new Response(null, { status: 302 }),
|
|
312
|
+
...overrides,
|
|
313
|
+
} as unknown as import('astro').APIContext
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
describe('astro-context', () => {
|
|
317
|
+
test('getAstroContext returns the APIContext stashed for the request seen by Hono', async () => {
|
|
318
|
+
const req = new Request('https://example.test/probe')
|
|
319
|
+
const apiContext = fakeApiContext()
|
|
320
|
+
setAstroContext(req, apiContext)
|
|
321
|
+
|
|
322
|
+
const app = new Hono()
|
|
323
|
+
let observed: import('astro').APIContext | null = null
|
|
324
|
+
app.get('/probe', (c) => {
|
|
325
|
+
observed = getAstroContext(c)
|
|
326
|
+
return c.json({ ok: true })
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
const res = await app.fetch(req)
|
|
330
|
+
expect(res.status).toBe(200)
|
|
331
|
+
expect(observed).toBe(apiContext)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
test('getAstroContext throws when called outside an Astro request scope', async () => {
|
|
335
|
+
const app = new Hono()
|
|
336
|
+
let captured: unknown = null
|
|
337
|
+
app.get('/probe', (c) => {
|
|
338
|
+
try {
|
|
339
|
+
getAstroContext(c)
|
|
340
|
+
} catch (err) {
|
|
341
|
+
captured = err
|
|
342
|
+
}
|
|
343
|
+
return c.json({ ok: true })
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
await app.fetch(new Request('https://example.test/probe'))
|
|
347
|
+
expect(captured).toBeInstanceOf(Error)
|
|
348
|
+
expect((captured as Error).message).toMatch(/outside an Astro request scope/i)
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
354
|
+
|
|
355
|
+
Run:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
npm test -- src/implementations/http/astro
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Expected: the two `astro-context` tests fail (`setAstroContext` / `getAstroContext` not exported).
|
|
362
|
+
|
|
363
|
+
- [ ] **Step 3: Implement `astro-context.ts`**
|
|
364
|
+
|
|
365
|
+
Replace `src/implementations/http/astro/astro-context.ts` with:
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
import type { APIContext } from 'astro'
|
|
369
|
+
import type { Context as HonoContext } from 'hono'
|
|
370
|
+
|
|
371
|
+
const astroContextMap = new WeakMap<Request, APIContext>()
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Internal — stash the Astro APIContext for the Request that Hono will receive.
|
|
375
|
+
*
|
|
376
|
+
* The key MUST be the post-rewrite Request (the one passed to `app.fetch`),
|
|
377
|
+
* because that is what Hono exposes via `c.req.raw` and what `getAstroContext`
|
|
378
|
+
* reads back. When no `pathPrefix` rewrite happens, this is the same Request
|
|
379
|
+
* that Astro originally invoked the adapter with.
|
|
380
|
+
*/
|
|
381
|
+
export function setAstroContext(request: Request, apiContext: APIContext): void {
|
|
382
|
+
astroContextMap.set(request, apiContext)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Reads Astro's APIContext from inside a Hono factory-context closure.
|
|
387
|
+
*
|
|
388
|
+
* Throws when the request is not in scope — typically because the Hono app
|
|
389
|
+
* is being served outside the Astro adapter (e.g., bound to a Node listener
|
|
390
|
+
* or via a different adapter) and the closure was invoked there.
|
|
391
|
+
*/
|
|
392
|
+
export function getAstroContext(c: HonoContext): APIContext {
|
|
393
|
+
const ctx = astroContextMap.get(c.req.raw)
|
|
394
|
+
if (!ctx) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
'getAstroContext was called outside an Astro request scope. ' +
|
|
397
|
+
'Ensure this Hono app is being served via createAstroHandler when this closure runs.'
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
return ctx
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
405
|
+
|
|
406
|
+
Run:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npm test -- src/implementations/http/astro
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Expected: all `stripPrefix` and `astro-context` tests pass.
|
|
413
|
+
|
|
414
|
+
- [ ] **Step 5: Commit**
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
git add src/implementations/http/astro/astro-context.ts src/implementations/http/astro/index.test.ts
|
|
418
|
+
git commit -m "feat(astro): add WeakMap-backed getAstroContext bridge"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Task 4: createAstroHandler — single app, no prefix
|
|
424
|
+
|
|
425
|
+
**Files:**
|
|
426
|
+
- Modify: `src/implementations/http/astro/create-handler.ts`
|
|
427
|
+
- Modify: `src/implementations/http/astro/index.ts`
|
|
428
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
429
|
+
|
|
430
|
+
- [ ] **Step 1: Write the failing test**
|
|
431
|
+
|
|
432
|
+
Append to `src/implementations/http/astro/index.test.ts`:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
import { Type } from 'typebox'
|
|
436
|
+
import { Procedures } from '../../../index.js'
|
|
437
|
+
import { HonoAPIAppBuilder } from '../hono-api/index.js'
|
|
438
|
+
import type { APIConfig } from '../../types.js'
|
|
439
|
+
import { createAstroHandler } from './create-handler.js'
|
|
440
|
+
|
|
441
|
+
function buildSimpleUserApi() {
|
|
442
|
+
const API = Procedures<{ db: Map<string, { id: string; name: string }> }, APIConfig>()
|
|
443
|
+
API.Create(
|
|
444
|
+
'GetUser',
|
|
445
|
+
{
|
|
446
|
+
path: '/users/:id',
|
|
447
|
+
method: 'get',
|
|
448
|
+
schema: {
|
|
449
|
+
input: { pathParams: Type.Object({ id: Type.String() }) },
|
|
450
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
async (ctx, { pathParams }) => {
|
|
454
|
+
const u = ctx.db.get(pathParams.id)
|
|
455
|
+
if (!u) throw new Error('not found')
|
|
456
|
+
return u
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
const db = new Map([['1', { id: '1', name: 'Ada' }]])
|
|
460
|
+
return new HonoAPIAppBuilder().register(API, () => ({ db })).build()
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
describe('createAstroHandler — single app, no prefix', () => {
|
|
464
|
+
test('exports ALL plus the seven HTTP method handlers', () => {
|
|
465
|
+
const handlers = createAstroHandler({ apps: buildSimpleUserApi() })
|
|
466
|
+
expect(typeof handlers.ALL).toBe('function')
|
|
467
|
+
for (const m of ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const) {
|
|
468
|
+
expect(typeof handlers[m]).toBe('function')
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
test('GET roundtrips through the underlying Hono app', async () => {
|
|
473
|
+
const { ALL } = createAstroHandler({ apps: buildSimpleUserApi() })
|
|
474
|
+
const apiContext = fakeApiContext({
|
|
475
|
+
request: new Request('https://example.test/users/1'),
|
|
476
|
+
url: new URL('https://example.test/users/1'),
|
|
477
|
+
})
|
|
478
|
+
const res = await (ALL as any)(apiContext)
|
|
479
|
+
expect(res.status).toBe(200)
|
|
480
|
+
expect(await res.json()).toEqual({ id: '1', name: 'Ada' })
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
486
|
+
|
|
487
|
+
Run:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
npm test -- src/implementations/http/astro
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Expected: the two `createAstroHandler` tests fail because `createAstroHandler` is not exported.
|
|
494
|
+
|
|
495
|
+
- [ ] **Step 3: Implement minimal `createAstroHandler`**
|
|
496
|
+
|
|
497
|
+
Replace `src/implementations/http/astro/create-handler.ts` with:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import type { Hono } from 'hono'
|
|
501
|
+
import type { APIRoute } from 'astro'
|
|
502
|
+
import { setAstroContext } from './astro-context.js'
|
|
503
|
+
import { stripPrefix } from './rewrite-request.js'
|
|
504
|
+
|
|
505
|
+
export type AstroAdapterConfig = {
|
|
506
|
+
/** One or more built Hono apps. Order matters in first-match dispatch. */
|
|
507
|
+
apps: Hono | Hono[]
|
|
508
|
+
/**
|
|
509
|
+
* Path prefix matching the Astro catch-all mount point.
|
|
510
|
+
* For src/pages/api/[...rest].ts use '/api'. The adapter strips this
|
|
511
|
+
* before delegating, so Hono routes do NOT need to repeat the prefix.
|
|
512
|
+
*
|
|
513
|
+
* Normalization: leading and trailing slashes are optional; '/api',
|
|
514
|
+
* 'api', and '/api/' are equivalent. Undefined or '/' means no rewrite.
|
|
515
|
+
*/
|
|
516
|
+
pathPrefix?: string
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export type AstroHandlers = {
|
|
520
|
+
ALL: APIRoute
|
|
521
|
+
GET: APIRoute
|
|
522
|
+
POST: APIRoute
|
|
523
|
+
PUT: APIRoute
|
|
524
|
+
PATCH: APIRoute
|
|
525
|
+
DELETE: APIRoute
|
|
526
|
+
HEAD: APIRoute
|
|
527
|
+
OPTIONS: APIRoute
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const
|
|
531
|
+
|
|
532
|
+
export function createAstroHandler(config: AstroAdapterConfig): AstroHandlers {
|
|
533
|
+
const apps = Array.isArray(config.apps) ? config.apps : [config.apps]
|
|
534
|
+
|
|
535
|
+
const ALL: APIRoute = async (apiContext) => {
|
|
536
|
+
const rewritten = stripPrefix(apiContext.request, config.pathPrefix)
|
|
537
|
+
if (rewritten === null) {
|
|
538
|
+
return new Response(null, { status: 404 })
|
|
539
|
+
}
|
|
540
|
+
setAstroContext(rewritten, apiContext)
|
|
541
|
+
|
|
542
|
+
for (const app of apps) {
|
|
543
|
+
const res = await app.fetch(rewritten)
|
|
544
|
+
if (res.status !== 404) return res
|
|
545
|
+
}
|
|
546
|
+
return new Response(null, { status: 404 })
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const handlers = { ALL } as AstroHandlers
|
|
550
|
+
for (const method of HTTP_METHODS) {
|
|
551
|
+
handlers[method] = ALL
|
|
552
|
+
}
|
|
553
|
+
return handlers
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
- [ ] **Step 4: Wire the public barrel**
|
|
558
|
+
|
|
559
|
+
Replace `src/implementations/http/astro/index.ts` with:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
export { createAstroHandler } from './create-handler.js'
|
|
563
|
+
export type { AstroAdapterConfig, AstroHandlers } from './create-handler.js'
|
|
564
|
+
export { getAstroContext } from './astro-context.js'
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
- [ ] **Step 5: Run tests to verify they pass**
|
|
568
|
+
|
|
569
|
+
Run:
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
npm test -- src/implementations/http/astro
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Expected: all tests up to and including `createAstroHandler — single app, no prefix` pass.
|
|
576
|
+
|
|
577
|
+
- [ ] **Step 6: Commit**
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
git add src/implementations/http/astro/create-handler.ts src/implementations/http/astro/index.ts src/implementations/http/astro/index.test.ts
|
|
581
|
+
git commit -m "feat(astro): add createAstroHandler with single-app dispatch"
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## Task 5: pathPrefix support + getAstroContext integration
|
|
587
|
+
|
|
588
|
+
**Files:**
|
|
589
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
590
|
+
|
|
591
|
+
`createAstroHandler` already calls `stripPrefix` and `setAstroContext` — this task adds the integration tests that prove they work end-to-end. No production changes if all goes well.
|
|
592
|
+
|
|
593
|
+
- [ ] **Step 1: Write the failing tests**
|
|
594
|
+
|
|
595
|
+
Append to `src/implementations/http/astro/index.test.ts`:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
describe('createAstroHandler — pathPrefix + getAstroContext', () => {
|
|
599
|
+
test('strips pathPrefix before delegating', async () => {
|
|
600
|
+
// Underlying Hono app has no /api in its routes.
|
|
601
|
+
const app = buildSimpleUserApi()
|
|
602
|
+
const { ALL } = createAstroHandler({ apps: app, pathPrefix: '/api' })
|
|
603
|
+
const apiContext = fakeApiContext({
|
|
604
|
+
request: new Request('https://example.test/api/users/1'),
|
|
605
|
+
url: new URL('https://example.test/api/users/1'),
|
|
606
|
+
})
|
|
607
|
+
const res = await (ALL as any)(apiContext)
|
|
608
|
+
expect(res.status).toBe(200)
|
|
609
|
+
expect(await res.json()).toEqual({ id: '1', name: 'Ada' })
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
test('returns 404 directly when request path is outside the prefix', async () => {
|
|
613
|
+
const { ALL } = createAstroHandler({ apps: buildSimpleUserApi(), pathPrefix: '/api' })
|
|
614
|
+
const apiContext = fakeApiContext({
|
|
615
|
+
request: new Request('https://example.test/somewhere-else'),
|
|
616
|
+
url: new URL('https://example.test/somewhere-else'),
|
|
617
|
+
})
|
|
618
|
+
const res = await (ALL as any)(apiContext)
|
|
619
|
+
expect(res.status).toBe(404)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
test('factory closure can read APIContext via getAstroContext', async () => {
|
|
623
|
+
const API = Procedures<{ user: { id: string } | null }, APIConfig>()
|
|
624
|
+
API.Create(
|
|
625
|
+
'WhoAmI',
|
|
626
|
+
{ path: '/whoami', method: 'get', schema: { returnType: Type.Object({ userId: Type.Union([Type.String(), Type.Null()]) }) } },
|
|
627
|
+
async (ctx) => ({ userId: ctx.user?.id ?? null })
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
const app = new HonoAPIAppBuilder()
|
|
631
|
+
.register(API, (c) => {
|
|
632
|
+
const astro = getAstroContext(c)
|
|
633
|
+
return { user: (astro.locals as { user?: { id: string } }).user ?? null }
|
|
634
|
+
})
|
|
635
|
+
.build()
|
|
636
|
+
|
|
637
|
+
const { ALL } = createAstroHandler({ apps: app })
|
|
638
|
+
const apiContext = fakeApiContext({
|
|
639
|
+
locals: { user: { id: 'u-42' } },
|
|
640
|
+
request: new Request('https://example.test/whoami'),
|
|
641
|
+
url: new URL('https://example.test/whoami'),
|
|
642
|
+
})
|
|
643
|
+
const res = await (ALL as any)(apiContext)
|
|
644
|
+
expect(res.status).toBe(200)
|
|
645
|
+
expect(await res.json()).toEqual({ userId: 'u-42' })
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
- [ ] **Step 2: Run tests to verify they pass without changes to production code**
|
|
651
|
+
|
|
652
|
+
Run:
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
npm test -- src/implementations/http/astro
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Expected: all three new tests pass. (Production code from Task 4 already supports both behaviors — these tests just lock them in.)
|
|
659
|
+
|
|
660
|
+
If any test fails, fix the production code in `create-handler.ts` until they pass before continuing.
|
|
661
|
+
|
|
662
|
+
- [ ] **Step 3: Commit**
|
|
663
|
+
|
|
664
|
+
```bash
|
|
665
|
+
git add src/implementations/http/astro/index.test.ts
|
|
666
|
+
git commit -m "test(astro): cover pathPrefix and getAstroContext end-to-end"
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Task 6: Multi-app dispatch (404 fallthrough + non-404 short-circuit)
|
|
672
|
+
|
|
673
|
+
**Files:**
|
|
674
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
675
|
+
|
|
676
|
+
The multi-app loop already exists in `create-handler.ts`. This task adds tests that cover both branches.
|
|
677
|
+
|
|
678
|
+
- [ ] **Step 1: Write the failing tests**
|
|
679
|
+
|
|
680
|
+
Append to `src/implementations/http/astro/index.test.ts`:
|
|
681
|
+
|
|
682
|
+
```ts
|
|
683
|
+
import { HonoRPCAppBuilder } from '../hono-rpc/index.js'
|
|
684
|
+
|
|
685
|
+
describe('createAstroHandler — multi-app dispatch', () => {
|
|
686
|
+
function makeAppWithRoute(path: string, status: number, body: unknown) {
|
|
687
|
+
const app = new Hono()
|
|
688
|
+
app.all(path, (c) => c.json(body, status as any))
|
|
689
|
+
return app
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
test('first 404 falls through to the second app', async () => {
|
|
693
|
+
const a = makeAppWithRoute('/only-a', 200, { from: 'a' })
|
|
694
|
+
const b = makeAppWithRoute('/only-b', 200, { from: 'b' })
|
|
695
|
+
const { ALL } = createAstroHandler({ apps: [a, b] })
|
|
696
|
+
|
|
697
|
+
const apiContext = fakeApiContext({
|
|
698
|
+
request: new Request('https://example.test/only-b'),
|
|
699
|
+
url: new URL('https://example.test/only-b'),
|
|
700
|
+
})
|
|
701
|
+
const res = await (ALL as any)(apiContext)
|
|
702
|
+
expect(res.status).toBe(200)
|
|
703
|
+
expect(await res.json()).toEqual({ from: 'b' })
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
test('a non-404 response from the first app short-circuits dispatch', async () => {
|
|
707
|
+
const a = makeAppWithRoute('/error', 500, { boom: true })
|
|
708
|
+
const b = makeAppWithRoute('/error', 200, { from: 'b' })
|
|
709
|
+
const { ALL } = createAstroHandler({ apps: [a, b] })
|
|
710
|
+
|
|
711
|
+
const apiContext = fakeApiContext({
|
|
712
|
+
request: new Request('https://example.test/error'),
|
|
713
|
+
url: new URL('https://example.test/error'),
|
|
714
|
+
})
|
|
715
|
+
const res = await (ALL as any)(apiContext)
|
|
716
|
+
expect(res.status).toBe(500)
|
|
717
|
+
expect(await res.json()).toEqual({ boom: true })
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
test('all-404 returns the adapter\'s own 404 with empty body', async () => {
|
|
721
|
+
const a = makeAppWithRoute('/x', 200, { from: 'a' })
|
|
722
|
+
const b = makeAppWithRoute('/y', 200, { from: 'b' })
|
|
723
|
+
const { ALL } = createAstroHandler({ apps: [a, b] })
|
|
724
|
+
|
|
725
|
+
const apiContext = fakeApiContext({
|
|
726
|
+
request: new Request('https://example.test/nope'),
|
|
727
|
+
url: new URL('https://example.test/nope'),
|
|
728
|
+
})
|
|
729
|
+
const res = await (ALL as any)(apiContext)
|
|
730
|
+
expect(res.status).toBe(404)
|
|
731
|
+
expect(await res.text()).toBe('')
|
|
732
|
+
})
|
|
733
|
+
})
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
- [ ] **Step 2: Run tests to verify they pass**
|
|
737
|
+
|
|
738
|
+
Run:
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
npm test -- src/implementations/http/astro
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Expected: all three multi-app tests pass.
|
|
745
|
+
|
|
746
|
+
- [ ] **Step 3: Commit**
|
|
747
|
+
|
|
748
|
+
```bash
|
|
749
|
+
git add src/implementations/http/astro/index.test.ts
|
|
750
|
+
git commit -m "test(astro): cover multi-app first-match and non-404 short-circuit"
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## Task 7: Stream pass-through
|
|
756
|
+
|
|
757
|
+
**Files:**
|
|
758
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
759
|
+
|
|
760
|
+
Confirms a `HonoStreamAppBuilder` produces a streaming Response that survives the adapter intact.
|
|
761
|
+
|
|
762
|
+
- [ ] **Step 1: Write the failing test**
|
|
763
|
+
|
|
764
|
+
Append to `src/implementations/http/astro/index.test.ts`:
|
|
765
|
+
|
|
766
|
+
```ts
|
|
767
|
+
import { HonoStreamAppBuilder } from '../hono-stream/index.js'
|
|
768
|
+
import type { StreamConfig } from '../../types.js'
|
|
769
|
+
|
|
770
|
+
describe('createAstroHandler — streams', () => {
|
|
771
|
+
test('SSE events from a HonoStream builder pass through the adapter', async () => {
|
|
772
|
+
const STREAM = Procedures<{}, StreamConfig>()
|
|
773
|
+
STREAM.CreateStream(
|
|
774
|
+
'Counter',
|
|
775
|
+
{
|
|
776
|
+
path: '/counter',
|
|
777
|
+
schema: {
|
|
778
|
+
yieldType: Type.Object({ n: Type.Number() }),
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
async function* () {
|
|
782
|
+
yield { n: 1 }
|
|
783
|
+
yield { n: 2 }
|
|
784
|
+
yield { n: 3 }
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
const streamApp = new HonoStreamAppBuilder().register(STREAM, () => ({})).build()
|
|
789
|
+
const { ALL } = createAstroHandler({ apps: streamApp })
|
|
790
|
+
|
|
791
|
+
const apiContext = fakeApiContext({
|
|
792
|
+
request: new Request('https://example.test/counter', { method: 'GET' }),
|
|
793
|
+
url: new URL('https://example.test/counter'),
|
|
794
|
+
})
|
|
795
|
+
const res = await (ALL as any)(apiContext)
|
|
796
|
+
expect(res.status).toBe(200)
|
|
797
|
+
expect(res.headers.get('content-type')).toMatch(/event-stream/)
|
|
798
|
+
|
|
799
|
+
// Drain the SSE body and assert all three events appear in order.
|
|
800
|
+
const text = await res.text()
|
|
801
|
+
expect(text).toMatch(/"n":1/)
|
|
802
|
+
expect(text).toMatch(/"n":2/)
|
|
803
|
+
expect(text).toMatch(/"n":3/)
|
|
804
|
+
})
|
|
805
|
+
})
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
NOTE: If `HonoStreamAppBuilder`'s constructor or `CreateStream` signature differs from this snippet, open `src/implementations/http/hono-stream/index.test.ts` to find the canonical example and copy the matching shape. Do NOT invent a different API surface.
|
|
809
|
+
|
|
810
|
+
- [ ] **Step 2: Run test to verify it passes (or fails informatively)**
|
|
811
|
+
|
|
812
|
+
Run:
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
npm test -- src/implementations/http/astro
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
Expected behaviour: the test passes. The adapter does not modify the streaming Response, so it should already work.
|
|
819
|
+
|
|
820
|
+
If the test fails because of a `HonoStreamAppBuilder` API mismatch (e.g., `CreateStream` has a different signature in the current version), update only the test wiring to match — don't change the adapter.
|
|
821
|
+
|
|
822
|
+
- [ ] **Step 3: Commit**
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
git add src/implementations/http/astro/index.test.ts
|
|
826
|
+
git commit -m "test(astro): verify HonoStream SSE bodies pass through unchanged"
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
## Task 8: Abort signal flow on client disconnect
|
|
832
|
+
|
|
833
|
+
**Files:**
|
|
834
|
+
- Modify: `src/implementations/http/astro/index.test.ts`
|
|
835
|
+
|
|
836
|
+
- [ ] **Step 1: Write the failing test**
|
|
837
|
+
|
|
838
|
+
Append to `src/implementations/http/astro/index.test.ts`:
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
describe('createAstroHandler — abort signal', () => {
|
|
842
|
+
test('aborting the incoming request aborts the procedure handler\'s ctx.signal', async () => {
|
|
843
|
+
let observedSignal: AbortSignal | undefined
|
|
844
|
+
const aborted = new Promise<void>((resolve) => {
|
|
845
|
+
const API = Procedures<{}, APIConfig>()
|
|
846
|
+
API.Create(
|
|
847
|
+
'Hang',
|
|
848
|
+
{ path: '/hang', method: 'get', schema: { returnType: Type.Object({ ok: Type.Boolean() }) } },
|
|
849
|
+
async (ctx) => {
|
|
850
|
+
observedSignal = ctx.signal
|
|
851
|
+
ctx.signal?.addEventListener('abort', () => resolve(), { once: true })
|
|
852
|
+
// Wait for abort or a generous timeout.
|
|
853
|
+
await new Promise<void>((r) => setTimeout(r, 200))
|
|
854
|
+
return { ok: true }
|
|
855
|
+
}
|
|
856
|
+
)
|
|
857
|
+
const app = new HonoAPIAppBuilder().register(API, () => ({})).build()
|
|
858
|
+
const { ALL } = createAstroHandler({ apps: app })
|
|
859
|
+
|
|
860
|
+
const controller = new AbortController()
|
|
861
|
+
const apiContext = fakeApiContext({
|
|
862
|
+
request: new Request('https://example.test/hang', { signal: controller.signal }),
|
|
863
|
+
url: new URL('https://example.test/hang'),
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
// Fire ALL but don't await — abort while the handler is still running.
|
|
867
|
+
;(ALL as any)(apiContext).catch(() => {})
|
|
868
|
+
setTimeout(() => controller.abort(), 20)
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
await aborted
|
|
872
|
+
expect(observedSignal?.aborted).toBe(true)
|
|
873
|
+
})
|
|
874
|
+
})
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
- [ ] **Step 2: Run test to verify it passes**
|
|
878
|
+
|
|
879
|
+
Run:
|
|
880
|
+
|
|
881
|
+
```bash
|
|
882
|
+
npm test -- src/implementations/http/astro
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
Expected: the test passes. The HonoAPIAppBuilder already wires `ctx.signal = c.req.raw.signal`, and our `stripPrefix` preserves the signal via `new Request(url, request)`.
|
|
886
|
+
|
|
887
|
+
If it fails, the cause is most likely that the rewritten `new Request(url, request)` is not preserving the signal in this Node/undici version. Investigate by adding a `console.log(rewritten.signal === request.signal)` inside `create-handler.ts` temporarily; if signals do not survive cloning, switch the rewrite to construct the new Request with explicit `signal: request.signal` in the `init` argument.
|
|
888
|
+
|
|
889
|
+
- [ ] **Step 3: Commit**
|
|
890
|
+
|
|
891
|
+
```bash
|
|
892
|
+
git add src/implementations/http/astro/index.test.ts
|
|
893
|
+
git commit -m "test(astro): verify client abort propagates to handler ctx.signal"
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
898
|
+
## Task 9: README for the adapter module
|
|
899
|
+
|
|
900
|
+
**Files:**
|
|
901
|
+
- Create: `src/implementations/http/astro/README.md`
|
|
902
|
+
|
|
903
|
+
- [ ] **Step 1: Read a sibling README for tone and structure**
|
|
904
|
+
|
|
905
|
+
Open `src/implementations/http/hono-api/README.md` and skim the structure (overview → install → quick example → config reference → recipes).
|
|
906
|
+
|
|
907
|
+
- [ ] **Step 2: Write the README**
|
|
908
|
+
|
|
909
|
+
Create `src/implementations/http/astro/README.md` with this content:
|
|
910
|
+
|
|
911
|
+
````markdown
|
|
912
|
+
# `ts-procedures/astro`
|
|
913
|
+
|
|
914
|
+
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.
|
|
915
|
+
|
|
916
|
+
## Install
|
|
917
|
+
|
|
918
|
+
The adapter is part of `ts-procedures`. Astro is an optional peer; install it in your Astro app:
|
|
919
|
+
|
|
920
|
+
```bash
|
|
921
|
+
npm install ts-procedures hono astro
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
## Quick start
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
// src/server/api.ts
|
|
928
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
929
|
+
import { getAstroContext } from 'ts-procedures/astro'
|
|
930
|
+
import { usersAPI } from './procedures/users'
|
|
931
|
+
import { db } from './db'
|
|
932
|
+
|
|
933
|
+
export const apiApp = new HonoAPIAppBuilder()
|
|
934
|
+
.register(usersAPI, (c) => {
|
|
935
|
+
const astro = getAstroContext(c)
|
|
936
|
+
return { db, user: astro.locals.user ?? null }
|
|
937
|
+
})
|
|
938
|
+
.build()
|
|
939
|
+
```
|
|
940
|
+
|
|
941
|
+
```ts
|
|
942
|
+
// src/pages/api/[...rest].ts
|
|
943
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
944
|
+
import { apiApp } from '../../server/api'
|
|
945
|
+
|
|
946
|
+
export const { ALL } = createAstroHandler({
|
|
947
|
+
apps: apiApp,
|
|
948
|
+
pathPrefix: '/api',
|
|
949
|
+
})
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
## Config reference
|
|
953
|
+
|
|
954
|
+
| Option | Type | Description |
|
|
955
|
+
| ------------ | --------------------- | -------------------------------------------------------------------------------------------- |
|
|
956
|
+
| `apps` | `Hono \| Hono[]` | One or more built Hono apps. Order matters in first-match dispatch. |
|
|
957
|
+
| `pathPrefix` | `string \| undefined` | Path prefix matching the Astro catch-all mount point. `/api`, `api`, `/api/` are equivalent. |
|
|
958
|
+
|
|
959
|
+
## Path prefix semantics
|
|
960
|
+
|
|
961
|
+
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.
|
|
962
|
+
|
|
963
|
+
| Request path | `pathPrefix` | Inner Hono sees |
|
|
964
|
+
| ---------------- | ------------ | --------------- |
|
|
965
|
+
| `/api/users/123` | `'/api'` | `/users/123` |
|
|
966
|
+
| `/api` | `'/api'` | `/` |
|
|
967
|
+
| `/somewhere` | `'/api'` | (404, no app invoked) |
|
|
968
|
+
| `/users/123` | `undefined` | `/users/123` |
|
|
969
|
+
|
|
970
|
+
## Multi-app dispatch
|
|
971
|
+
|
|
972
|
+
Pass an array. The adapter tries each app in order:
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
export const { ALL } = createAstroHandler({
|
|
976
|
+
apps: [apiApp, rpcApp, streamsApp],
|
|
977
|
+
pathPrefix: '/api',
|
|
978
|
+
})
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
Dispatch rules:
|
|
982
|
+
- The first response with `status !== 404` is returned.
|
|
983
|
+
- A `500` from app A stops dispatch (it is treated as a real answer, not a miss).
|
|
984
|
+
- Only literal 404s fall through to the next app.
|
|
985
|
+
- All apps 404 → adapter returns its own `Response(null, { status: 404 })`.
|
|
986
|
+
|
|
987
|
+
## Streams
|
|
988
|
+
|
|
989
|
+
`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.
|
|
990
|
+
|
|
991
|
+
## What's NOT included
|
|
992
|
+
|
|
993
|
+
- Express RPC support (Express uses Node `req`/`res`, not Web Fetch).
|
|
994
|
+
- DocRegistry coupling — wire `DocRegistry` against the same builders separately for client codegen.
|
|
995
|
+
````
|
|
996
|
+
|
|
997
|
+
- [ ] **Step 3: Commit**
|
|
998
|
+
|
|
999
|
+
```bash
|
|
1000
|
+
git add src/implementations/http/astro/README.md
|
|
1001
|
+
git commit -m "docs(astro): add README for ts-procedures/astro subpath"
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
---
|
|
1005
|
+
|
|
1006
|
+
## Task 10: End-to-end walkthrough doc
|
|
1007
|
+
|
|
1008
|
+
**Files:**
|
|
1009
|
+
- Create: `docs/astro-adapter.md`
|
|
1010
|
+
|
|
1011
|
+
- [ ] **Step 1: Write the walkthrough**
|
|
1012
|
+
|
|
1013
|
+
Create `docs/astro-adapter.md` with this content:
|
|
1014
|
+
|
|
1015
|
+
````markdown
|
|
1016
|
+
# Astro adapter walkthrough
|
|
1017
|
+
|
|
1018
|
+
This walkthrough builds an Astro app that serves ts-procedures handlers from a single catch-all endpoint.
|
|
1019
|
+
|
|
1020
|
+
## Project shape
|
|
1021
|
+
|
|
1022
|
+
```
|
|
1023
|
+
my-astro-app/
|
|
1024
|
+
├── astro.config.mjs
|
|
1025
|
+
├── package.json
|
|
1026
|
+
└── src/
|
|
1027
|
+
├── server/
|
|
1028
|
+
│ ├── db.ts
|
|
1029
|
+
│ ├── procedures/
|
|
1030
|
+
│ │ └── users.ts
|
|
1031
|
+
│ └── api.ts
|
|
1032
|
+
└── pages/
|
|
1033
|
+
└── api/
|
|
1034
|
+
└── [...rest].ts
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
## 1. Define procedures
|
|
1038
|
+
|
|
1039
|
+
```ts
|
|
1040
|
+
// src/server/procedures/users.ts
|
|
1041
|
+
import { Procedures } from 'ts-procedures'
|
|
1042
|
+
import type { APIConfig } from 'ts-procedures/http'
|
|
1043
|
+
import { Type } from 'typebox'
|
|
1044
|
+
|
|
1045
|
+
type UserContext = {
|
|
1046
|
+
db: { findUser(id: string): Promise<{ id: string; name: string } | null> }
|
|
1047
|
+
currentUser: { id: string } | null
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export const usersAPI = Procedures<UserContext, APIConfig>()
|
|
1051
|
+
|
|
1052
|
+
usersAPI.Create(
|
|
1053
|
+
'GetUser',
|
|
1054
|
+
{
|
|
1055
|
+
path: '/users/:id',
|
|
1056
|
+
method: 'get',
|
|
1057
|
+
schema: {
|
|
1058
|
+
input: { pathParams: Type.Object({ id: Type.String() }) },
|
|
1059
|
+
returnType: Type.Object({ id: Type.String(), name: Type.String() }),
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
async (ctx, { pathParams }) => {
|
|
1063
|
+
const u = await ctx.db.findUser(pathParams.id)
|
|
1064
|
+
if (!u) throw new Error('not found')
|
|
1065
|
+
return u
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
## 2. Build the Hono app once
|
|
1071
|
+
|
|
1072
|
+
```ts
|
|
1073
|
+
// src/server/api.ts
|
|
1074
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
1075
|
+
import { getAstroContext } from 'ts-procedures/astro'
|
|
1076
|
+
import { usersAPI } from './procedures/users'
|
|
1077
|
+
import { db } from './db'
|
|
1078
|
+
|
|
1079
|
+
export const apiApp = new HonoAPIAppBuilder()
|
|
1080
|
+
.register(usersAPI, (c) => {
|
|
1081
|
+
const astro = getAstroContext(c)
|
|
1082
|
+
return {
|
|
1083
|
+
db,
|
|
1084
|
+
currentUser: astro.locals.user ?? null,
|
|
1085
|
+
}
|
|
1086
|
+
})
|
|
1087
|
+
.build()
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
## 3. Mount the catch-all
|
|
1091
|
+
|
|
1092
|
+
```ts
|
|
1093
|
+
// src/pages/api/[...rest].ts
|
|
1094
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
1095
|
+
import { apiApp } from '../../server/api'
|
|
1096
|
+
|
|
1097
|
+
export const { ALL } = createAstroHandler({
|
|
1098
|
+
apps: apiApp,
|
|
1099
|
+
pathPrefix: '/api',
|
|
1100
|
+
})
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
That's it — `GET /api/users/123` runs through `usersAPI.GetUser`. The factory closure sees `astro.locals.user` populated from your Astro middleware.
|
|
1104
|
+
|
|
1105
|
+
## Where do `Astro.locals.user` come from?
|
|
1106
|
+
|
|
1107
|
+
A typical pattern: an Astro middleware that reads a session cookie and sets `locals.user`:
|
|
1108
|
+
|
|
1109
|
+
```ts
|
|
1110
|
+
// src/middleware.ts
|
|
1111
|
+
import { defineMiddleware } from 'astro:middleware'
|
|
1112
|
+
import { db } from './server/db'
|
|
1113
|
+
|
|
1114
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
1115
|
+
const sessionId = context.cookies.get('sid')?.value
|
|
1116
|
+
if (sessionId) {
|
|
1117
|
+
context.locals.user = await db.findUserBySession(sessionId)
|
|
1118
|
+
}
|
|
1119
|
+
return next()
|
|
1120
|
+
})
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
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.
|
|
1124
|
+
|
|
1125
|
+
## Client codegen — where does it go?
|
|
1126
|
+
|
|
1127
|
+
The adapter does NOT couple to `DocRegistry`. Wire codegen separately, against the same builders:
|
|
1128
|
+
|
|
1129
|
+
```ts
|
|
1130
|
+
// scripts/build-docs.ts
|
|
1131
|
+
import { DocRegistry } from 'ts-procedures/http-docs'
|
|
1132
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
1133
|
+
import { usersAPI } from '../src/server/procedures/users'
|
|
1134
|
+
|
|
1135
|
+
const builder = new HonoAPIAppBuilder().register(usersAPI, () => ({} as never))
|
|
1136
|
+
builder.build()
|
|
1137
|
+
|
|
1138
|
+
const registry = new DocRegistry().from(builder)
|
|
1139
|
+
console.log(JSON.stringify(registry.toEnvelope(), null, 2))
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
Then run `npx ts-procedures-codegen --file <envelope.json> --out ./generated/api`.
|
|
1143
|
+
|
|
1144
|
+
## Multi-app composition
|
|
1145
|
+
|
|
1146
|
+
For larger projects with separate API/RPC/Stream surfaces, pass an array:
|
|
1147
|
+
|
|
1148
|
+
```ts
|
|
1149
|
+
export const { ALL } = createAstroHandler({
|
|
1150
|
+
apps: [apiApp, rpcApp, streamsApp],
|
|
1151
|
+
pathPrefix: '/api',
|
|
1152
|
+
})
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
Order matters — first non-404 wins. See the module README for full dispatch rules.
|
|
1156
|
+
````
|
|
1157
|
+
|
|
1158
|
+
- [ ] **Step 2: Commit**
|
|
1159
|
+
|
|
1160
|
+
```bash
|
|
1161
|
+
git add docs/astro-adapter.md
|
|
1162
|
+
git commit -m "docs: add Astro adapter walkthrough"
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1167
|
+
## Task 11: Scaffold template for ts-procedures-scaffold skill
|
|
1168
|
+
|
|
1169
|
+
**Files:**
|
|
1170
|
+
- Create: `agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.ts`
|
|
1171
|
+
|
|
1172
|
+
- [ ] **Step 1: Read an existing template for tone**
|
|
1173
|
+
|
|
1174
|
+
Open `agent_config/claude-code/skills/ts-procedures-scaffold/templates/hono-rpc.ts` (or whichever exists) and skim the comment style.
|
|
1175
|
+
|
|
1176
|
+
- [ ] **Step 2: Write the template**
|
|
1177
|
+
|
|
1178
|
+
Create `agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.ts`:
|
|
1179
|
+
|
|
1180
|
+
```ts
|
|
1181
|
+
// src/pages/api/[...rest].ts
|
|
1182
|
+
//
|
|
1183
|
+
// Catch-all entry point that serves every ts-procedures route from a single Astro file.
|
|
1184
|
+
// Adjust `pathPrefix` to match the directory the file lives in (e.g., '/api' for
|
|
1185
|
+
// src/pages/api/[...rest].ts; '/v1' for src/pages/v1/[...rest].ts).
|
|
1186
|
+
//
|
|
1187
|
+
// Build your Hono app(s) ONCE in a sibling module, then drop them in here.
|
|
1188
|
+
|
|
1189
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
1190
|
+
import { apiApp } from '../../server/api' // ← your built Hono app(s)
|
|
1191
|
+
|
|
1192
|
+
export const { ALL } = createAstroHandler({
|
|
1193
|
+
apps: apiApp,
|
|
1194
|
+
pathPrefix: '/api',
|
|
1195
|
+
})
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
- [ ] **Step 3: Commit**
|
|
1199
|
+
|
|
1200
|
+
```bash
|
|
1201
|
+
git add agent_config/claude-code/skills/ts-procedures-scaffold/templates/astro-catchall.ts
|
|
1202
|
+
git commit -m "feat(agent-config): add astro-catchall scaffold template"
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## Task 12: Update agent_config rules (api-reference + patterns + Copilot/Cursor)
|
|
1208
|
+
|
|
1209
|
+
**Files:**
|
|
1210
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/api-reference.md`
|
|
1211
|
+
- Modify: `agent_config/claude-code/skills/ts-procedures/patterns.md`
|
|
1212
|
+
- Modify: `agent_config/copilot/copilot-instructions.md`
|
|
1213
|
+
- Modify: `agent_config/cursor/cursorrules`
|
|
1214
|
+
|
|
1215
|
+
- [ ] **Step 1: Read the current api-reference.md**
|
|
1216
|
+
|
|
1217
|
+
Open `agent_config/claude-code/skills/ts-procedures/api-reference.md` and locate the existing section that lists builders (HonoAPIAppBuilder, HonoRPCAppBuilder, HonoStreamAppBuilder, etc.).
|
|
1218
|
+
|
|
1219
|
+
- [ ] **Step 2: Add an Astro adapter section**
|
|
1220
|
+
|
|
1221
|
+
After the existing builders section in `api-reference.md`, append:
|
|
1222
|
+
|
|
1223
|
+
````markdown
|
|
1224
|
+
## Astro adapter — `ts-procedures/astro`
|
|
1225
|
+
|
|
1226
|
+
For Astro apps, mount one or more pre-built Hono apps inside a single catch-all endpoint:
|
|
1227
|
+
|
|
1228
|
+
```ts
|
|
1229
|
+
// src/pages/api/[...rest].ts
|
|
1230
|
+
import { createAstroHandler } from 'ts-procedures/astro'
|
|
1231
|
+
import { apiApp } from '../../server/api'
|
|
1232
|
+
|
|
1233
|
+
export const { ALL } = createAstroHandler({
|
|
1234
|
+
apps: apiApp,
|
|
1235
|
+
pathPrefix: '/api',
|
|
1236
|
+
})
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
Inside factory-context closures, read Astro's APIContext via `getAstroContext`:
|
|
1240
|
+
|
|
1241
|
+
```ts
|
|
1242
|
+
import { getAstroContext } from 'ts-procedures/astro'
|
|
1243
|
+
|
|
1244
|
+
new HonoAPIAppBuilder()
|
|
1245
|
+
.register(usersAPI, (c) => {
|
|
1246
|
+
const astro = getAstroContext(c)
|
|
1247
|
+
return { db, user: astro.locals.user ?? null }
|
|
1248
|
+
})
|
|
1249
|
+
.build()
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
Multi-app: pass an array; first non-404 response wins. All-404 falls back to the adapter's own 404. Streams pass through unchanged.
|
|
1253
|
+
|
|
1254
|
+
Constraints:
|
|
1255
|
+
- Astro 5+ in SSR (or `prerender = false`).
|
|
1256
|
+
- Express builders are NOT supported by this adapter — Hono builders only.
|
|
1257
|
+
````
|
|
1258
|
+
|
|
1259
|
+
- [ ] **Step 3: Update patterns.md**
|
|
1260
|
+
|
|
1261
|
+
Open `agent_config/claude-code/skills/ts-procedures/patterns.md` and add a new "Astro integration" pattern at the end of the file:
|
|
1262
|
+
|
|
1263
|
+
````markdown
|
|
1264
|
+
### Astro integration
|
|
1265
|
+
|
|
1266
|
+
When a downstream developer is building inside an Astro app, recommend the catch-all pattern:
|
|
1267
|
+
|
|
1268
|
+
1. Build the Hono app ONCE in `src/server/api.ts` using `HonoAPIAppBuilder` / `HonoRPCAppBuilder` / `HonoStreamAppBuilder` exactly as you would for any other deployment.
|
|
1269
|
+
2. Inside the factory-context closure, call `getAstroContext(c)` to read `locals`, `cookies`, `params`, etc.
|
|
1270
|
+
3. Mount the built app(s) in a single catch-all file: `src/pages/api/[...rest].ts`. Use `pathPrefix: '/api'` so your Hono routes don't need to repeat the prefix.
|
|
1271
|
+
|
|
1272
|
+
Anti-pattern: don't create one Astro file per procedure — that loses the centralized factory wiring and forces the developer to repeat the context closure.
|
|
1273
|
+
|
|
1274
|
+
Anti-pattern: don't try to use `express-rpc` with the Astro adapter. The adapter only accepts Web-Fetch apps (Hono).
|
|
1275
|
+
````
|
|
1276
|
+
|
|
1277
|
+
- [ ] **Step 4: Mirror the same content into Copilot and Cursor rules**
|
|
1278
|
+
|
|
1279
|
+
Per CLAUDE.md, `agent_config/copilot/copilot-instructions.md` and `agent_config/cursor/cursorrules` are kept identical inside their `<!-- BEGIN/END ts-procedures -->` markers.
|
|
1280
|
+
|
|
1281
|
+
In each file, locate the existing builders section and append (verbatim — same text in both files):
|
|
1282
|
+
|
|
1283
|
+
```markdown
|
|
1284
|
+
## Astro adapter
|
|
1285
|
+
|
|
1286
|
+
Catch-all endpoint pattern. Build Hono apps once with HonoAPI/RPC/Stream builders, 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. Express builders are not supported.
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
- [ ] **Step 5: Run the docs-consistency check**
|
|
1290
|
+
|
|
1291
|
+
Run:
|
|
1292
|
+
|
|
1293
|
+
```bash
|
|
1294
|
+
npm run check-docs
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
Expected: passes. If it fails because Copilot and Cursor diverged, reconcile them — both files should contain the same Astro section verbatim.
|
|
1298
|
+
|
|
1299
|
+
- [ ] **Step 6: Commit**
|
|
1300
|
+
|
|
1301
|
+
```bash
|
|
1302
|
+
git add agent_config/
|
|
1303
|
+
git commit -m "docs(agent-config): teach Claude/Cursor/Copilot rules the Astro adapter"
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
---
|
|
1307
|
+
|
|
1308
|
+
## Task 13: Final validation
|
|
1309
|
+
|
|
1310
|
+
**Files:** none
|
|
1311
|
+
|
|
1312
|
+
- [ ] **Step 1: Run the full test suite**
|
|
1313
|
+
|
|
1314
|
+
Run:
|
|
1315
|
+
|
|
1316
|
+
```bash
|
|
1317
|
+
npm test
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
Expected: all tests pass, including the new Astro suite.
|
|
1321
|
+
|
|
1322
|
+
- [ ] **Step 2: Run the build**
|
|
1323
|
+
|
|
1324
|
+
Run:
|
|
1325
|
+
|
|
1326
|
+
```bash
|
|
1327
|
+
npm run build
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
Expected: build succeeds. The `./astro` subpath emits `build/implementations/http/astro/index.{js,d.ts}`.
|
|
1331
|
+
|
|
1332
|
+
- [ ] **Step 3: Run the lint**
|
|
1333
|
+
|
|
1334
|
+
Run:
|
|
1335
|
+
|
|
1336
|
+
```bash
|
|
1337
|
+
npm run lint
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
Expected: no warnings or errors.
|
|
1341
|
+
|
|
1342
|
+
- [ ] **Step 4: Verify the subpath resolves**
|
|
1343
|
+
|
|
1344
|
+
Run:
|
|
1345
|
+
|
|
1346
|
+
```bash
|
|
1347
|
+
node --input-type=module -e "import('ts-procedures/astro').then(m => console.log(Object.keys(m).sort()))"
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
Expected: `['createAstroHandler', 'getAstroContext']` (order may vary, but both names present).
|
|
1351
|
+
|
|
1352
|
+
- [ ] **Step 5: Final commit if any tail-end fixes were needed**
|
|
1353
|
+
|
|
1354
|
+
If steps 1-4 required any code adjustments, commit them with:
|
|
1355
|
+
|
|
1356
|
+
```bash
|
|
1357
|
+
git add -A
|
|
1358
|
+
git commit -m "chore(astro): final cleanup after full-suite validation"
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
If nothing changed, skip this step.
|
|
1362
|
+
|
|
1363
|
+
---
|
|
1364
|
+
|
|
1365
|
+
## Self-Review
|
|
1366
|
+
|
|
1367
|
+
**Spec coverage:**
|
|
1368
|
+
- Approach (catch-all + delegate to Hono apps) — Tasks 4, 5, 6
|
|
1369
|
+
- WeakMap-based `getAstroContext` — Task 3, integration test in Task 5
|
|
1370
|
+
- Path prefix rewrite with all normalizations + query preservation — Task 2 (unit), Task 5 (integration)
|
|
1371
|
+
- Multi-app first-non-404 dispatch with non-404 short-circuit — Task 6
|
|
1372
|
+
- Stream pass-through — Task 7
|
|
1373
|
+
- Abort signal flow — Task 8
|
|
1374
|
+
- 404 fallback when nothing matches or prefix mismatches — Task 5 + Task 6
|
|
1375
|
+
- File layout (`astro-context.ts`, `rewrite-request.ts`, `create-handler.ts`, `index.ts`, `index.test.ts`, `README.md`) — Tasks 1, 2, 3, 4, 9
|
|
1376
|
+
- Package.json `./astro` export, `optionalDependencies` + `devDependencies` for `astro` — Task 1
|
|
1377
|
+
- `import type` only — Task 3 (`astro-context.ts` uses `import type { APIContext }`); Task 4 (`create-handler.ts` uses `import type { Hono }`, `import type { APIRoute }`)
|
|
1378
|
+
- `docs/astro-adapter.md` walkthrough — Task 10
|
|
1379
|
+
- Agent config updates (api-reference, patterns, scaffold template, Copilot/Cursor) — Tasks 11, 12
|
|
1380
|
+
|
|
1381
|
+
**Placeholder scan:** No "TBD"/"TODO"/"implement later". Every code block contains the actual content. Tests have specific assertions, not "verify behavior".
|
|
1382
|
+
|
|
1383
|
+
**Type consistency:**
|
|
1384
|
+
- `AstroAdapterConfig` — defined Task 4, referenced Task 4. Single definition.
|
|
1385
|
+
- `AstroHandlers` — defined Task 4, referenced Task 4. Single definition.
|
|
1386
|
+
- `setAstroContext(req, ctx)` — defined Task 3, called Task 4. Same signature both places.
|
|
1387
|
+
- `getAstroContext(c)` — defined Task 3, called Tasks 5, 9, 10, 11, 12. Same signature.
|
|
1388
|
+
- `stripPrefix(request, prefix)` — defined Task 2, called Task 4. Same signature.
|
|
1389
|
+
|
|
1390
|
+
**Out-of-scope items confirmed not in plan:**
|
|
1391
|
+
- Express adapter — explicitly excluded
|
|
1392
|
+
- Native Astro builders — explicitly excluded
|
|
1393
|
+
- DocRegistry coupling — wired separately in walkthrough (Task 10), not in adapter
|
|
1394
|
+
- `dispatch: 'merge'` knob — not introduced
|
|
1395
|
+
|
|
1396
|
+
Ready for execution.
|