feat: mockly-hono — file-based mock API server (Hono)
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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user