From 35e624270c276ec693f784010b49a88e218d7741 Mon Sep 17 00:00:00 2001 From: bryantgillespie Date: Mon, 1 Jun 2026 16:35:01 -0400 Subject: [PATCH 1/3] Reduce API reference prerender memory --- app/components/ApiEndpoint.vue | 2 +- app/components/ApiInlineMarkdown.vue | 34 +++++++++++++++++ app/components/ApiParamsField.vue | 2 +- app/utils/flattenSchema.ts | 56 ++++++++++++++++++++++------ app/utils/resolveOasRef.ts | 23 +++++++++++- app/utils/responseToExample.ts | 15 ++++++-- 6 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 app/components/ApiInlineMarkdown.vue diff --git a/app/components/ApiEndpoint.vue b/app/components/ApiEndpoint.vue index a7a3b7cbb..6c45760ba 100644 --- a/app/components/ApiEndpoint.vue +++ b/app/components/ApiEndpoint.vue @@ -125,7 +125,7 @@ const statusCodeDescriptions: StatusCodeDescriptions = { }" class="[&_p]:my-0" > - diff --git a/app/components/ApiInlineMarkdown.vue b/app/components/ApiInlineMarkdown.vue new file mode 100644 index 000000000..0982fe883 --- /dev/null +++ b/app/components/ApiInlineMarkdown.vue @@ -0,0 +1,34 @@ + + + diff --git a/app/components/ApiParamsField.vue b/app/components/ApiParamsField.vue index 0840295c9..19e70bf86 100644 --- a/app/components/ApiParamsField.vue +++ b/app/components/ApiParamsField.vue @@ -17,7 +17,7 @@ defineProps<{ }" class="[&_p]:my-0" > - >(); + export default function (openapi: OpenAPIObject, schema: SchemaObject | ReferenceObject): FlattenedParam | null { - const parseLevel = (schemaOrRef: SchemaObject | ReferenceObject, name?: string): FlattenedParam | null => { - const schema = '$ref' in schemaOrRef ? resolveOasRef(openapi, schemaOrRef.$ref) : schemaOrRef; + let cache = caches.get(openapi); + + if (!cache) { + cache = new Map(); + caches.set(openapi, cache); + } + + // Tracks $refs on the current descent path to break self-referential cycles. + const parseLevel = (schemaOrRef: SchemaObject | ReferenceObject, name?: string, seen: Set = new Set()): FlattenedParam | null => { + const ref = '$ref' in schemaOrRef ? schemaOrRef.$ref : null; + + if (ref) { + if (seen.has(ref)) { + // Circular reference - emit a stub instead of recursing forever. + return { name, type: 'object', description: undefined }; + } + + if (cache.has(ref)) { + const cached = cache.get(ref)!; + return cached ? { ...cached, name } : null; + } + } + + const schema = ref ? resolveOasRef(openapi, ref) : schemaOrRef as SchemaObject; if (!schema) return null; + const nextSeen = ref ? new Set(seen).add(ref) : seen; + const type = Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type; const node: FlattenedParam = { name: name, type, description: schema.description }; if ('anyOf' in schema && schema.anyOf) { node.anyOf = schema.anyOf - .map(child => parseLevel(child)) + .map(child => parseLevel(child, undefined, nextSeen)) .filter((child): child is FlattenedParam => child !== null); - - return node; } - - if (schema.type === 'object') { + else if (schema.type === 'object') { node.children = Object.entries(schema.properties ?? {}) - .map(([key, value]) => parseLevel(value, key)) + .map(([key, value]) => parseLevel(value, key, nextSeen)) .filter((child): child is FlattenedParam => child !== null); } - - if (schema.type === 'array') { - const parsedItems = parseLevel(schema.items ?? {}); + else if (schema.type === 'array') { + const parsedItems = parseLevel(schema.items ?? {}, undefined, nextSeen); if (parsedItems) { node.children = [parsedItems]; } } + // Cache the fully-resolved node for this ref (sans the call-specific name) + // so other operations referencing it reuse the result. + if (ref) { + cache.set(ref, { ...node, name: undefined }); + } + return node; }; diff --git a/app/utils/resolveOasRef.ts b/app/utils/resolveOasRef.ts index c7c161383..c69946027 100644 --- a/app/utils/resolveOasRef.ts +++ b/app/utils/resolveOasRef.ts @@ -1,12 +1,33 @@ import { get } from 'lodash-es'; import type { OpenAPIObject } from 'openapi3-ts/oas30'; +/** + * Resolved $ref values are stable for a given spec, so cache them by ref string + * to avoid repeatedly walking the spec object. Keyed per spec via a WeakMap so + * the cache stays correct if more than one spec is used (e.g. in tests) and is + * released when a spec is garbage collected. + */ +const caches = new WeakMap>(); + /** * Resolve a $ref path from the OpenAPI spec object * * @note This does not support relative refs */ export default function(spec: OpenAPIObject, path: string): O | null { + let cache = caches.get(spec); + + if (!cache) { + cache = new Map(); + caches.set(spec, cache); + } + + if (cache.has(path)) { + return cache.get(path) as O | null; + } + const pathParts = path.split('/').slice(1); - return get(spec, pathParts, null); + const resolved = get(spec, pathParts, null) as O | null; + cache.set(path, resolved); + return resolved; } diff --git a/app/utils/responseToExample.ts b/app/utils/responseToExample.ts index 5b00ee266..d9df25220 100644 --- a/app/utils/responseToExample.ts +++ b/app/utils/responseToExample.ts @@ -14,19 +14,26 @@ export default function (openapi: OpenAPIObject, root: SchemaObject): unknown | return null; } - const parseLevel = (schemaOrRef: SchemaObject | ReferenceObject): unknown => { - const schemaObj = '$ref' in schemaOrRef ? resolveOasRef(openapi, schemaOrRef.$ref) : schemaOrRef; + // Tracks $refs on the current descent path to break self-referential cycles. + const parseLevel = (schemaOrRef: SchemaObject | ReferenceObject, seen: Set = new Set()): unknown => { + const ref = '$ref' in schemaOrRef ? schemaOrRef.$ref : null; + + if (ref && seen.has(ref)) return undefined; + + const schemaObj = ref ? resolveOasRef(openapi, ref) : schemaOrRef as SchemaObject; if (!schemaObj) return undefined; if ('example' in schemaObj) return schemaObj.example; + const nextSeen = ref ? new Set(seen).add(ref) : seen; + if (schemaObj.type === 'object') { const obj: ExampleObject = {}; if (schemaObj.properties) { for (const [key, value] of Object.entries(schemaObj.properties)) { - const parsedVal = parseLevel(value); + const parsedVal = parseLevel(value, nextSeen); if (parsedVal !== undefined) { obj[key] = parsedVal; @@ -39,7 +46,7 @@ export default function (openapi: OpenAPIObject, root: SchemaObject): unknown | if (schemaObj.type === 'array') { if (schemaObj.items) { - const parsedVal = parseLevel(schemaObj.items); + const parsedVal = parseLevel(schemaObj.items, nextSeen); if (parsedVal !== undefined) return [parsedVal]; return []; } From 0e99c1b1ebb2496c7112fd6ca1ba758afa1680df Mon Sep 17 00:00:00 2001 From: bryantgillespie Date: Mon, 1 Jun 2026 16:54:41 -0400 Subject: [PATCH 2/3] Generate API reference page payloads --- .gitignore | 1 + app/components/ApiEndpoint.vue | 93 ++------- app/layouts/api.vue | 6 +- app/pages/api/[tag].vue | 43 ++-- app/pages/api/index.vue | 6 +- app/types.ts | 45 +++++ app/utils/codeSamplesMd.ts | 13 +- app/utils/preMd.ts | 2 + nuxt.config.ts | 11 +- package.json | 9 +- scripts/generate-api-reference.ts | 325 ++++++++++++++++++++++++++++++ 11 files changed, 428 insertions(+), 126 deletions(-) create mode 100644 scripts/generate-api-reference.ts diff --git a/.gitignore b/.gitignore index 4b70306eb..55d54301e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .nitro .cache dist +app/generated/api-reference # Node dependencies node_modules diff --git a/app/components/ApiEndpoint.vue b/app/components/ApiEndpoint.vue index 6c45760ba..60487fc09 100644 --- a/app/components/ApiEndpoint.vue +++ b/app/components/ApiEndpoint.vue @@ -1,68 +1,15 @@