rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
147 lines (127 loc) • 9.61 kB
Markdown
# RapiDoc Utilities
## Overview
Framework-free helpers behind the `<rapi-doc>` component (`src/rapidoc.ts`), its templates and
the static renderer (`@rapidoc/prerender`, `packages/rapidoc-prerender`). `utils/` owns the spec pipeline (load,
dereference, group, anchor), schema rendering and example generation, request/response/mock
helpers, and UI helpers (highlighting, Markdown, sanitizing, navbar binding, color, OAuth). It
does not own rendering (`templates/`, `components/`) or styles. The MCP manifest, runtime and
project writer are documented in [`./mcp/README.md`](./mcp/README.md); Arazzo parsing and workflow diagrams in
[`./arazzo/README.md`](./arazzo/README.md).
Everything except `arazzo/` is bundled into `dist/rapidoc-min.js`; `arazzo/` goes into
`dist/rapidoc-flow-min.js` and `@rapidoc/prerender`. `@rapidoc/portal` depends on this folder
indirectly through `@rapidoc/prerender` (`methodPathSlug`, `operationAnchorSource`, `toKebabCase`)
and the anchors `<rapi-doc>` emits, so anchor generation is public behavior.
Several modules run in Node (static renderer, unit tests) and must guard `window`/`document`.
## Interface
Spec pipeline:
- `spec-parser.js`: default `ProcessSpec` (spec URL, text or object to the render model of
tags, tag groups, components, security schemes, servers); `isTagExpanded`, `groupByTagGroups`.
- `slugger.js`: `OperationSlugger` (`slug`, `slugOp`, `slugTag`, `slugTagGroup`, `reset`),
`operationAnchorSource`, `methodPathSlug`, `toKebabCase`, `clampWordBoundary`, `createSlugger`.
- `common-utils.js`: search (`advancedSearch`, `getMatchedPaths`), `debounce`, `sleep`,
clipboard and download helpers, `rapidocApiKey`.
Schema rendering:
- `schema-utils.js`: `getTypeInfo`, `schemaToAST` (schema tree for renderers),
`schemaInObjectNotation` (older tree format), `schemaToSampleObj` and `generateExample`
(examples), `json2xml`, `getMergedProperties`/`getMergedRequired`.
Request, response, mock:
- `url-utils.js`: path/query serialization, `constructFetchURL`, `oauthReceiverUrl`.
- `request-utils.js`: headers, url-encoded and multipart bodies.
- `response-utils.js`: `processFetchResponse` (decode a `Response`),
`prepareResponseDefinitions`.
- `curl-utils.js`: `generateCurlCommand`.
- `mock-interceptor.js`: `enableMockServer`, `disableMockServer`, `updateMockConfig`,
`isMockServerActive`, `compileRoutes` (from a `ProcessSpec` result).
UI helpers:
- `highlighter.js`: `highlightAllCode`, `scheduleHighlight` (CSS Custom Highlight API).
- `markdown-utils.js`: `configureMarkdown` and token helpers for the global `marked`.
- `sanitize.js`: `sanitizeHTML`.
- `bind-navbar.js`: `bindNavBar` ties a static `getNavBarHtml` sidebar to `<rapi-doc>`.
- `auto-color-scheme.js`: `AutoColorSchemeController`, `resolvePageColorScheme`.
- `oauth-utils.js`: PKCE (`randomUrlSafe`, `pkceChallenge`), `readOAuthReply`,
`isExpectedOAuthReply`.
- `color-utils.js`, `xml-utils.js` (`formatXml`): pure helpers.
- `mcp-generator.js`: `generateMcpServer(spec, options) -> { manifest, files, budget, hostConfig }`,
re-exporting `./mcp` (`buildMcpManifest`, `createMcpRuntime`, `writeMcpProject`, `createZip`,
`parseMcpEnabled`, ...).
## Invariants
- **`ProcessSpec` never rejects** (`[spec-parser.js:ProcessSpec]`). Loading and post-load
processing are each wrapped in `try/catch`. Every failure resolves to
`{ specLoadError: true, isSpecLoading: false, info: { title, description, version }, tags: [],
tagGroups: [] }`, where `description` is `"<url> ┃ Not Found (404)"`,
`"Unable to load <url> ┃ <status|message>"`, `"Invalid Spec Definition"`, or
`"Unable to process the spec ┃ <message>"` for failures after loading. Callers check
`specLoadError`, not exceptions. It uses an optional `this` (`requestUpdate`,
`before-render` event), so the component calls it with `.call(element, ...)`.
- **Webhooks** (`[spec-parser.js:groupByTags]`) are grouped after paths and flagged
`isWebhook`; they are never written into the spec's `paths`, so a webhook named like a path
does not replace it.
- **Stable anchors** (`[slugger.js:operationAnchorSource]`, `[slugger.js:OperationSlugger.slug]`):
- Source priority: `operationId` (trimmed, 1-45 chars, not a placeholder like `operation12`),
then `summary` (the parser passes `summary || description`; clamped to 40 chars at a word
boundary), then `methodPathSlug(method, path)`.
- Memoized per method + path (and tag slug), so one operation keeps one `elementId` per run.
- Tag prefix `<tagSlug>--` is applied only when the spec has non-empty `x-tagGroups` or
`x-tag-groups` (`[spec-parser.js:ProcessSpec]`), and never for the tag `general`.
- Collisions try `<prefix><method>-<slug>` first, then `github-slugger`'s `-1`, `-2` suffixes,
which depend on encounter order (paths, then webhooks; methods in `supportedMethods` order).
- Tag and tag-group ids (`tag--<kebab>`, `tag-group--<kebab>`) are not deduplicated.
- The portal links to these ids and checks them with `operationAnchorSource`/`methodPathSlug`;
changing any rule above is a breaking change for published portals.
- **Circular references** (`[spec-parser.js:breakCircularRefs]`): after dereferencing, the tree
is cloned without `x-ref` keys and each cycle becomes `{ $ref }` (fallback
`#/components/schemas/Circular`); shared non-cyclic nodes stay shared. Schema code treats a
remaining `$ref` as the cycle marker, so renderers must not recurse into it.
- **Schema depth limits** (`[schema-utils.js]`): `schemaToSampleObj` returns `undefined` and
`schemaInObjectNotation` returns a `::type` stub when `level > 8`. `schemaToAST` expands to
`MAX_SCHEMA_DEPTH` (8) and, while the tree exceeds `SCHEMA_NODE_BUDGET` (8000 nodes), retries
with a smaller depth down to `MIN_SCHEMA_DEPTH` (4); cut containers become nodes with
`truncated: true`. Its module state makes it synchronous and non-reentrant.
- **Sanitization** (`[sanitize.js:sanitizeHTML]`): non-strings return `""`. The native
`Sanitizer` path allows all attributes; the fallback removes only `<script>`, `on*` attributes
and `javascript:` / `data:text/html` in `href`/`src`. Without `DOMParser` (Node) the input is
returned unchanged, so server-side output is not sanitized.
- **Mock server** (`[mock-interceptor.js:enableMockServer]`): one mock per `options.owner` (the
element; omitted, a page-wide default), so several `<rapi-doc>` / `<rapi-doc-mini>` on a page
mock their own APIs with their own status codes, delay and cycle positions. Repeated calls for an
owner refresh its routes and config but never stack wrappers; `disableMockServer(owner)` drops
that mock and restores the originals once none is left. A request is answered by the most
specific match across the mocks (server origin and base path, then base path, then bare path;
ties to the mock enabled first); unmatched requests fall through to the real `fetch`/XHR. Mock
responses carry `X-Mock-Server: RapiDoc`.
- **Highlighter** (`[highlighter.js]`): `highlightAllCode` and `scheduleHighlight` are no-ops
without `window` and `CSS.highlights`; highlighting never mutates the DOM.
- **Headless boundary**: `spec-parser.js`, `schema-utils.js`, `slugger.js`, `mcp-generator.js`,
`mcp/`, `arazzo/` and everything they import are shared with the Node builds (`@rapidoc/prerender`,
`@rapidoc/mcp`). They never import Lit, elements, `components/`, `templates/`, `styles/` or
`static/`, and use only the third-party packages allowed in `tests/unit/headless-boundary.test.js`.
DOM calls inside functions are allowed (Node never calls them); DOM access at import time is not.
## Side Effects
- `ProcessSpec` fetches the spec and external `$ref`s, logs load errors with `console.info`, and
mutates the spec it processes: `x-ref` during dereference, `paths` replaced by `filterPaths`,
`callbacks` filtered, relative `servers[*].url` prefixed with `location.origin`,
`computedUrl` added.
- `mock-interceptor.js` replaces `window.fetch` and `XMLHttpRequest.prototype` methods while
active and keeps routes and config in module state.
- `markdown-utils.js` configures the global `marked` at import time.
- `highlighter.js` writes the global `CSS.highlights` registry and schedules
`requestAnimationFrame`.
- `bind-navbar.js` adds DOM and `window` listeners, writes `history`; some timers and the
`spec-loaded` listener outlive `unbind()`.
- `auto-color-scheme.js` observes `<html>`/`<body>` attributes and `prefers-color-scheme`.
- `response-utils.js` creates object URLs it does not revoke; `common-utils.js` clipboard and
download helpers create temporary elements.
## Verification
Run `npm run test:unit` from the repo root before and after changing `spec-parser.js` or
`schema-utils.js`, then `npm run lint` and `npm run format-fix`. Files in `tests/unit/`:
- `spec-parser.js`: `spec-parser-relative-url`, `spec-parser-security`,
`spec-parser-tag-groups`, `spec-parser-webhooks`, `unused-tags`, `openapi-3-2-bookstore`.
- `slugger.js`: `slugger`.
- `schema-utils.js`: `schema-utils`, `schema-ast`, `schema-in-object-notation`,
`schema-nullable`, `schema-modernization`, `multipart-form-curl`.
- `url-utils.js`: `query-param-serialization`, `oauth-receiver-url`.
- `request-utils.js`, `curl-utils.js`: `request-utils`, `multipart-part-types`, `curl-command`.
- `response-utils.js`: `api-request-response`.
- `mock-interceptor.js`, `oauth-utils.js`, `markdown-utils.js`, `mcp-generator.js`: the
test file of the same name.
- Headless boundary (imports of the modules above, transitively): `headless-boundary`.