@trpc/openapi 0.0.0-alpha.0 → 11.13.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,834 @@
1
+ const require_objectSpread2$1 = require('./objectSpread2-Cw30I7tb.cjs');
2
+ const node_path = require_objectSpread2$1.__toESM(require("node:path"));
3
+ const typescript = require_objectSpread2$1.__toESM(require("typescript"));
4
+ const node_url = require_objectSpread2$1.__toESM(require("node:url"));
5
+
6
+ //#region src/schemaExtraction.ts
7
+ /**
8
+ * Zod v4 stores `.describe()` strings in `globalThis.__zod_globalRegistry`,
9
+ * a WeakMap-backed `$ZodRegistry<GlobalMeta>`. We access it via globalThis
10
+ * because zod is an optional peer dependency.
11
+ */
12
+ function getZodGlobalRegistry() {
13
+ const reg = globalThis.__zod_globalRegistry;
14
+ return reg && typeof reg.get === "function" ? reg : null;
15
+ }
16
+ /** Runtime check: does this value look like a `$ZodType` (has `_zod.def`)? */
17
+ function isZodSchema(value) {
18
+ if (value == null || typeof value !== "object") return false;
19
+ const zod = value._zod;
20
+ return zod != null && typeof zod === "object" && "def" in zod;
21
+ }
22
+ /** Get the object shape from a Zod object schema, if applicable. */
23
+ function zodObjectShape(schema) {
24
+ const def = schema._zod.def;
25
+ if (def.type === "object" && "shape" in def) return def.shape;
26
+ return null;
27
+ }
28
+ /** Get the element schema from a Zod array schema, if applicable. */
29
+ function zodArrayElement(schema) {
30
+ const def = schema._zod.def;
31
+ if (def.type === "array" && "element" in def) return def.element;
32
+ return null;
33
+ }
34
+ /** Wrapper def types whose inner schema is accessible via `innerType` or `in`. */
35
+ const wrapperDefTypes = new Set([
36
+ "optional",
37
+ "nullable",
38
+ "nonoptional",
39
+ "default",
40
+ "prefault",
41
+ "catch",
42
+ "readonly",
43
+ "pipe",
44
+ "transform",
45
+ "promise",
46
+ "lazy"
47
+ ]);
48
+ /**
49
+ * Extract the wrapped inner schema from a wrapper def.
50
+ * Most wrappers use `innerType`; `pipe` uses `in`.
51
+ */
52
+ function getWrappedInner(def) {
53
+ if ("innerType" in def) return def.innerType;
54
+ if ("in" in def) return def.in;
55
+ return null;
56
+ }
57
+ /** Unwrap wrapper types (optional, nullable, default, readonly, etc.) to get the inner schema. */
58
+ function unwrapZodSchema(schema) {
59
+ let current = schema;
60
+ const seen = /* @__PURE__ */ new Set();
61
+ while (!seen.has(current)) {
62
+ seen.add(current);
63
+ const def = current._zod.def;
64
+ if (!wrapperDefTypes.has(def.type)) break;
65
+ const inner = getWrappedInner(def);
66
+ if (!inner) break;
67
+ current = inner;
68
+ }
69
+ return current;
70
+ }
71
+ /**
72
+ * Walk a Zod schema and collect description strings at each property path.
73
+ * Returns `null` if the value is not a Zod schema or has no descriptions.
74
+ */
75
+ function extractZodDescriptions(schema) {
76
+ if (!isZodSchema(schema)) return null;
77
+ const registry = getZodGlobalRegistry();
78
+ if (!registry) return null;
79
+ const map = { properties: /* @__PURE__ */ new Map() };
80
+ let hasAny = false;
81
+ const topMeta = registry.get(schema);
82
+ if (topMeta === null || topMeta === void 0 ? void 0 : topMeta.description) {
83
+ map.self = topMeta.description;
84
+ hasAny = true;
85
+ }
86
+ walkZodShape(schema, "", {
87
+ registry,
88
+ map
89
+ });
90
+ if (map.properties.size > 0) hasAny = true;
91
+ return hasAny ? map : null;
92
+ }
93
+ function walkZodShape(schema, prefix, ctx) {
94
+ const unwrapped = unwrapZodSchema(schema);
95
+ const element = zodArrayElement(unwrapped);
96
+ if (element) {
97
+ var _elemMeta$description;
98
+ const unwrappedElement = unwrapZodSchema(element);
99
+ const elemMeta = ctx.registry.get(element);
100
+ const innerElemMeta = unwrappedElement !== element ? ctx.registry.get(unwrappedElement) : void 0;
101
+ 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;
102
+ if (elemDesc) {
103
+ const itemsPath = prefix ? `${prefix}.[]` : "[]";
104
+ ctx.map.properties.set(itemsPath, elemDesc);
105
+ }
106
+ walkZodShape(element, prefix, ctx);
107
+ return;
108
+ }
109
+ const shape = zodObjectShape(unwrapped);
110
+ if (!shape) return;
111
+ for (const [key, fieldSchema] of Object.entries(shape)) {
112
+ var _meta$description;
113
+ const path = prefix ? `${prefix}.${key}` : key;
114
+ const meta = ctx.registry.get(fieldSchema);
115
+ const unwrappedField = unwrapZodSchema(fieldSchema);
116
+ const innerMeta = unwrappedField !== fieldSchema ? ctx.registry.get(unwrappedField) : void 0;
117
+ 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;
118
+ if (description) ctx.map.properties.set(path, description);
119
+ walkZodShape(unwrappedField, path, ctx);
120
+ }
121
+ }
122
+ /** Check whether a value looks like a tRPC router instance at runtime. */
123
+ function isRouterInstance(value) {
124
+ if (value == null) return false;
125
+ const obj = value;
126
+ const def = obj["_def"];
127
+ return typeof obj === "object" && def != null && typeof def === "object" && def["record"] != null && typeof def["record"] === "object";
128
+ }
129
+ /**
130
+ * Search a module's exports for a tRPC router instance.
131
+ *
132
+ * Tries (in order):
133
+ * 1. Exact `exportName` match
134
+ * 2. lcfirst variant (`AppRouter` → `appRouter`)
135
+ * 3. First export that looks like a router
136
+ */
137
+ function findRouterExport(mod, exportName) {
138
+ if (isRouterInstance(mod[exportName])) return mod[exportName];
139
+ const lcFirst = exportName.charAt(0).toLowerCase() + exportName.slice(1);
140
+ if (lcFirst !== exportName && isRouterInstance(mod[lcFirst])) return mod[lcFirst];
141
+ for (const value of Object.values(mod)) if (isRouterInstance(value)) return value;
142
+ return null;
143
+ }
144
+ /**
145
+ * Try to dynamically import the router file and extract a tRPC router
146
+ * instance. Returns `null` if the import fails (e.g. no TS loader) or
147
+ * no router export is found.
148
+ */
149
+ async function tryImportRouter(resolvedPath, exportName) {
150
+ try {
151
+ const mod = await import((0, node_url.pathToFileURL)(resolvedPath).href);
152
+ return findRouterExport(mod, exportName);
153
+ } catch (_unused) {
154
+ return null;
155
+ }
156
+ }
157
+ /**
158
+ * Walk a runtime tRPC router/record and collect Zod `.describe()` strings
159
+ * keyed by procedure path.
160
+ */
161
+ function collectRuntimeDescriptions(routerOrRecord, prefix, result) {
162
+ const record = isRouterInstance(routerOrRecord) ? routerOrRecord._def.record : routerOrRecord;
163
+ for (const [key, value] of Object.entries(record)) {
164
+ const fullPath = prefix ? `${prefix}.${key}` : key;
165
+ if (isProcedure(value)) {
166
+ const def = value._def;
167
+ let inputDescs = null;
168
+ for (const input of def.inputs) {
169
+ const descs = extractZodDescriptions(input);
170
+ if (descs) {
171
+ var _inputDescs, _descs$self;
172
+ (_inputDescs = inputDescs) !== null && _inputDescs !== void 0 || (inputDescs = { properties: /* @__PURE__ */ new Map() });
173
+ inputDescs.self = (_descs$self = descs.self) !== null && _descs$self !== void 0 ? _descs$self : inputDescs.self;
174
+ for (const [p, d] of descs.properties) inputDescs.properties.set(p, d);
175
+ }
176
+ }
177
+ let outputDescs = null;
178
+ const outputParser = def["output"];
179
+ if (outputParser) outputDescs = extractZodDescriptions(outputParser);
180
+ if (inputDescs || outputDescs) result.set(fullPath, {
181
+ input: inputDescs,
182
+ output: outputDescs
183
+ });
184
+ } else collectRuntimeDescriptions(value, fullPath, result);
185
+ }
186
+ }
187
+ /** Type guard: check if a RouterRecord value is a procedure (callable). */
188
+ function isProcedure(value) {
189
+ return typeof value === "function";
190
+ }
191
+ /**
192
+ * Overlay description strings from a `DescriptionMap` onto an existing
193
+ * JSON schema produced by the TypeScript type checker. Mutates in place.
194
+ */
195
+ function applyDescriptions(schema, descs) {
196
+ if (descs.self) schema.description = descs.self;
197
+ for (const [propPath, description] of descs.properties) setNestedDescription(schema, propPath.split("."), description);
198
+ }
199
+ function setNestedDescription(schema, pathParts, description) {
200
+ var _schema$properties;
201
+ if (pathParts.length === 0) return;
202
+ const [head, ...rest] = pathParts;
203
+ if (!head) return;
204
+ if (head === "[]") {
205
+ const items = schema.type === "array" && schema.items && typeof schema.items === "object" ? schema.items : null;
206
+ if (!items) return;
207
+ if (rest.length === 0) items.description = description;
208
+ else setNestedDescription(items, rest, description);
209
+ return;
210
+ }
211
+ const propSchema = (_schema$properties = schema.properties) === null || _schema$properties === void 0 ? void 0 : _schema$properties[head];
212
+ if (!propSchema || typeof propSchema !== "object") return;
213
+ if (rest.length === 0) propSchema.description = description;
214
+ else {
215
+ const target = propSchema.type === "array" && propSchema.items && typeof propSchema.items === "object" ? propSchema.items : propSchema;
216
+ setNestedDescription(target, rest, description);
217
+ }
218
+ }
219
+
220
+ //#endregion
221
+ //#region src/generate.ts
222
+ var import_objectSpread2 = require_objectSpread2$1.__toESM(require_objectSpread2$1.require_objectSpread2(), 1);
223
+ const log = console;
224
+ const PRIMITIVE_FLAGS = typescript.TypeFlags.String | typescript.TypeFlags.Number | typescript.TypeFlags.Boolean | typescript.TypeFlags.StringLiteral | typescript.TypeFlags.NumberLiteral | typescript.TypeFlags.BooleanLiteral;
225
+ function hasFlag(type, flag) {
226
+ return (type.getFlags() & flag) !== 0;
227
+ }
228
+ function isPrimitive(type) {
229
+ return hasFlag(type, PRIMITIVE_FLAGS);
230
+ }
231
+ function isObjectType(type) {
232
+ return hasFlag(type, typescript.TypeFlags.Object);
233
+ }
234
+ function isOptionalSymbol(sym) {
235
+ return (sym.flags & typescript.SymbolFlags.Optional) !== 0;
236
+ }
237
+ /**
238
+ * If `type` is a branded intersection (primitive & object), return just the
239
+ * primitive part. Otherwise return the type as-is.
240
+ */
241
+ function unwrapBrand(type) {
242
+ if (!type.isIntersection()) return type;
243
+ const primitives = type.types.filter(isPrimitive);
244
+ const hasObject = type.types.some(isObjectType);
245
+ const [first] = primitives;
246
+ if (first && hasObject) return first;
247
+ return type;
248
+ }
249
+ const ANONYMOUS_NAMES = new Set([
250
+ "__type",
251
+ "__object",
252
+ "Object",
253
+ ""
254
+ ]);
255
+ /** Try to determine a meaningful name for a TS type (type alias or interface). */
256
+ function getTypeName(type) {
257
+ var _type$aliasSymbol, _type$getSymbol;
258
+ const aliasName = (_type$aliasSymbol = type.aliasSymbol) === null || _type$aliasSymbol === void 0 ? void 0 : _type$aliasSymbol.getName();
259
+ if (aliasName && !ANONYMOUS_NAMES.has(aliasName)) return aliasName;
260
+ const symName = (_type$getSymbol = type.getSymbol()) === null || _type$getSymbol === void 0 ? void 0 : _type$getSymbol.getName();
261
+ if (symName && !ANONYMOUS_NAMES.has(symName) && !symName.startsWith("__")) return symName;
262
+ return null;
263
+ }
264
+ function ensureUniqueName(name, existing) {
265
+ if (!(name in existing)) return name;
266
+ let i = 2;
267
+ while (`${name}${i}` in existing) i++;
268
+ return `${name}${i}`;
269
+ }
270
+ function schemaRef(name) {
271
+ return { $ref: `#/components/schemas/${name}` };
272
+ }
273
+ function isNonEmptySchema(s) {
274
+ for (const _ in s) return true;
275
+ return false;
276
+ }
277
+ /**
278
+ * Convert a TS type to a JSON Schema. If the type has been pre-registered
279
+ * (or has a meaningful TS name), it is stored in `ctx.schemas` and a `$ref`
280
+ * is returned instead of an inline schema.
281
+ */
282
+ function typeToJsonSchema(type, ctx, depth = 0) {
283
+ if (depth > 20) {
284
+ log.warn(`[openapi] Schema conversion reached maximum depth (20) for type "${ctx.checker.typeToString(type)}". The resulting schema will be incomplete.`);
285
+ return {};
286
+ }
287
+ const refName = ctx.typeToRef.get(type);
288
+ if (refName) {
289
+ if (refName in ctx.schemas) return schemaRef(refName);
290
+ ctx.schemas[refName] = {};
291
+ const schema$1 = convertTypeToSchema(type, ctx, depth);
292
+ ctx.schemas[refName] = schema$1;
293
+ return schemaRef(refName);
294
+ }
295
+ const schema = convertTypeToSchema(type, ctx, depth);
296
+ if (!schema.description && !schema.$ref && type.aliasSymbol) {
297
+ const aliasJsDoc = getJsDocComment(type.aliasSymbol, ctx.checker);
298
+ if (aliasJsDoc) schema.description = aliasJsDoc;
299
+ }
300
+ return schema;
301
+ }
302
+ /**
303
+ * When we encounter a type we're already visiting, it's recursive.
304
+ * Register it as a named schema and return a $ref.
305
+ */
306
+ function handleCyclicRef(type, ctx) {
307
+ let refName = ctx.typeToRef.get(type);
308
+ if (!refName) {
309
+ var _getTypeName;
310
+ const name = (_getTypeName = getTypeName(type)) !== null && _getTypeName !== void 0 ? _getTypeName : "RecursiveType";
311
+ refName = ensureUniqueName(name, ctx.schemas);
312
+ ctx.typeToRef.set(type, refName);
313
+ ctx.schemas[refName] = {};
314
+ }
315
+ return schemaRef(refName);
316
+ }
317
+ function convertPrimitiveOrLiteral(type, flags, checker) {
318
+ if (flags & typescript.TypeFlags.String) return { type: "string" };
319
+ if (flags & typescript.TypeFlags.Number) return { type: "number" };
320
+ if (flags & typescript.TypeFlags.Boolean) return { type: "boolean" };
321
+ if (flags & typescript.TypeFlags.Null) return { type: "null" };
322
+ if (flags & typescript.TypeFlags.Undefined) return {};
323
+ if (flags & typescript.TypeFlags.Void) return {};
324
+ if (flags & typescript.TypeFlags.Any || flags & typescript.TypeFlags.Unknown) return {};
325
+ if (flags & typescript.TypeFlags.Never) return { not: {} };
326
+ if (flags & typescript.TypeFlags.BigInt || flags & typescript.TypeFlags.BigIntLiteral) return {
327
+ type: "integer",
328
+ format: "bigint"
329
+ };
330
+ if (flags & typescript.TypeFlags.StringLiteral) return {
331
+ type: "string",
332
+ const: type.value
333
+ };
334
+ if (flags & typescript.TypeFlags.NumberLiteral) return {
335
+ type: "number",
336
+ const: type.value
337
+ };
338
+ if (flags & typescript.TypeFlags.BooleanLiteral) {
339
+ const isTrue = checker.typeToString(type) === "true";
340
+ return {
341
+ type: "boolean",
342
+ const: isTrue
343
+ };
344
+ }
345
+ return null;
346
+ }
347
+ function convertUnionType(type, ctx, depth) {
348
+ const members = type.types;
349
+ const defined = members.filter((m) => !hasFlag(m, typescript.TypeFlags.Undefined | typescript.TypeFlags.Void));
350
+ if (defined.length === 0) return {};
351
+ const hasNull = defined.some((m) => hasFlag(m, typescript.TypeFlags.Null));
352
+ const nonNull = defined.filter((m) => !hasFlag(m, typescript.TypeFlags.Null));
353
+ const boolLiterals = nonNull.filter((m) => hasFlag(unwrapBrand(m), typescript.TypeFlags.BooleanLiteral));
354
+ const hasBoolPair = boolLiterals.length === 2 && boolLiterals.some((m) => ctx.checker.typeToString(unwrapBrand(m)) === "true") && boolLiterals.some((m) => ctx.checker.typeToString(unwrapBrand(m)) === "false");
355
+ const effective = hasBoolPair ? nonNull.filter((m) => !hasFlag(unwrapBrand(m), typescript.TypeFlags.BooleanLiteral)) : nonNull;
356
+ if (hasBoolPair && effective.length === 0) return hasNull ? { type: ["boolean", "null"] } : { type: "boolean" };
357
+ const collapsedEnum = tryCollapseLiteralUnion(effective, hasNull);
358
+ if (collapsedEnum) return collapsedEnum;
359
+ const schemas = effective.map((m) => typeToJsonSchema(m, ctx, depth + 1)).filter(isNonEmptySchema);
360
+ if (hasBoolPair) schemas.push({ type: "boolean" });
361
+ if (hasNull) schemas.push({ type: "null" });
362
+ if (schemas.length === 0) return {};
363
+ const [firstSchema] = schemas;
364
+ if (schemas.length === 1 && firstSchema !== void 0) return firstSchema;
365
+ if (schemas.every(isSimpleTypeSchema)) return { type: schemas.map((s) => s.type) };
366
+ const discriminatorProp = detectDiscriminatorProperty(schemas);
367
+ if (discriminatorProp) return {
368
+ oneOf: schemas,
369
+ discriminator: { propertyName: discriminatorProp }
370
+ };
371
+ return { oneOf: schemas };
372
+ }
373
+ /**
374
+ * If every schema in a oneOf is an object with a common required property
375
+ * whose value is a `const`, return that property name. Otherwise return null.
376
+ */
377
+ function detectDiscriminatorProperty(schemas) {
378
+ if (schemas.length < 2) return null;
379
+ if (!schemas.every((s) => s.type === "object" && s.properties)) return null;
380
+ const first = schemas[0];
381
+ if (!(first === null || first === void 0 ? void 0 : first.properties)) return null;
382
+ const firstProps = Object.keys(first.properties);
383
+ for (const prop of firstProps) {
384
+ const allHaveConst = schemas.every((s) => {
385
+ var _s$properties, _s$required;
386
+ const propSchema = (_s$properties = s.properties) === null || _s$properties === void 0 ? void 0 : _s$properties[prop];
387
+ return propSchema !== void 0 && propSchema.const !== void 0 && ((_s$required = s.required) === null || _s$required === void 0 ? void 0 : _s$required.includes(prop));
388
+ });
389
+ if (allHaveConst) return prop;
390
+ }
391
+ return null;
392
+ }
393
+ /** A schema that is just `{ type: "somePrimitive" }` with no other keys. */
394
+ function isSimpleTypeSchema(s) {
395
+ const keys = Object.keys(s);
396
+ return keys.length === 1 && keys[0] === "type" && typeof s.type === "string";
397
+ }
398
+ /**
399
+ * If every non-null member is a string or number literal of the same kind,
400
+ * collapse them into a single `{ type, enum }` schema.
401
+ */
402
+ function tryCollapseLiteralUnion(nonNull, hasNull) {
403
+ if (nonNull.length <= 1) return null;
404
+ const allLiterals = nonNull.every((m) => hasFlag(m, typescript.TypeFlags.StringLiteral | typescript.TypeFlags.NumberLiteral));
405
+ if (!allLiterals) return null;
406
+ const [first] = nonNull;
407
+ if (!first) return null;
408
+ const isString = hasFlag(first, typescript.TypeFlags.StringLiteral);
409
+ const targetFlag = isString ? typescript.TypeFlags.StringLiteral : typescript.TypeFlags.NumberLiteral;
410
+ const allSameKind = nonNull.every((m) => hasFlag(m, targetFlag));
411
+ if (!allSameKind) return null;
412
+ const values = nonNull.map((m) => isString ? m.value : m.value);
413
+ const baseType = isString ? "string" : "number";
414
+ return {
415
+ type: hasNull ? [baseType, "null"] : baseType,
416
+ enum: values
417
+ };
418
+ }
419
+ function convertIntersectionType(type, ctx, depth) {
420
+ const hasPrimitiveMember = type.types.some(isPrimitive);
421
+ const nonBrand = hasPrimitiveMember ? type.types.filter((m) => !isObjectType(m)) : type.types;
422
+ const schemas = nonBrand.map((m) => typeToJsonSchema(m, ctx, depth + 1)).filter(isNonEmptySchema);
423
+ if (schemas.length === 0) return {};
424
+ const [onlySchema] = schemas;
425
+ if (schemas.length === 1 && onlySchema !== void 0) return onlySchema;
426
+ if (schemas.every(isInlineObjectSchema)) return mergeObjectSchemas(schemas);
427
+ return { allOf: schemas };
428
+ }
429
+ /** True when the schema is an inline `{ type: "object", ... }` (not a $ref). */
430
+ function isInlineObjectSchema(s) {
431
+ return s.type === "object" && !s.$ref;
432
+ }
433
+ /**
434
+ * Merge multiple `{ type: "object" }` schemas into one.
435
+ * Falls back to `allOf` if any property names conflict across schemas.
436
+ */
437
+ function mergeObjectSchemas(schemas) {
438
+ const seen = /* @__PURE__ */ new Set();
439
+ for (const s of schemas) if (s.properties) for (const prop of Object.keys(s.properties)) {
440
+ if (seen.has(prop)) return { allOf: schemas };
441
+ seen.add(prop);
442
+ }
443
+ const properties = {};
444
+ const required = [];
445
+ let additionalProperties;
446
+ for (const s of schemas) {
447
+ if (s.properties) Object.assign(properties, s.properties);
448
+ if (s.required) required.push(...s.required);
449
+ if (s.additionalProperties !== void 0) additionalProperties = s.additionalProperties;
450
+ }
451
+ const result = { type: "object" };
452
+ if (Object.keys(properties).length > 0) result.properties = properties;
453
+ if (required.length > 0) result.required = required;
454
+ if (additionalProperties !== void 0) result.additionalProperties = additionalProperties;
455
+ return result;
456
+ }
457
+ function convertWellKnownType(type, ctx, depth) {
458
+ var _type$getSymbol2;
459
+ const symName = (_type$getSymbol2 = type.getSymbol()) === null || _type$getSymbol2 === void 0 ? void 0 : _type$getSymbol2.getName();
460
+ if (symName === "Date") return {
461
+ type: "string",
462
+ format: "date-time"
463
+ };
464
+ if (symName === "Uint8Array" || symName === "Buffer") return {
465
+ type: "string",
466
+ format: "binary"
467
+ };
468
+ if (symName === "Promise") {
469
+ const [inner] = ctx.checker.getTypeArguments(type);
470
+ return inner ? typeToJsonSchema(inner, ctx, depth + 1) : {};
471
+ }
472
+ return null;
473
+ }
474
+ function convertArrayType(type, ctx, depth) {
475
+ const [elem] = ctx.checker.getTypeArguments(type);
476
+ const schema = { type: "array" };
477
+ if (elem) schema.items = typeToJsonSchema(elem, ctx, depth + 1);
478
+ return schema;
479
+ }
480
+ function convertTupleType(type, ctx, depth) {
481
+ const args = ctx.checker.getTypeArguments(type);
482
+ const schemas = args.map((a) => typeToJsonSchema(a, ctx, depth + 1));
483
+ return {
484
+ type: "array",
485
+ prefixItems: schemas,
486
+ items: false,
487
+ minItems: args.length,
488
+ maxItems: args.length
489
+ };
490
+ }
491
+ function convertPlainObject(type, ctx, depth) {
492
+ var _autoRegName;
493
+ const { checker } = ctx;
494
+ const stringIndexType = type.getStringIndexType();
495
+ const typeProps = type.getProperties();
496
+ if (typeProps.length === 0 && stringIndexType) return {
497
+ type: "object",
498
+ additionalProperties: typeToJsonSchema(stringIndexType, ctx, depth + 1)
499
+ };
500
+ let autoRegName = null;
501
+ const tsName = getTypeName(type);
502
+ const isNamedUnregisteredType = tsName !== null && typeProps.length > 0 && !ctx.typeToRef.has(type);
503
+ if (isNamedUnregisteredType) {
504
+ autoRegName = ensureUniqueName(tsName, ctx.schemas);
505
+ ctx.typeToRef.set(type, autoRegName);
506
+ ctx.schemas[autoRegName] = {};
507
+ }
508
+ ctx.visited.add(type);
509
+ const properties = {};
510
+ const required = [];
511
+ for (const prop of typeProps) {
512
+ const propType = checker.getTypeOfSymbol(prop);
513
+ const propSchema = typeToJsonSchema(propType, ctx, depth + 1);
514
+ const jsDoc = getJsDocComment(prop, checker);
515
+ if (jsDoc && !propSchema.description && !propSchema.$ref) propSchema.description = jsDoc;
516
+ properties[prop.name] = propSchema;
517
+ if (!isOptionalSymbol(prop)) required.push(prop.name);
518
+ }
519
+ ctx.visited.delete(type);
520
+ const result = { type: "object" };
521
+ if (Object.keys(properties).length > 0) result.properties = properties;
522
+ if (required.length > 0) result.required = required;
523
+ if (stringIndexType) result.additionalProperties = typeToJsonSchema(stringIndexType, ctx, depth + 1);
524
+ else if (Object.keys(properties).length > 0) result.additionalProperties = false;
525
+ const registeredName = (_autoRegName = autoRegName) !== null && _autoRegName !== void 0 ? _autoRegName : ctx.typeToRef.get(type);
526
+ if (registeredName) {
527
+ ctx.schemas[registeredName] = result;
528
+ return schemaRef(registeredName);
529
+ }
530
+ return result;
531
+ }
532
+ function convertObjectType(type, ctx, depth) {
533
+ const wellKnown = convertWellKnownType(type, ctx, depth);
534
+ if (wellKnown) return wellKnown;
535
+ if (ctx.checker.isArrayType(type)) return convertArrayType(type, ctx, depth);
536
+ if (ctx.checker.isTupleType(type)) return convertTupleType(type, ctx, depth);
537
+ return convertPlainObject(type, ctx, depth);
538
+ }
539
+ /** Core type-to-schema conversion (no ref handling). */
540
+ function convertTypeToSchema(type, ctx, depth) {
541
+ if (ctx.visited.has(type)) return handleCyclicRef(type, ctx);
542
+ const flags = type.getFlags();
543
+ const primitive = convertPrimitiveOrLiteral(type, flags, ctx.checker);
544
+ if (primitive) return primitive;
545
+ if (type.isUnion()) return convertUnionType(type, ctx, depth);
546
+ if (type.isIntersection()) return convertIntersectionType(type, ctx, depth);
547
+ if (isObjectType(type)) return convertObjectType(type, ctx, depth);
548
+ return {};
549
+ }
550
+ /**
551
+ * Inspect `_def.type` and return the procedure type string, or null if this is
552
+ * not a procedure (e.g. a nested router).
553
+ */
554
+ function getProcedureTypeName(defType, checker) {
555
+ const typeSym = defType.getProperty("type");
556
+ if (!typeSym) return null;
557
+ const typeType = checker.getTypeOfSymbol(typeSym);
558
+ const raw = checker.typeToString(typeType).replace(/['"]/g, "");
559
+ if (raw === "query" || raw === "mutation" || raw === "subscription") return raw;
560
+ return null;
561
+ }
562
+ function isVoidLikeInput(inputType) {
563
+ if (!inputType) return true;
564
+ const isVoidOrUndefinedOrNever = hasFlag(inputType, typescript.TypeFlags.Void | typescript.TypeFlags.Undefined | typescript.TypeFlags.Never);
565
+ if (isVoidOrUndefinedOrNever) return true;
566
+ const isUnionOfVoids = inputType.isUnion() && inputType.types.every((t) => hasFlag(t, typescript.TypeFlags.Void | typescript.TypeFlags.Undefined));
567
+ return isUnionOfVoids;
568
+ }
569
+ function extractProcedure(def, ctx) {
570
+ const { schemaCtx } = ctx;
571
+ const { checker } = schemaCtx;
572
+ const $typesSym = def.defType.getProperty("$types");
573
+ if (!$typesSym) return;
574
+ const $typesType = checker.getTypeOfSymbol($typesSym);
575
+ const inputSym = $typesType.getProperty("input");
576
+ const outputSym = $typesType.getProperty("output");
577
+ const inputType = inputSym ? checker.getTypeOfSymbol(inputSym) : null;
578
+ const outputType = outputSym ? checker.getTypeOfSymbol(outputSym) : null;
579
+ const inputSchema = !inputType || isVoidLikeInput(inputType) ? null : typeToJsonSchema(inputType, schemaCtx);
580
+ const outputSchema = outputType ? typeToJsonSchema(outputType, schemaCtx) : null;
581
+ const runtimeDescs = ctx.runtimeDescriptions.get(def.path);
582
+ if (runtimeDescs) {
583
+ if (inputSchema && runtimeDescs.input) applyDescriptions(inputSchema, runtimeDescs.input);
584
+ if (outputSchema && runtimeDescs.output) applyDescriptions(outputSchema, runtimeDescs.output);
585
+ }
586
+ ctx.procedures.push({
587
+ path: def.path,
588
+ type: def.typeName,
589
+ inputSchema,
590
+ outputSchema,
591
+ description: def.description
592
+ });
593
+ }
594
+ /** Extract the JSDoc comment text from a symbol, if any. */
595
+ function getJsDocComment(sym, checker) {
596
+ const parts = sym.getDocumentationComment(checker);
597
+ if (parts.length === 0) return void 0;
598
+ const text = parts.map((p) => p.text).join("");
599
+ return text || void 0;
600
+ }
601
+ function walkType(opts) {
602
+ const { type, ctx, currentPath, description } = opts;
603
+ if (ctx.seen.has(type)) return;
604
+ const defSym = type.getProperty("_def");
605
+ if (!defSym) {
606
+ if (isObjectType(type)) {
607
+ ctx.seen.add(type);
608
+ walkRecord(type, ctx, currentPath);
609
+ ctx.seen.delete(type);
610
+ }
611
+ return;
612
+ }
613
+ const { checker } = ctx.schemaCtx;
614
+ const defType = checker.getTypeOfSymbol(defSym);
615
+ const procedureTypeName = getProcedureTypeName(defType, checker);
616
+ if (procedureTypeName) {
617
+ extractProcedure({
618
+ defType,
619
+ typeName: procedureTypeName,
620
+ path: currentPath,
621
+ description
622
+ }, ctx);
623
+ return;
624
+ }
625
+ const routerSym = defType.getProperty("router");
626
+ if (!routerSym) return;
627
+ const isRouter = checker.typeToString(checker.getTypeOfSymbol(routerSym)) === "true";
628
+ if (!isRouter) return;
629
+ const recordSym = defType.getProperty("record");
630
+ if (!recordSym) return;
631
+ ctx.seen.add(type);
632
+ const recordType = checker.getTypeOfSymbol(recordSym);
633
+ walkRecord(recordType, ctx, currentPath);
634
+ ctx.seen.delete(type);
635
+ }
636
+ function walkRecord(recordType, ctx, prefix) {
637
+ for (const prop of recordType.getProperties()) {
638
+ const propType = ctx.schemaCtx.checker.getTypeOfSymbol(prop);
639
+ const fullPath = prefix ? `${prefix}.${prop.name}` : prop.name;
640
+ const description = getJsDocComment(prop, ctx.schemaCtx.checker);
641
+ walkType({
642
+ type: propType,
643
+ ctx,
644
+ currentPath: fullPath,
645
+ description
646
+ });
647
+ }
648
+ }
649
+ function loadCompilerOptions(startDir) {
650
+ const configPath = typescript.findConfigFile(startDir, (f) => typescript.sys.fileExists(f), "tsconfig.json");
651
+ if (!configPath) return {
652
+ target: typescript.ScriptTarget.ES2020,
653
+ moduleResolution: typescript.ModuleResolutionKind.Bundler,
654
+ skipLibCheck: true,
655
+ noEmit: true
656
+ };
657
+ const configFile = typescript.readConfigFile(configPath, (f) => typescript.sys.readFile(f));
658
+ const parsed = typescript.parseJsonConfigFileContent(configFile.config, typescript.sys, node_path.dirname(configPath));
659
+ const options = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, parsed.options), {}, { noEmit: true });
660
+ if (options.moduleResolution === void 0) {
661
+ const mod = options.module;
662
+ if (mod === typescript.ModuleKind.Node16 || mod === typescript.ModuleKind.NodeNext) options.moduleResolution = typescript.ModuleResolutionKind.NodeNext;
663
+ else if (mod === typescript.ModuleKind.Preserve || mod === typescript.ModuleKind.ES2022 || mod === typescript.ModuleKind.ESNext) options.moduleResolution = typescript.ModuleResolutionKind.Bundler;
664
+ else options.moduleResolution = typescript.ModuleResolutionKind.Node10;
665
+ }
666
+ return options;
667
+ }
668
+ /**
669
+ * Walk `_def._config.$types.errorShape` on the router type and convert
670
+ * it to a JSON Schema. Returns `null` when the path cannot be resolved
671
+ * (e.g. older tRPC versions or missing type info).
672
+ */
673
+ function extractErrorSchema(routerType, checker, schemaCtx) {
674
+ const walk = (type, keys) => {
675
+ const [head, ...rest] = keys;
676
+ if (!head) return type;
677
+ const sym = type.getProperty(head);
678
+ if (!sym) return null;
679
+ return walk(checker.getTypeOfSymbol(sym), rest);
680
+ };
681
+ const errorShapeType = walk(routerType, [
682
+ "_def",
683
+ "_config",
684
+ "$types",
685
+ "errorShape"
686
+ ]);
687
+ if (!errorShapeType) return null;
688
+ if (hasFlag(errorShapeType, typescript.TypeFlags.Any)) return null;
689
+ return typeToJsonSchema(errorShapeType, schemaCtx);
690
+ }
691
+ /** Fallback error schema when the router type doesn't expose an error shape. */
692
+ const DEFAULT_ERROR_SCHEMA = {
693
+ type: "object",
694
+ properties: {
695
+ message: { type: "string" },
696
+ code: { type: "string" },
697
+ data: { type: "object" }
698
+ },
699
+ required: ["message", "code"]
700
+ };
701
+ /**
702
+ * Wrap a procedure's output schema in the tRPC success envelope.
703
+ *
704
+ * tRPC HTTP responses are always serialised as:
705
+ * `{ result: { data: T } }`
706
+ *
707
+ * When the procedure has no output the envelope is still present but
708
+ * the `data` property is omitted.
709
+ */
710
+ function wrapInSuccessEnvelope(outputSchema) {
711
+ const hasOutput = outputSchema !== null && isNonEmptySchema(outputSchema);
712
+ const resultSchema = (0, import_objectSpread2.default)({
713
+ type: "object",
714
+ properties: (0, import_objectSpread2.default)({}, hasOutput ? { data: outputSchema } : {})
715
+ }, hasOutput ? { required: ["data"] } : {});
716
+ return {
717
+ type: "object",
718
+ properties: { result: resultSchema },
719
+ required: ["result"]
720
+ };
721
+ }
722
+ function buildProcedureOperation(proc, method) {
723
+ const operation = (0, import_objectSpread2.default)((0, import_objectSpread2.default)({ operationId: proc.path }, proc.description ? { description: proc.description } : {}), {}, {
724
+ tags: [proc.path.split(".")[0]],
725
+ responses: {
726
+ "200": {
727
+ description: "Successful response",
728
+ content: { "application/json": { schema: wrapInSuccessEnvelope(proc.outputSchema) } }
729
+ },
730
+ default: { $ref: "#/components/responses/Error" }
731
+ }
732
+ });
733
+ if (proc.inputSchema === null) return operation;
734
+ if (method === "get") operation["parameters"] = [{
735
+ name: "input",
736
+ in: "query",
737
+ required: true,
738
+ style: "deepObject",
739
+ content: { "application/json": { schema: proc.inputSchema } }
740
+ }];
741
+ else operation["requestBody"] = {
742
+ required: true,
743
+ content: { "application/json": { schema: proc.inputSchema } }
744
+ };
745
+ return operation;
746
+ }
747
+ function buildOpenAPIDocument(procedures, options, meta = { errorSchema: null }) {
748
+ var _options$title, _options$version, _meta$errorSchema;
749
+ const paths = {};
750
+ for (const proc of procedures) {
751
+ var _paths$opPath;
752
+ if (proc.type === "subscription") continue;
753
+ const opPath = `/${proc.path}`;
754
+ const method = proc.type === "query" ? "get" : "post";
755
+ const pathItem = (_paths$opPath = paths[opPath]) !== null && _paths$opPath !== void 0 ? _paths$opPath : {};
756
+ paths[opPath] = pathItem;
757
+ pathItem[method] = buildProcedureOperation(proc, method);
758
+ }
759
+ const hasNamedSchemas = meta.schemas !== void 0 && Object.keys(meta.schemas).length > 0;
760
+ return {
761
+ openapi: "3.1.1",
762
+ jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base",
763
+ info: {
764
+ title: (_options$title = options.title) !== null && _options$title !== void 0 ? _options$title : "tRPC API",
765
+ version: (_options$version = options.version) !== null && _options$version !== void 0 ? _options$version : "0.0.0"
766
+ },
767
+ paths,
768
+ components: (0, import_objectSpread2.default)((0, import_objectSpread2.default)({}, hasNamedSchemas ? { schemas: meta.schemas } : {}), {}, { responses: { Error: {
769
+ description: "Error response",
770
+ content: { "application/json": { schema: {
771
+ type: "object",
772
+ properties: { error: (_meta$errorSchema = meta.errorSchema) !== null && _meta$errorSchema !== void 0 ? _meta$errorSchema : DEFAULT_ERROR_SCHEMA },
773
+ required: ["error"]
774
+ } } }
775
+ } } })
776
+ };
777
+ }
778
+ /**
779
+ * Analyse the given TypeScript router file using the TypeScript compiler and
780
+ * return an OpenAPI 3.1 document describing all query and mutation procedures.
781
+ *
782
+ * @param routerFilePath - Absolute or relative path to the file that exports
783
+ * the AppRouter.
784
+ * @param options - Optional generation settings (export name, title, version).
785
+ */
786
+ async function generateOpenAPIDocument(routerFilePath, options = {}) {
787
+ var _options$exportName;
788
+ const resolvedPath = node_path.resolve(routerFilePath);
789
+ const exportName = (_options$exportName = options.exportName) !== null && _options$exportName !== void 0 ? _options$exportName : "AppRouter";
790
+ const compilerOptions = loadCompilerOptions(node_path.dirname(resolvedPath));
791
+ const program = typescript.createProgram([resolvedPath], compilerOptions);
792
+ const checker = program.getTypeChecker();
793
+ const sourceFile = program.getSourceFile(resolvedPath);
794
+ if (!sourceFile) throw new Error(`Could not load TypeScript file: ${resolvedPath}`);
795
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
796
+ if (!moduleSymbol) throw new Error(`No module exports found in: ${resolvedPath}`);
797
+ const tsExports = checker.getExportsOfModule(moduleSymbol);
798
+ const routerSymbol = tsExports.find((sym) => sym.getName() === exportName);
799
+ if (!routerSymbol) {
800
+ const available = tsExports.map((e) => e.getName()).join(", ");
801
+ throw new Error(`No export named '${exportName}' found in: ${resolvedPath}\nAvailable exports: ${available || "(none)"}`);
802
+ }
803
+ let routerType;
804
+ if (routerSymbol.valueDeclaration) routerType = checker.getTypeOfSymbolAtLocation(routerSymbol, routerSymbol.valueDeclaration);
805
+ else routerType = checker.getDeclaredTypeOfSymbol(routerSymbol);
806
+ const schemaCtx = {
807
+ checker,
808
+ visited: /* @__PURE__ */ new Set(),
809
+ schemas: {},
810
+ typeToRef: /* @__PURE__ */ new Map()
811
+ };
812
+ const runtimeDescriptions = /* @__PURE__ */ new Map();
813
+ const router = await tryImportRouter(resolvedPath, exportName);
814
+ if (router) collectRuntimeDescriptions(router, "", runtimeDescriptions);
815
+ const walkCtx = {
816
+ procedures: [],
817
+ seen: /* @__PURE__ */ new Set(),
818
+ schemaCtx,
819
+ runtimeDescriptions
820
+ };
821
+ walkType({
822
+ type: routerType,
823
+ ctx: walkCtx,
824
+ currentPath: ""
825
+ });
826
+ const errorSchema = extractErrorSchema(routerType, checker, schemaCtx);
827
+ return buildOpenAPIDocument(walkCtx.procedures, options, {
828
+ errorSchema,
829
+ schemas: schemaCtx.schemas
830
+ });
831
+ }
832
+
833
+ //#endregion
834
+ exports.generateOpenAPIDocument = generateOpenAPIDocument;