UNPKG

@kubb/plugin-ts

Version:

TypeScript code generation plugin for Kubb, transforming OpenAPI schemas into TypeScript interfaces, types, and utility functions.

227 lines (193 loc) 9.52 kB
import { jsStringEscape, stringify } from '@internals/utils' import type { ArraySchemaNode, SchemaNode } from '@kubb/ast/types' import type { PrinterFactoryOptions } from '@kubb/core' import { definePrinter } from '@kubb/core' import type ts from 'typescript' import * as factory from './factory.ts' type TsOptions = { /** * @default `'questionToken'` */ optionalType: 'questionToken' | 'undefined' | 'questionTokenAndUndefined' /** * @default `'array'` */ arrayType: 'array' | 'generic' /** * @default `'inlineLiteral'` */ enumType: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral' /** * @default `'Key'` */ enumTypeSuffix?: string } type TsPrinter = PrinterFactoryOptions<'typescript', TsOptions, ts.TypeNode> function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined { if (format === 'boolean') { return factory.createLiteralTypeNode(value === true ? factory.createTrue() : factory.createFalse()) } if (format === 'number' && typeof value === 'number') { if (value < 0) { return factory.createLiteralTypeNode(factory.createPrefixUnaryExpression(factory.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)))) } return factory.createLiteralTypeNode(factory.createNumericLiteral(value)) } return factory.createLiteralTypeNode(factory.createStringLiteral(String(value))) } function dateOrStringNode(node: { representation?: string }): ts.TypeNode { return node.representation === 'date' ? factory.createTypeReferenceNode(factory.createIdentifier('Date')) : factory.keywordTypeNodes.string } function buildMemberNodes(members: Array<SchemaNode> | undefined, print: (node: SchemaNode) => ts.TypeNode | null | undefined): Array<ts.TypeNode> { return (members ?? []).map(print).filter(Boolean) as Array<ts.TypeNode> } function buildTupleNode(node: ArraySchemaNode, print: (node: SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined { let items = (node.items ?? []).map(print).filter(Boolean) as Array<ts.TypeNode> const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined const { min, max } = node if (max !== undefined) { items = items.slice(0, max) if (items.length < max && restNode) { items = [...items, ...Array(max - items.length).fill(restNode)] } } if (min !== undefined) { items = items.map((item, i) => (i >= min ? factory.createOptionalTypeNode(item) : item)) } if (max === undefined && restNode) { items.push(factory.createRestTypeNode(factory.createArrayTypeNode(restNode))) } return factory.createTupleTypeNode(items) } function buildPropertyType(schema: SchemaNode, baseType: ts.TypeNode, optionalType: TsOptions['optionalType']): ts.TypeNode { const addsUndefined = ['undefined', 'questionTokenAndUndefined'].includes(optionalType) let type = baseType if (schema.nullable) { type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.null] }) as ts.TypeNode } if ((schema.nullish || schema.optional) && addsUndefined) { type = factory.createUnionDeclaration({ nodes: [type, factory.keywordTypeNodes.undefined] }) as ts.TypeNode } return type } function buildPropertyJSDocComments(schema: SchemaNode): Array<string | undefined> { return [ 'description' in schema && schema.description ? `@description ${jsStringEscape(schema.description as string)}` : undefined, 'deprecated' in schema && schema.deprecated ? '@deprecated' : undefined, 'min' in schema && schema.min !== undefined ? `@minLength ${schema.min}` : undefined, 'max' in schema && schema.max !== undefined ? `@maxLength ${schema.max}` : undefined, 'pattern' in schema && schema.pattern ? `@pattern ${schema.pattern}` : undefined, 'default' in schema && schema.default !== undefined ? `@default ${'primitive' in schema && schema.primitive === 'string' ? stringify(schema.default as string) : schema.default}` : undefined, 'example' in schema && schema.example !== undefined ? `@example ${schema.example}` : undefined, 'primitive' in schema && schema.primitive ? [`@type ${schema.primitive || 'unknown'}`, 'optional' in schema && schema.optional ? ' | undefined' : undefined].filter(Boolean).join('') : undefined, ] } function buildIndexSignatures( node: { additionalProperties?: SchemaNode | boolean; patternProperties?: Record<string, SchemaNode> }, propertyCount: number, print: (node: SchemaNode) => ts.TypeNode | null | undefined, ): Array<ts.TypeElement> { const elements: Array<ts.TypeElement> = [] if (node.additionalProperties && node.additionalProperties !== true) { const additionalType = (print(node.additionalProperties as SchemaNode) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode elements.push(factory.createIndexSignature(propertyCount > 0 ? factory.keywordTypeNodes.unknown : additionalType)) } else if (node.additionalProperties === true) { elements.push(factory.createIndexSignature(factory.keywordTypeNodes.unknown)) } if (node.patternProperties) { const first = Object.values(node.patternProperties)[0] if (first) { let patternType = (print(first) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode if (first.nullable) { patternType = factory.createUnionDeclaration({ nodes: [patternType, factory.keywordTypeNodes.null] }) as ts.TypeNode } elements.push(factory.createIndexSignature(patternType)) } } return elements } /** * Converts a `SchemaNode` AST node into a TypeScript `ts.TypeNode`. * * Built on `definePrinter` — dispatches on `node.type`, with options closed over * per printer instance. Produces the same `ts.TypeNode` output as the keyword-based * `parse` in `parser.ts`. */ export const printerTs = definePrinter<TsPrinter>((options) => ({ name: 'typescript', options, nodes: { any: () => factory.keywordTypeNodes.any, unknown: () => factory.keywordTypeNodes.unknown, void: () => factory.keywordTypeNodes.void, boolean: () => factory.keywordTypeNodes.boolean, null: () => factory.keywordTypeNodes.null, blob: () => factory.createTypeReferenceNode('Blob', []), string: () => factory.keywordTypeNodes.string, uuid: () => factory.keywordTypeNodes.string, email: () => factory.keywordTypeNodes.string, url: () => factory.keywordTypeNodes.string, datetime: () => factory.keywordTypeNodes.string, number: () => factory.keywordTypeNodes.number, integer: () => factory.keywordTypeNodes.number, bigint: () => factory.keywordTypeNodes.bigint, date: (node) => dateOrStringNode(node), time: (node) => dateOrStringNode(node), ref(node) { if (!node.name) { return undefined } return factory.createTypeReferenceNode(node.name, undefined) }, enum(node) { const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? [] if (this.options.enumType === 'inlineLiteral' || !node.name) { const literalNodes = values .filter((v): v is string | number | boolean => v !== null) .map((value) => constToTypeNode(value, typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string')) .filter(Boolean) as Array<ts.TypeNode> return factory.createUnionDeclaration({ withParentheses: true, nodes: literalNodes }) ?? undefined } const enumTypeSuffix = this.options.enumTypeSuffix ?? 'Key' const typeName = ['asConst', 'asPascalConst'].includes(this.options.enumType) ? `${node.name}${enumTypeSuffix}` : node.name return factory.createTypeReferenceNode(typeName, undefined) }, union(node) { return factory.createUnionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.print) }) ?? undefined }, intersection(node) { return factory.createIntersectionDeclaration({ withParentheses: true, nodes: buildMemberNodes(node.members, this.print) }) ?? undefined }, array(node) { const itemNodes = (node.items ?? []).map((item) => this.print(item)).filter(Boolean) as Array<ts.TypeNode> return factory.createArrayDeclaration({ nodes: itemNodes, arrayType: this.options.arrayType }) ?? undefined }, tuple(node) { return buildTupleNode(node, this.print) }, object(node) { const addsQuestionToken = ['questionToken', 'questionTokenAndUndefined'].includes(this.options.optionalType) const { print } = this const propertyNodes: Array<ts.TypeElement> = node.properties.map((prop) => { const baseType = (print(prop.schema) ?? factory.keywordTypeNodes.unknown) as ts.TypeNode const type = buildPropertyType(prop.schema, baseType, this.options.optionalType) const propertyNode = factory.createPropertySignature({ questionToken: prop.schema.optional || prop.schema.nullish ? addsQuestionToken : false, name: prop.name, type, readOnly: prop.schema.readOnly, }) return factory.appendJSDocToNode({ node: propertyNode, comments: buildPropertyJSDocComments(prop.schema) }) }) const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, print)] if (!allElements.length) { return factory.keywordTypeNodes.object } return factory.createTypeLiteralNode(allElements) }, }, }))