@trpc/openapi
Version:
1,035 lines (1,031 loc) • 42.3 kB
JavaScript
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