UNPKG

@trpc/openapi

Version:

OpenAPI document generator for tRPC routers

1,035 lines (1,031 loc) 42.3 kB
import { __toESM } from "./chunk-CJ2cON1m.mjs"; import { require_objectSpread2 } from "./objectSpread2-Blgb1OZh.mjs"; import * as fs from "node:fs"; import * as path from "node:path"; import * as ts from "typescript"; import { pathToFileURL } from "node:url"; //#region src/schemaExtraction.ts /** * Zod v4 stores `.describe()` strings in `globalThis.__zod_globalRegistry`, * a WeakMap-backed `$ZodRegistry<GlobalMeta>`. We access it via globalThis * because zod is an optional peer dependency. */ function getZodGlobalRegistry() { const reg = globalThis.__zod_globalRegistry; return reg && typeof reg.get === "function" ? reg : null; } /** Runtime check: does this value look like a `$ZodType` (has `_zod.def`)? */ function isZodSchema(value) { if (value == null || typeof value !== "object") return false; const zod = value._zod; return zod != null && typeof zod === "object" && "def" in zod; } /** Get the object shape from a Zod object schema, if applicable. */ function zodObjectShape(schema) { const def = schema._zod.def; if (def.type === "object" && "shape" in def) return def.shape; return null; } /** Get the element schema from a Zod array schema, if applicable. */ function zodArrayElement(schema) { const def = schema._zod.def; if (def.type === "array" && "element" in def) return def.element; return null; } /** Wrapper def types whose inner schema is accessible via `innerType` or `in`. */ const wrapperDefTypes = new Set([ "optional", "nullable", "nonoptional", "default", "prefault", "catch", "readonly", "pipe", "transform", "promise" ]); /** * Extract the wrapped inner schema from a wrapper def. * Most wrappers use `innerType`; `pipe` uses `in`. */ function getWrappedInner(def) { if ("innerType" in def) return def.innerType; if ("in" in def) return def.in; return null; } /** Unwrap wrapper types (optional, nullable, default, readonly, etc.) to get the inner schema. */ function unwrapZodSchema(schema) { let current = schema; const seen = /* @__PURE__ */ new Set(); while (!seen.has(current)) { seen.add(current); const def = current._zod.def; if (!wrapperDefTypes.has(def.type)) break; const inner = getWrappedInner(def); if (!inner) break; current = inner; } return current; } /** * Walk a Zod schema and collect description strings at each property path. * Returns `null` if the value is not a Zod schema or has no descriptions. */ function extractZodDescriptions(schema) { if (!isZodSchema(schema)) return null; const registry = getZodGlobalRegistry(); if (!registry) return null; const map = { properties: /* @__PURE__ */ new Map() }; let hasAny = false; const topMeta = registry.get(schema); if (topMeta === null || topMeta === void 0 ? void 0 : topMeta.description) { map.self = topMeta.description; hasAny = true; } walkZodShape(schema, "", { registry, map, seenLazy: /* @__PURE__ */ new Set() }); if (map.properties.size > 0) hasAny = true; return hasAny ? map : null; } function walkZodShape(schema, prefix, ctx) { const unwrapped = unwrapZodSchema(schema); const def = unwrapped._zod.def; if (def.type === "lazy" && "getter" in def) { if (ctx.seenLazy.has(unwrapped)) return; ctx.seenLazy.add(unwrapped); const inner = def.getter(); if (isZodSchema(inner)) walkZodShape(inner, prefix, ctx); return; } const element = zodArrayElement(unwrapped); if (element) { var _elemMeta$description; const unwrappedElement = unwrapZodSchema(element); const elemMeta = ctx.registry.get(element); const innerElemMeta = unwrappedElement !== element ? ctx.registry.get(unwrappedElement) : void 0; const elemDesc = (_elemMeta$description = elemMeta === null || elemMeta === void 0 ? void 0 : elemMeta.description) !== null && _elemMeta$description !== void 0 ? _elemMeta$description : innerElemMeta === null || innerElemMeta === void 0 ? void 0 : innerElemMeta.description; if (elemDesc) { const itemsPath = prefix ? `${prefix}.[]` : "[]"; ctx.map.properties.set(itemsPath, elemDesc); } walkZodShape(element, prefix, ctx); return; } const shape = zodObjectShape(unwrapped); if (!shape) return; for (const [key, fieldSchema] of Object.entries(shape)) { var _meta$description; const path$1 = prefix ? `${prefix}.${key}` : key; const meta = ctx.registry.get(fieldSchema); const unwrappedField = unwrapZodSchema(fieldSchema); const innerMeta = unwrappedField !== fieldSchema ? ctx.registry.get(unwrappedField) : void 0; const description = (_meta$description = meta === null || meta === void 0 ? void 0 : meta.description) !== null && _meta$description !== void 0 ? _meta$description : innerMeta === null || innerMeta === void 0 ? void 0 : innerMeta.description; if (description) ctx.map.properties.set(path$1, description); walkZodShape(unwrappedField, path$1, ctx); } } /** Check whether a value looks like a tRPC router instance at runtime. */ function isRouterInstance(value) { if (value == null) return false; const obj = value; const def = obj["_def"]; return typeof obj === "object" && def != null && typeof def === "object" && def["record"] != null && typeof def["record"] === "object"; } /** * Search a module's exports for a tRPC router instance. * * Tries (in order): * 1. Exact `exportName` match * 2. lcfirst variant (`AppRouter` → `appRouter`) * 3. First export that looks like a router */ function findRouterExport(mod, exportName) { if (isRouterInstance(mod[exportName])) return mod[exportName]; const lcFirst = exportName.charAt(0).toLowerCase() + exportName.slice(1); if (lcFirst !== exportName && isRouterInstance(mod[lcFirst])) return mod[lcFirst]; for (const value of Object.values(mod)) if (isRouterInstance(value)) return value; return null; } /** * Try to dynamically import the router file and extract a tRPC router * instance. Returns `null` if the import fails (e.g. no TS loader) or * no router export is found. */ async function tryImportRouter(resolvedPath, exportName) { try { const mod = await import(pathToFileURL(resolvedPath).href); return findRouterExport(mod, exportName); } catch (_unused) { return null; } } /** * Walk a runtime tRPC router/record and collect Zod `.describe()` strings * keyed by procedure path. */ function collectRuntimeDescriptions(routerOrRecord, prefix, result) { const record = isRouterInstance(routerOrRecord) ? routerOrRecord._def.record : routerOrRecord; for (const [key, value] of Object.entries(record)) { const fullPath = prefix ? `${prefix}.${key}` : key; if (isProcedure(value)) { const def = value._def; let inputDescs = null; for (const input of def.inputs) { const descs = extractZodDescriptions(input); if (descs) { var _inputDescs, _descs$self; (_inputDescs = inputDescs) !== null && _inputDescs !== void 0 || (inputDescs = { properties: /* @__PURE__ */ new Map() }); inputDescs.self = (_descs$self = descs.self) !== null && _descs$self !== void 0 ? _descs$self : inputDescs.self; for (const [p, d] of descs.properties) inputDescs.properties.set(p, d); } } let outputDescs = null; const outputParser = def["output"]; if (outputParser) outputDescs = extractZodDescriptions(outputParser); if (inputDescs || outputDescs) result.set(fullPath, { input: inputDescs, output: outputDescs }); } else collectRuntimeDescriptions(value, fullPath, result); } } /** Type guard: check if a RouterRecord value is a procedure (callable). */ function isProcedure(value) { return typeof value === "function"; } /** * Overlay description strings from a `DescriptionMap` onto an existing * JSON schema produced by the TypeScript type checker. Mutates in place. */ function applyDescriptions(schema, descs, schemas) { if (descs.self) schema.description = descs.self; for (const [propPath, description] of descs.properties) setNestedDescription({ schema, pathParts: propPath.split("."), description, schemas }); } function resolveSchemaRef(schema, schemas) { var _schemas$refName; const ref = schema.$ref; if (!ref) return schema; if (!schemas || !ref.startsWith("#/components/schemas/")) return null; const refName = ref.slice(21); return refName ? (_schemas$refName = schemas[refName]) !== null && _schemas$refName !== void 0 ? _schemas$refName : null : null; } function getArrayItemsSchema(schema) { const items = schema.items; if (schema.type !== "array" || items == null || items === false) return null; return items; } function getPropertySchema(schema, propertyName) { var _schema$properties$pr, _schema$properties; return (_schema$properties$pr = (_schema$properties = schema.properties) === null || _schema$properties === void 0 ? void 0 : _schema$properties[propertyName]) !== null && _schema$properties$pr !== void 0 ? _schema$properties$pr : null; } function setLeafDescription(schema, description) { if (schema.$ref) { var _schema$allOf; const ref = schema.$ref; delete schema.$ref; schema.allOf = [{ $ref: ref }, ...(_schema$allOf = schema.allOf) !== null && _schema$allOf !== void 0 ? _schema$allOf : []]; } schema.description = description; } function setNestedDescription({ schema, pathParts, description, schemas }) { if (pathParts.length === 0) return; const [head, ...rest] = pathParts; if (!head) return; if (head === "[]") { const items = getArrayItemsSchema(schema); if (!items) return; if (rest.length === 0) setLeafDescription(items, description); else { var _resolveSchemaRef; const target = (_resolveSchemaRef = resolveSchemaRef(items, schemas)) !== null && _resolveSchemaRef !== void 0 ? _resolveSchemaRef : items; setNestedDescription({ schema: target, pathParts: rest, description, schemas }); } return; } const propSchema = getPropertySchema(schema, head); if (!propSchema) return; if (rest.length === 0) setLeafDescription(propSchema, description); else { var _getArrayItemsSchema, _resolveSchemaRef2; const target = (_getArrayItemsSchema = getArrayItemsSchema(propSchema)) !== null && _getArrayItemsSchema !== void 0 ? _getArrayItemsSchema : propSchema; const resolvedTarget = (_resolveSchemaRef2 = resolveSchemaRef(target, schemas)) !== null && _resolveSchemaRef2 !== void 0 ? _resolveSchemaRef2 : target; setNestedDescription({ schema: resolvedTarget, pathParts: rest, description, schemas }); } } //#endregion //#region src/generate.ts var import_objectSpread2 = __toESM(require_objectSpread2(), 1); const PRIMITIVE_FLAGS = ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean | ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.BooleanLiteral; function hasFlag(type, flag) { return (type.getFlags() & flag) !== 0; } function isPrimitive(type) { return hasFlag(type, PRIMITIVE_FLAGS); } function isObjectType(type) { return hasFlag(type, ts.TypeFlags.Object); } function isOptionalSymbol(sym) { return (sym.flags & ts.SymbolFlags.Optional) !== 0; } /** * If `type` is a branded intersection (primitive & object), return just the * primitive part. Otherwise return the type as-is. */ function unwrapBrand(type) { if (!type.isIntersection()) return type; const primitives = type.types.filter(isPrimitive); const hasObject = type.types.some(isObjectType); const [first] = primitives; if (first && hasObject) return first; return type; } const ANONYMOUS_NAMES = new Set([ "__type", "__object", "Object", "" ]); const INTERNAL_COMPUTED_PROPERTY_SYMBOL = /^__@.*@\d+$/; /** Try to determine a meaningful name for a TS type (type alias or interface). */ function getTypeName(type) { var _type$aliasSymbol, _type$getSymbol; const aliasName = (_type$aliasSymbol = type.aliasSymbol) === null || _type$aliasSymbol === void 0 ? void 0 : _type$aliasSymbol.getName(); if (aliasName && !ANONYMOUS_NAMES.has(aliasName)) return aliasName; const symName = (_type$getSymbol = type.getSymbol()) === null || _type$getSymbol === void 0 ? void 0 : _type$getSymbol.getName(); if (symName && !ANONYMOUS_NAMES.has(symName) && !symName.startsWith("__")) return symName; return null; } function shouldSkipPropertySymbol(prop) { var _prop$declarations$so, _prop$declarations; return (_prop$declarations$so = (_prop$declarations = prop.declarations) === null || _prop$declarations === void 0 ? void 0 : _prop$declarations.some((declaration) => { const declarationName = ts.getNameOfDeclaration(declaration); if (!declarationName || !ts.isComputedPropertyName(declarationName)) return false; return INTERNAL_COMPUTED_PROPERTY_SYMBOL.test(prop.getName()); })) !== null && _prop$declarations$so !== void 0 ? _prop$declarations$so : false; } function getReferencedSchema(schema, schemas) { var _schemas$refName; const ref = schema === null || schema === void 0 ? void 0 : schema.$ref; if (!(ref === null || ref === void 0 ? void 0 : ref.startsWith("#/components/schemas/"))) return schema; const refName = ref.slice(21); return refName ? (_schemas$refName = schemas[refName]) !== null && _schemas$refName !== void 0 ? _schemas$refName : null : schema; } function ensureUniqueName(name, existing) { if (!(name in existing)) return name; let i = 2; while (`${name}${i}` in existing) i++; return `${name}${i}`; } function schemaRef(name) { return { $ref: `#/components/schemas/${name}` }; } function isSelfSchemaRef(schema, name) { return schema.$ref === schemaRef(name).$ref; } function isNonEmptySchema(s) { for (const _ in s) return true; return false; } /** * Convert a TS type to a JSON Schema. If the type has been pre-registered * (or has a meaningful TS name), it is stored in `ctx.schemas` and a `$ref` * is returned instead of an inline schema. * * Named types (type aliases, interfaces) are auto-registered before conversion * so that recursive references (including through unions and intersections) * resolve to a `$ref` instead of causing infinite recursion. */ function typeToJsonSchema(type, ctx, depth = 0) { const existingRef = ctx.typeToRef.get(type); if (existingRef) { const storedSchema = ctx.schemas[existingRef]; if (storedSchema && (isNonEmptySchema(storedSchema) || ctx.visited.has(type))) return schemaRef(existingRef); ctx.schemas[existingRef] = storedSchema !== null && storedSchema !== void 0 ? storedSchema : {}; const schema$1 = convertTypeToSchema(type, ctx, depth); if (!isSelfSchemaRef(schema$1, existingRef)) ctx.schemas[existingRef] = schema$1; return schemaRef(existingRef); } const schema = convertTypeToSchema(type, ctx, depth); const postConvertRef = ctx.typeToRef.get(type); if (postConvertRef) { const stored = ctx.schemas[postConvertRef]; if (stored && !isNonEmptySchema(stored) && !isSelfSchemaRef(schema, postConvertRef)) ctx.schemas[postConvertRef] = schema; return schemaRef(postConvertRef); } if (!schema.description && !schema.$ref && type.aliasSymbol) { const aliasJsDoc = getJsDocComment(type.aliasSymbol, ctx.checker); if (aliasJsDoc) schema.description = aliasJsDoc; } return schema; } /** * When we encounter a type we're already visiting, it's recursive. * Register it as a named schema and return a $ref. */ function handleCyclicRef(type, ctx) { let refName = ctx.typeToRef.get(type); if (!refName) { var _getTypeName; const name = (_getTypeName = getTypeName(type)) !== null && _getTypeName !== void 0 ? _getTypeName : "RecursiveType"; refName = ensureUniqueName(name, ctx.schemas); ctx.typeToRef.set(type, refName); ctx.schemas[refName] = {}; } return schemaRef(refName); } function convertPrimitiveOrLiteral(type, flags, checker) { if (flags & ts.TypeFlags.String) return { type: "string" }; if (flags & ts.TypeFlags.Number) return { type: "number" }; if (flags & ts.TypeFlags.Boolean) return { type: "boolean" }; if (flags & ts.TypeFlags.Null) return { type: "null" }; if (flags & ts.TypeFlags.Undefined) return {}; if (flags & ts.TypeFlags.Void) return {}; if (flags & ts.TypeFlags.Any || flags & ts.TypeFlags.Unknown) return {}; if (flags & ts.TypeFlags.Never) return { not: {} }; if (flags & ts.TypeFlags.BigInt || flags & ts.TypeFlags.BigIntLiteral) return { type: "integer", format: "bigint" }; if (flags & ts.TypeFlags.StringLiteral) return { type: "string", const: type.value }; if (flags & ts.TypeFlags.NumberLiteral) return { type: "number", const: type.value }; if (flags & ts.TypeFlags.BooleanLiteral) { const isTrue = checker.typeToString(type) === "true"; return { type: "boolean", const: isTrue }; } return null; } function convertUnionType(type, ctx, depth) { const members = type.types; const defined = members.filter((m) => !hasFlag(m, ts.TypeFlags.Undefined | ts.TypeFlags.Void)); if (defined.length === 0) return {}; const hasNull = defined.some((m) => hasFlag(m, ts.TypeFlags.Null)); const nonNull = defined.filter((m) => !hasFlag(m, ts.TypeFlags.Null)); const boolLiterals = nonNull.filter((m) => hasFlag(unwrapBrand(m), ts.TypeFlags.BooleanLiteral)); const hasBoolPair = boolLiterals.length === 2 && boolLiterals.some((m) => ctx.checker.typeToString(unwrapBrand(m)) === "true") && boolLiterals.some((m) => ctx.checker.typeToString(unwrapBrand(m)) === "false"); const effective = hasBoolPair ? nonNull.filter((m) => !hasFlag(unwrapBrand(m), ts.TypeFlags.BooleanLiteral)) : nonNull; if (hasBoolPair && effective.length === 0) return hasNull ? { type: ["boolean", "null"] } : { type: "boolean" }; const collapsedEnum = tryCollapseLiteralUnion(effective, hasNull); if (collapsedEnum) return collapsedEnum; const schemas = effective.map((m) => typeToJsonSchema(m, ctx, depth + 1)).filter(isNonEmptySchema); if (hasBoolPair) schemas.push({ type: "boolean" }); if (hasNull) schemas.push({ type: "null" }); if (schemas.length === 0) return {}; const [firstSchema] = schemas; if (schemas.length === 1 && firstSchema !== void 0) return firstSchema; if (schemas.every(isSimpleTypeSchema)) return { type: schemas.map((s) => s.type) }; const discriminatorProp = detectDiscriminatorProperty(schemas); if (discriminatorProp) return { oneOf: schemas, discriminator: { propertyName: discriminatorProp } }; return { oneOf: schemas }; } /** * If every schema in a oneOf is an object with a common required property * whose value is a `const`, return that property name. Otherwise return null. */ function detectDiscriminatorProperty(schemas) { if (schemas.length < 2) return null; if (!schemas.every((s) => s.type === "object" && s.properties)) return null; const first = schemas[0]; if (!(first === null || first === void 0 ? void 0 : first.properties)) return null; const firstProps = Object.keys(first.properties); for (const prop of firstProps) { const allHaveConst = schemas.every((s) => { var _s$properties, _s$required; const propSchema = (_s$properties = s.properties) === null || _s$properties === void 0 ? void 0 : _s$properties[prop]; return propSchema !== void 0 && propSchema.const !== void 0 && ((_s$required = s.required) === null || _s$required === void 0 ? void 0 : _s$required.includes(prop)); }); if (allHaveConst) return prop; } return null; } /** A schema that is just `{ type: "somePrimitive" }` with no other keys. */ function isSimpleTypeSchema(s) { const keys = Object.keys(s); return keys.length === 1 && keys[0] === "type" && typeof s.type === "string"; } /** * If every non-null member is a string or number literal of the same kind, * collapse them into a single `{ type, enum }` schema. */ function tryCollapseLiteralUnion(nonNull, hasNull) { if (nonNull.length <= 1) return null; const allLiterals = nonNull.every((m) => hasFlag(m, ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral)); if (!allLiterals) return null; const [first] = nonNull; if (!first) return null; const isString = hasFlag(first, ts.TypeFlags.StringLiteral); const targetFlag = isString ? ts.TypeFlags.StringLiteral : ts.TypeFlags.NumberLiteral; const allSameKind = nonNull.every((m) => hasFlag(m, targetFlag)); if (!allSameKind) return null; const values = nonNull.map((m) => isString ? m.value : m.value); const baseType = isString ? "string" : "number"; return { type: hasNull ? [baseType, "null"] : baseType, enum: values }; } function convertIntersectionType(type, ctx, depth) { const hasPrimitiveMember = type.types.some(isPrimitive); const nonBrand = hasPrimitiveMember ? type.types.filter((m) => !isObjectType(m)) : type.types; const schemas = nonBrand.map((m) => typeToJsonSchema(m, ctx, depth + 1)).filter(isNonEmptySchema); if (schemas.length === 0) return {}; const [onlySchema] = schemas; if (schemas.length === 1 && onlySchema !== void 0) return onlySchema; if (schemas.every(isInlineObjectSchema)) return mergeObjectSchemas(schemas); return { allOf: schemas }; } /** True when the schema is an inline `{ type: "object", ... }` (not a $ref). */ function isInlineObjectSchema(s) { return s.type === "object" && !s.$ref; } /** * Merge multiple `{ type: "object" }` schemas into one. * Falls back to `allOf` if any property names conflict across schemas. */ function mergeObjectSchemas(schemas) { const seen = /* @__PURE__ */ new Set(); for (const s of schemas) if (s.properties) for (const prop of Object.keys(s.properties)) { if (seen.has(prop)) return { allOf: schemas }; seen.add(prop); } const properties = {}; const required = []; let additionalProperties; for (const s of schemas) { if (s.properties) Object.assign(properties, s.properties); if (s.required) required.push(...s.required); if (s.additionalProperties !== void 0) additionalProperties = s.additionalProperties; } const result = { type: "object" }; if (Object.keys(properties).length > 0) result.properties = properties; if (required.length > 0) result.required = required; if (additionalProperties !== void 0) result.additionalProperties = additionalProperties; return result; } function convertWellKnownType(type, ctx, depth) { var _type$getSymbol2; const symName = (_type$getSymbol2 = type.getSymbol()) === null || _type$getSymbol2 === void 0 ? void 0 : _type$getSymbol2.getName(); if (symName === "Date") return { type: "string", format: "date-time" }; if (symName === "Uint8Array" || symName === "Buffer") return { type: "string", format: "binary" }; if (symName === "Promise") { const [inner] = ctx.checker.getTypeArguments(type); return inner ? typeToJsonSchema(inner, ctx, depth + 1) : {}; } return null; } function convertArrayType(type, ctx, depth) { const [elem] = ctx.checker.getTypeArguments(type); const schema = { type: "array" }; if (elem) schema.items = typeToJsonSchema(elem, ctx, depth + 1); return schema; } function convertTupleType(type, ctx, depth) { const args = ctx.checker.getTypeArguments(type); const schemas = args.map((a) => typeToJsonSchema(a, ctx, depth + 1)); return { type: "array", prefixItems: schemas, items: false, minItems: args.length, maxItems: args.length }; } function convertPlainObject(type, ctx, depth) { var _autoRegName; const { checker } = ctx; const stringIndexType = type.getStringIndexType(); const typeProps = type.getProperties(); if (typeProps.length === 0 && stringIndexType) return { type: "object", additionalProperties: typeToJsonSchema(stringIndexType, ctx, depth + 1) }; let autoRegName = null; const tsName = getTypeName(type); const isNamedUnregisteredType = tsName !== null && typeProps.length > 0 && !ctx.typeToRef.has(type); if (isNamedUnregisteredType) { autoRegName = ensureUniqueName(tsName, ctx.schemas); ctx.typeToRef.set(type, autoRegName); ctx.schemas[autoRegName] = {}; } ctx.visited.add(type); const properties = {}; const required = []; for (const prop of typeProps) { if (shouldSkipPropertySymbol(prop)) continue; const propType = checker.getTypeOfSymbol(prop); const propSchema = typeToJsonSchema(propType, ctx, depth + 1); const jsDoc = getJsDocComment(prop, checker); if (jsDoc && !propSchema.description && !propSchema.$ref) propSchema.description = jsDoc; properties[prop.name] = propSchema; if (!isOptionalSymbol(prop)) required.push(prop.name); } ctx.visited.delete(type); const result = { type: "object" }; if (Object.keys(properties).length > 0) result.properties = properties; if (required.length > 0) result.required = required; if (stringIndexType) result.additionalProperties = typeToJsonSchema(stringIndexType, ctx, depth + 1); else if (Object.keys(properties).length > 0) result.additionalProperties = false; const registeredName = (_autoRegName = autoRegName) !== null && _autoRegName !== void 0 ? _autoRegName : ctx.typeToRef.get(type); if (registeredName) { ctx.schemas[registeredName] = result; return schemaRef(registeredName); } return result; } function convertObjectType(type, ctx, depth) { const wellKnown = convertWellKnownType(type, ctx, depth); if (wellKnown) return wellKnown; if (ctx.checker.isArrayType(type)) return convertArrayType(type, ctx, depth); if (ctx.checker.isTupleType(type)) return convertTupleType(type, ctx, depth); return convertPlainObject(type, ctx, depth); } /** Core type-to-schema conversion (no ref handling). */ function convertTypeToSchema(type, ctx, depth) { if (ctx.visited.has(type)) return handleCyclicRef(type, ctx); const flags = type.getFlags(); const primitive = convertPrimitiveOrLiteral(type, flags, ctx.checker); if (primitive) return primitive; if (type.isUnion()) { ctx.visited.add(type); const result = convertUnionType(type, ctx, depth); ctx.visited.delete(type); return result; } if (type.isIntersection()) { ctx.visited.add(type); const result = convertIntersectionType(type, ctx, depth); ctx.visited.delete(type); return result; } if (isObjectType(type)) return convertObjectType(type, ctx, depth); return {}; } /** * Inspect `_def.type` and return the procedure type string, or null if this is * not a procedure (e.g. a nested router). */ function getProcedureTypeName(defType, checker) { const typeSym = defType.getProperty("type"); if (!typeSym) return null; const typeType = checker.getTypeOfSymbol(typeSym); const raw = checker.typeToString(typeType).replace(/['"]/g, ""); if (raw === "query" || raw === "mutation" || raw === "subscription") return raw; return null; } function isVoidLikeInput(inputType) { if (!inputType) return true; const isVoidOrUndefinedOrNever = hasFlag(inputType, ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never); if (isVoidOrUndefinedOrNever) return true; const isUnionOfVoids = inputType.isUnion() && inputType.types.every((t) => hasFlag(t, ts.TypeFlags.Void | ts.TypeFlags.Undefined)); return isUnionOfVoids; } function shouldIncludeProcedureInOpenAPI(type) { return type !== "subscription"; } function getProcedureInputTypeName(type, path$1) { const directName = getTypeName(type); if (directName) return directName; for (const sym of [type.aliasSymbol, type.getSymbol()].filter((candidate) => !!candidate)) { var _sym$declarations; for (const declaration of (_sym$declarations = sym.declarations) !== null && _sym$declarations !== void 0 ? _sym$declarations : []) { var _ts$getNameOfDeclarat; const declarationName = (_ts$getNameOfDeclarat = ts.getNameOfDeclaration(declaration)) === null || _ts$getNameOfDeclarat === void 0 ? void 0 : _ts$getNameOfDeclarat.getText(); if (declarationName && !ANONYMOUS_NAMES.has(declarationName) && !declarationName.startsWith("__")) return declarationName; } } const fallbackName = path$1.split(".").filter(Boolean).map((segment) => segment.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")).join(""); return `${fallbackName || "Procedure"}Input`; } function isUnknownLikeType(type) { return hasFlag(type, ts.TypeFlags.Unknown | ts.TypeFlags.Any); } function isCollapsedProcedureInputType(type) { return isUnknownLikeType(type) || isObjectType(type) && type.getProperties().length === 0 && !type.getStringIndexType(); } function recoverProcedureInputType(def, checker) { var _def$symbol$declarati; let initializer = null; for (const declaration of (_def$symbol$declarati = def.symbol.declarations) !== null && _def$symbol$declarati !== void 0 ? _def$symbol$declarati : []) { if (ts.isPropertyAssignment(declaration)) { initializer = declaration.initializer; break; } if (ts.isVariableDeclaration(declaration) && declaration.initializer) { initializer = declaration.initializer; break; } } if (!initializer) return null; let recovered = null; const visit = (expr) => { if (!ts.isCallExpression(expr)) return; const callee = expr.expression; if (!ts.isPropertyAccessExpression(callee)) return; visit(callee.expression); if (callee.name.text !== "input") return; const [parserExpr] = expr.arguments; if (!parserExpr) return; const parserType = checker.getTypeAtLocation(parserExpr); const standardSym = parserType.getProperty("~standard"); if (!standardSym) return; const standardType = checker.getTypeOfSymbolAtLocation(standardSym, parserExpr); const typesSym = standardType.getProperty("types"); if (!typesSym) return; const typesType = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(typesSym, parserExpr)); const outputSym = typesType.getProperty("output"); if (!outputSym) return; const outputType = checker.getTypeOfSymbolAtLocation(outputSym, parserExpr); if (!isUnknownLikeType(outputType)) recovered = outputType; }; visit(initializer); return recovered; } function extractProcedure(def, ctx) { var _recoverProcedureInpu; const { schemaCtx } = ctx; const { checker } = schemaCtx; const $typesSym = def.defType.getProperty("$types"); if (!$typesSym) return; const $typesType = checker.getTypeOfSymbol($typesSym); const inputSym = $typesType.getProperty("input"); const outputSym = $typesType.getProperty("output"); const inputType = inputSym ? checker.getTypeOfSymbol(inputSym) : null; const outputType = outputSym ? checker.getTypeOfSymbol(outputSym) : null; const resolvedInputType = inputType && isCollapsedProcedureInputType(inputType) ? (_recoverProcedureInpu = recoverProcedureInputType(def, checker)) !== null && _recoverProcedureInpu !== void 0 ? _recoverProcedureInpu : inputType : inputType; let inputSchema = null; if (!resolvedInputType || isVoidLikeInput(resolvedInputType)) {} else { const ensureRecoveredInputRegistration = (type) => { if (schemaCtx.typeToRef.has(type)) return; const refName = ensureUniqueName(getProcedureInputTypeName(type, def.path), schemaCtx.schemas); schemaCtx.typeToRef.set(type, refName); schemaCtx.schemas[refName] = {}; }; if (resolvedInputType !== inputType) ensureRecoveredInputRegistration(resolvedInputType); const initialSchema = typeToJsonSchema(resolvedInputType, schemaCtx); if (!isNonEmptySchema(initialSchema) && !schemaCtx.typeToRef.has(resolvedInputType)) { ensureRecoveredInputRegistration(resolvedInputType); inputSchema = typeToJsonSchema(resolvedInputType, schemaCtx); } else inputSchema = initialSchema; } const outputSchema = outputType ? typeToJsonSchema(outputType, schemaCtx) : null; const runtimeDescs = ctx.runtimeDescriptions.get(def.path); if (runtimeDescs) { const resolvedInputSchema = getReferencedSchema(inputSchema, schemaCtx.schemas); const resolvedOutputSchema = getReferencedSchema(outputSchema, schemaCtx.schemas); if (resolvedInputSchema && runtimeDescs.input) applyDescriptions(resolvedInputSchema, runtimeDescs.input, schemaCtx.schemas); if (resolvedOutputSchema && runtimeDescs.output) applyDescriptions(resolvedOutputSchema, runtimeDescs.output, schemaCtx.schemas); } ctx.procedures.push({ path: def.path, type: def.typeName, inputSchema, outputSchema, description: def.description }); } /** Extract the JSDoc comment text from a symbol, if any. */ function getJsDocComment(sym, checker) { var _sym$declarations2; const isWithinPath = (candidate, parent) => { const rel = path.relative(parent, candidate); return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); }; const normalize = (filePath) => filePath.replace(/\\/g, "/"); const workspaceRoot = normalize(process.cwd()); const declarations = (_sym$declarations2 = sym.declarations) !== null && _sym$declarations2 !== void 0 ? _sym$declarations2 : []; const isExternalNodeModulesDeclaration = declarations.length > 0 && declarations.every((declaration) => { const sourceFile = declaration.getSourceFile(); if (!sourceFile.isDeclarationFile) return false; const declarationPath = normalize(sourceFile.fileName); if (!declarationPath.includes("/node_modules/")) return false; try { const realPath = normalize(fs.realpathSync.native(sourceFile.fileName)); if (isWithinPath(realPath, workspaceRoot) && !realPath.includes("/node_modules/")) return false; } catch (_unused) {} return true; }); if (isExternalNodeModulesDeclaration) return void 0; const parts = sym.getDocumentationComment(checker); if (parts.length === 0) return void 0; const text = parts.map((p) => p.text).join(""); return text || void 0; } function walkType(opts) { const { type, ctx, currentPath, description, symbol } = opts; if (ctx.seen.has(type)) return; const defSym = type.getProperty("_def"); if (!defSym) { if (isObjectType(type)) { ctx.seen.add(type); walkRecord(type, ctx, currentPath); ctx.seen.delete(type); } return; } const { checker } = ctx.schemaCtx; const defType = checker.getTypeOfSymbol(defSym); const procedureTypeName = getProcedureTypeName(defType, checker); if (procedureTypeName) { var _ref; if (!shouldIncludeProcedureInOpenAPI(procedureTypeName)) return; extractProcedure({ defType, typeName: procedureTypeName, path: currentPath, description, symbol: (_ref = symbol !== null && symbol !== void 0 ? symbol : type.getSymbol()) !== null && _ref !== void 0 ? _ref : defSym }, ctx); return; } const routerSym = defType.getProperty("router"); if (!routerSym) return; const isRouter = checker.typeToString(checker.getTypeOfSymbol(routerSym)) === "true"; if (!isRouter) return; const recordSym = defType.getProperty("record"); if (!recordSym) return; ctx.seen.add(type); const recordType = checker.getTypeOfSymbol(recordSym); walkRecord(recordType, ctx, currentPath); ctx.seen.delete(type); } function walkRecord(recordType, ctx, prefix) { for (const prop of recordType.getProperties()) { const propType = ctx.schemaCtx.checker.getTypeOfSymbol(prop); const fullPath = prefix ? `${prefix}.${prop.name}` : prop.name; const description = getJsDocComment(prop, ctx.schemaCtx.checker); walkType({ type: propType, ctx, currentPath: fullPath, description, symbol: prop }); } } function loadCompilerOptions(startDir) { const configPath = ts.findConfigFile(startDir, (f) => ts.sys.fileExists(f), "tsconfig.json"); if (!configPath) return { target: ts.ScriptTarget.ES2020, moduleResolution: ts.ModuleResolutionKind.Bundler, skipLibCheck: true, noEmit: true }; const configFile = ts.readConfigFile(configPath, (f) => ts.sys.readFile(f)); const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(configPath)); const options = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, parsed.options), {}, { noEmit: true }); if (options.moduleResolution === void 0) { const mod = options.module; if (mod === ts.ModuleKind.Node16 || mod === ts.ModuleKind.NodeNext) options.moduleResolution = ts.ModuleResolutionKind.NodeNext; else if (mod === ts.ModuleKind.Preserve || mod === ts.ModuleKind.ES2022 || mod === ts.ModuleKind.ESNext) options.moduleResolution = ts.ModuleResolutionKind.Bundler; else options.moduleResolution = ts.ModuleResolutionKind.Node10; } return options; } /** * Walk `_def._config.$types.errorShape` on the router type and convert * it to a JSON Schema. Returns `null` when the path cannot be resolved * (e.g. older tRPC versions or missing type info). */ function extractErrorSchema(routerType, checker, schemaCtx) { const walk = (type, keys) => { const [head, ...rest] = keys; if (!head) return type; const sym = type.getProperty(head); if (!sym) return null; return walk(checker.getTypeOfSymbol(sym), rest); }; const errorShapeType = walk(routerType, [ "_def", "_config", "$types", "errorShape" ]); if (!errorShapeType) return null; if (hasFlag(errorShapeType, ts.TypeFlags.Any)) return null; return typeToJsonSchema(errorShapeType, schemaCtx); } /** Fallback error schema when the router type doesn't expose an error shape. */ const DEFAULT_ERROR_SCHEMA = { type: "object", properties: { message: { type: "string" }, code: { type: "string" }, data: { type: "object" } }, required: ["message", "code"] }; /** * Wrap a procedure's output schema in the tRPC success envelope. * * tRPC HTTP responses are always serialised as: * `{ result: { data: T } }` * * When the procedure has no output the envelope is still present but * the `data` property is omitted. */ function wrapInSuccessEnvelope(outputSchema) { const hasOutput = outputSchema !== null && isNonEmptySchema(outputSchema); const resultSchema = (0, import_objectSpread2.default)({ type: "object", properties: (0, import_objectSpread2.default)({}, hasOutput ? { data: outputSchema } : {}) }, hasOutput ? { required: ["data"] } : {}); return { type: "object", properties: { result: resultSchema }, required: ["result"] }; } function buildProcedureOperation(proc, method) { const [tag = proc.path] = proc.path.split("."); const operation = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({ operationId: proc.path }, proc.description ? { description: proc.description } : {}), {}, { tags: [tag], responses: { "200": { description: "Successful response", content: { "application/json": { schema: wrapInSuccessEnvelope(proc.outputSchema) } } }, default: { $ref: "#/components/responses/Error" } } }); if (proc.inputSchema === null) return operation; if (method === "get") operation.parameters = [{ name: "input", in: "query", required: true, style: "deepObject", content: { "application/json": { schema: proc.inputSchema } } }]; else operation.requestBody = { required: true, content: { "application/json": { schema: proc.inputSchema } } }; return operation; } function buildOpenAPIDocument(procedures, options, meta = { errorSchema: null }) { var _options$title, _options$version, _meta$errorSchema; const paths = {}; for (const proc of procedures) { var _paths$opPath; if (!shouldIncludeProcedureInOpenAPI(proc.type)) continue; const opPath = `/${proc.path}`; const method = proc.type === "query" ? "get" : "post"; const pathItem = (_paths$opPath = paths[opPath]) !== null && _paths$opPath !== void 0 ? _paths$opPath : {}; paths[opPath] = pathItem; pathItem[method] = buildProcedureOperation(proc, method); } const hasNamedSchemas = meta.schemas !== void 0 && Object.keys(meta.schemas).length > 0; return { openapi: "3.1.1", jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base", info: { title: (_options$title = options.title) !== null && _options$title !== void 0 ? _options$title : "tRPC API", version: (_options$version = options.version) !== null && _options$version !== void 0 ? _options$version : "0.0.0" }, paths, components: (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, hasNamedSchemas && meta.schemas ? { schemas: meta.schemas } : {}), {}, { responses: { Error: { description: "Error response", content: { "application/json": { schema: { type: "object", properties: { error: (_meta$errorSchema = meta.errorSchema) !== null && _meta$errorSchema !== void 0 ? _meta$errorSchema : DEFAULT_ERROR_SCHEMA }, required: ["error"] } } } } } }) }; } /** * Analyse the given TypeScript router file using the TypeScript compiler and * return an OpenAPI 3.1 document describing all query and mutation procedures. * * @param routerFilePath - Absolute or relative path to the file that exports * the AppRouter. * @param options - Optional generation settings (export name, title, version). */ async function generateOpenAPIDocument(routerFilePath, options = {}) { var _options$exportName; const resolvedPath = path.resolve(routerFilePath); const exportName = (_options$exportName = options.exportName) !== null && _options$exportName !== void 0 ? _options$exportName : "AppRouter"; const compilerOptions = loadCompilerOptions(path.dirname(resolvedPath)); const program = ts.createProgram([resolvedPath], compilerOptions); const checker = program.getTypeChecker(); const sourceFile = program.getSourceFile(resolvedPath); if (!sourceFile) throw new Error(`Could not load TypeScript file: ${resolvedPath}`); const moduleSymbol = checker.getSymbolAtLocation(sourceFile); if (!moduleSymbol) throw new Error(`No module exports found in: ${resolvedPath}`); const tsExports = checker.getExportsOfModule(moduleSymbol); const routerSymbol = tsExports.find((sym) => sym.getName() === exportName); if (!routerSymbol) { const available = tsExports.map((e) => e.getName()).join(", "); throw new Error(`No export named '${exportName}' found in: ${resolvedPath}\nAvailable exports: ${available || "(none)"}`); } let routerType; if (routerSymbol.valueDeclaration) routerType = checker.getTypeOfSymbolAtLocation(routerSymbol, routerSymbol.valueDeclaration); else routerType = checker.getDeclaredTypeOfSymbol(routerSymbol); const schemaCtx = { checker, visited: /* @__PURE__ */ new Set(), schemas: {}, typeToRef: /* @__PURE__ */ new Map() }; const runtimeDescriptions = /* @__PURE__ */ new Map(); const router = await tryImportRouter(resolvedPath, exportName); if (router) collectRuntimeDescriptions(router, "", runtimeDescriptions); const walkCtx = { procedures: [], seen: /* @__PURE__ */ new Set(), schemaCtx, runtimeDescriptions }; walkType({ type: routerType, ctx: walkCtx, currentPath: "" }); const errorSchema = extractErrorSchema(routerType, checker, schemaCtx); return buildOpenAPIDocument(walkCtx.procedures, options, { errorSchema, schemas: schemaCtx.schemas }); } //#endregion //#region src/types.ts var types_exports = {}; //#endregion export { types_exports as OpenAPIV3_1, generateOpenAPIDocument }; //# sourceMappingURL=index.mjs.map