import type { Context } from 'hono' import { Hono } from 'hono' import type { Handler as vocs_Handler } from 'vocs/server' import type * as App from '../../App.js' import * as Metadata from '../metadata.js' /** * The docs route group: the Vocs API reference served at the app root. * * ```ts * import { docs } from 'tapimo/docs' * App.create(options).route('/', data()).route('/', management()).route('/', docs()) * ``` * * The group registers no routes itself: it carries a reference factory (via * {@link Metadata}) that `App.create` mounts on finalize, after every group and * the OpenAPI/OpenRPC documents — so the reference always renders the full * composed surface and its catch-all stays behind API routes, wherever the * group sits in the composition chain. * * Lives in the `tapimo/docs` subpath (never re-exported from `tapimo/apps`) so * the Vocs renderer and its multi-MB icon/font data are bundled only by * deployments that actually serve docs. Even then the renderer loads lazily on * the first docs request (see `reference.ts`), so API traffic and isolate * startup never pay for it. */ export function docs(options: docs.Options = {}) { return Metadata.attach(new Hono(), { docs: ({ spec }) => { // Memoized across requests: all docs traffic shares one handler. let handler: Promise | undefined return new Hono().get('*', async (c, next) => { handler ??= import('./reference.js').then((m) => m.create({ mount: mountFromRoutePath(c.req.routePath), spec, vocs: options }), ) const response = await (await handler).fetch(c.req.raw, c.env, executionCtx(c)) // Inner 404 means "not a docs route" (`fallback: 'next'` semantics): // defer to the host app's later routes / JSON `notFound`. if (response.status === 404) return next() return response }) }, }) } export declare namespace docs { /** Vocs API reference options (theme, top navigation, logo, socials, …). */ type Options = NonNullable[0]['vocs']> } /** Derives the host mount prefix from Hono's matched `routePath` (`/api/*` → `/api`, `/*` → ``). */ function mountFromRoutePath(routePath: string) { return routePath.replace(/\*+$/, '').replace(/\/+$/, '') } // `c.executionCtx` throws outside Workers (tests, Node); docs rendering never // uses `waitUntil`, so absence is fine. function executionCtx(c: Context) { try { return c.executionCtx } catch { return undefined } }