Files
Peter Meier b122de518c 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>
2026-06-23 12:44:31 +02:00

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
}