b122de518c
File path = route. JSON/JSON5, dynamic .ts handlers + faker, templating, stateful CRUD (pagination/sort/persist), __variants, sequence, chaos, rate-limit, auth, validation, per-route + global proxy/record, splat routes, X-Scenario overlays, SSE. Visual docs (Scalar) at /__docs from a live OpenAPI 3 spec. OpenAPI import script. Zero-build via tsx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.0 KiB
TypeScript
55 lines
2.0 KiB
TypeScript
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
import { dirname, join } from 'node:path'
|
|
import type { Context } from 'hono'
|
|
|
|
/**
|
|
* Forward unmatched requests to a real upstream (PROXY_TARGET).
|
|
* With record=true, persist the upstream JSON response as a mock file so the
|
|
* next run serves it locally — record-once, replay-forever.
|
|
*/
|
|
export async function proxyPass(
|
|
c: Context,
|
|
opts: { target: string; record: boolean; routesDir: string },
|
|
): Promise<Response> {
|
|
const reqUrl = new URL(c.req.url)
|
|
const upstream = new URL(reqUrl.pathname + reqUrl.search, opts.target)
|
|
|
|
const init: RequestInit = {
|
|
method: c.req.method,
|
|
headers: stripHopHeaders(c.req.raw.headers),
|
|
}
|
|
if (!['GET', 'HEAD'].includes(c.req.method)) init.body = await c.req.raw.arrayBuffer()
|
|
|
|
const res = await fetch(upstream, init)
|
|
const buf = await res.arrayBuffer()
|
|
|
|
if (opts.record && res.ok && (res.headers.get('content-type') ?? '').includes('json')) {
|
|
try {
|
|
recordMock(opts.routesDir, reqUrl.pathname, c.req.method, JSON.parse(Buffer.from(buf).toString('utf8')))
|
|
} catch {
|
|
/* non-json body — skip recording */
|
|
}
|
|
}
|
|
|
|
const headers = new Headers(res.headers)
|
|
headers.set('X-Mock-Proxy', upstream.href)
|
|
if (opts.record) headers.set('X-Mock-Recorded', 'true')
|
|
return new Response(buf, { status: res.status, headers })
|
|
}
|
|
|
|
function recordMock(routesDir: string, pathname: string, method: string, body: unknown) {
|
|
const segs = pathname.split('/').filter(Boolean)
|
|
const base = segs.length ? segs.join('/') : 'index'
|
|
const suffix = method.toLowerCase() === 'get' ? '' : `.${method.toLowerCase()}`
|
|
const file = join(routesDir, `${base}${suffix}.json`)
|
|
mkdirSync(dirname(file), { recursive: true })
|
|
writeFileSync(file, JSON.stringify(body, null, 2) + '\n')
|
|
console.log(`⤓ recorded ${method} ${pathname} -> ${file}`)
|
|
}
|
|
|
|
function stripHopHeaders(h: Headers): Headers {
|
|
const out = new Headers(h)
|
|
for (const k of ['host', 'connection', 'content-length', 'accept-encoding']) out.delete(k)
|
|
return out
|
|
}
|