import { Hono } from 'hono' import { Handler as vocs_Handler } from 'vocs/server' import type * as App from '../../App.js' import openrpcSpec from '../../openrpc.json' with { type: 'json' } import type { docs } from './App.js' import { pilatFontFaces } from './fonts.js' import { logoUrl } from './logo.js' /** * Builds the Vocs reference handler. Loaded via dynamic import on the first * docs request (see `docs()` in `App.ts`), keeping the multi-MB renderer off * isolate startup. */ export function create(options: create.Options): Hono { const { mount, spec, vocs } = options const handler = vocs_Handler.openApi( { // Inline the OpenRPC doc so vocs expands it without a self-subrequest // (see `inlineOpenrpc`); the public `/openapi.json` keeps the URL ref. spec: () => spec().then(inlineOpenrpc), sidebar: { collapsed: true }, vocs: { logoUrl, title: 'Tempo API Reference', ...vocs }, }, { css, fallback: 'next' }, ) if (!mount) return handler // Re-mount under the host prefix so the handler's `routePath`-derived mount // (asset link bases, known-route checks) matches a direct `.route()` mount. return new Hono().route(mount, handler) } export declare namespace create { /** Options for building the reference handler. */ type Options = { /** Host mount prefix derived from the wrapper's matched `routePath` (`''` at root). */ mount: string /** Memoized OpenAPI document builder — the same document served at `/openapi.json`. */ spec: App.create.DocsContext['spec'] /** Vocs reference options (theme, top navigation, logo, socials, …). */ vocs: docs.Options } } /** * Custom CSS injected into the standalone reference's shell `` (via * `Handler.openApi(_, { css })`), after the Vocs design-system styles so it * overrides them: * * - Page/surface backgrounds use Tempo's near-black dark palette and pure-white * light palette via `light-dark(, )`. * - The dark border color is darkened (the light theme keeps its default). * - Every corner is squared. The universal `!important` rule beats hard-coded * `border-radius` values and inline styles (e.g. the Scalar playground), which * overriding the radius scale variables alone would miss. * - The default sans/body font is Pilat (Vocs reads `--vocs-font-family`); the * webfont is embedded via {@link pilatFontFaces}. Body stays at the default * weight (400); headings are pinned to 500, which renders Pilat Demi (the * family has no 500 cut, so Demi covers the 500-600 range — see * {@link pilatFontFaces}). */ const css = `${pilatFontFaces} :root { --vocs-font-family: "Pilat", Arial, Helvetica, sans-serif; --vocs-background-color-primary: light-dark(#fafafa, #0a0a0a); --vocs-background-color-surface: light-dark(#fff, #121212); } :is(h1, h2, h3, h4, h5, h6)[data-v] { font-weight: 500; } /* Nudge the header logo a touch larger than the default max-h-7 (1.75rem). */ [data-v-logo-image] { max-height: 2rem; } /* Sidebar leaf items render Regular (the nav's 450 default otherwise resolves to * Demi via the 500-600 range). Group labels ([data-v-sidebar-section-header]) * keep their heavier weight. */ [data-v-sidebar-item] { font-weight: 400; } [data-vocs-theme='dark'] { --vocs-color-gray4: oklch(20% 0 0); --vocs-color-gray5: oklch(0.23 0 0); --vocs-border-color-primary: oklch(0.25 0 0); --vocs-border-color-secondary: oklch(0.25 0 0); } *, *::before, *::after { border-radius: 0 !important; }` /** * Returns a shallow copy of `spec` with the JSON-RPC passthrough operation's * `x-openrpc` replaced by the inline OpenRPC document, for the docs handler. * * Vocs expands `x-openrpc` at request time. A URL reference (as kept in the * public `/openapi.json` by `App.create`'s `applyOpenrpcLink`) would make the * docs worker fetch its own `/openrpc.json` — a self-subrequest that is * unreliable on Cloudflare Workers and silently falls back to a single, * unexpanded operation. Inlining the committed artifact keeps expansion * deterministic across local and deployed environments, without bloating the * public spec. Only the affected path/op are cloned; the rest of the (large) * document is shared by reference. */ function inlineOpenrpc }>(spec: spec): spec { const paths: Record = {} for (const [pathname, item] of Object.entries(spec.paths)) { let next = item if (item && typeof item === 'object' && !Array.isArray(item)) for (const [method, op] of Object.entries(item)) if (isRpcOperation(op)) next = { ...item, [method]: { ...op, 'x-openrpc': openrpcSpec } } paths[pathname] = next } return { ...spec, paths } } // The JSON-RPC passthrough's operation id, set by its route definition and // targeted by `App.create`'s `applyOpenrpcLink`. function isRpcOperation(op: unknown): op is Record { return ( !!op && typeof op === 'object' && !Array.isArray(op) && 'operationId' in op && op.operationId === 'rpcRequest' ) }