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'
)
}