diff --git a/src/declaration-generator.ts b/src/declaration-generator.ts new file mode 100644 index 0000000..ab493c0 --- /dev/null +++ b/src/declaration-generator.ts @@ -0,0 +1,152 @@ +import { + TypeNode, + PrimitiveType, + ArrayType, + ObjectType, + UnionType, + LiteralType, + OptionalType, + TypeDefinition, +} from './types.js'; + +export class DeclarationGenerator { + private indent: string; + private newLine: string; + + constructor(indentSize: number = 2) { + this.indent = ' '.repeat(indentSize); + this.newLine = '\n'; + } + + generate(types: TypeDefinition[]): string { + if (types.length === 0) { + return ''; + } + + const declarations = types.map((t) => this.generateTypeDeclaration(t)); + + return declarations.join(this.newLine + this.newLine); + } + + generateTypeDeclaration(typeDef: TypeDefinition): string { + const declaration = this.generateTypeNode(typeDef.type, 0, new Set()); + + if (typeDef.type.kind === 'object') { + return `export interface ${typeDef.name} ${this.generateObjectBody(typeDef.type, 0)}`; + } + + return `export type ${typeDef.name} = ${declaration};`; + } + + generateTypeNode(type: TypeNode, depth: number, _visited: Set): string { + switch (type.kind) { + case 'primitive': + return this.formatPrimitiveType(type); + case 'literal': + return this.formatLiteralType(type); + case 'array': + return this.formatArrayType(type, depth); + case 'object': + return this.formatObjectType(type, depth); + case 'union': + return this.formatUnionType(type, depth); + case 'optional': + return this.formatOptionalType(type, depth); + default: + return 'unknown'; + } + } + + private formatPrimitiveType(type: PrimitiveType): string { + switch (type.type) { + case 'string': + return 'string'; + case 'number': + return 'number'; + case 'boolean': + return 'boolean'; + case 'null': + return 'null'; + case 'any': + return 'any'; + case 'unknown': + return 'unknown'; + default: + return 'unknown'; + } + } + + private formatLiteralType(type: LiteralType): string { + if (typeof type.value === 'string') { + return `'${type.value}'`; + } + return String(type.value); + } + + private formatArrayType(type: ArrayType, depth: number): string { + const elementType = this.generateTypeNode(type.elementType, depth, new Set()); + + if (this.needsParens(type.elementType)) { + return `(${elementType})[]`; + } + return `${elementType}[]`; + } + + private formatObjectType(type: ObjectType, depth: number): string { + return this.generateObjectBody(type, depth); + } + + private generateObjectBody(type: ObjectType, depth: number): string { + if (type.properties.length === 0) { + return '{}'; + } + + const currentIndent = this.indent.repeat(depth); + const innerIndent = this.indent.repeat(depth + 1); + + const propertyLines = type.properties.map((prop) => { + const optionalMarker = prop.optional ? '?' : ''; + const propType = this.generateTypeNode(prop.type, depth + 1, new Set()); + return `${innerIndent}${prop.name}${optionalMarker}: ${propType};`; + }); + + return `{${this.newLine}${propertyLines.join(this.newLine)}${this.newLine}${currentIndent}}`; + } + + private formatUnionType(type: UnionType, depth: number): string { + if (type.types.length === 0) { + return 'never'; + } + + if (type.types.length === 1) { + return this.generateTypeNode(type.types[0], depth, new Set()); + } + + const formattedTypes = type.types.map((t) => { + const inner = this.generateTypeNode(t, depth, new Set()); + return this.needsParens(t) ? `(${inner})` : inner; + }); + + const separator = this.newLine + this.indent.repeat(depth) + '| '; + + if (depth === 0) { + return formattedTypes.join(' | '); + } + + return formattedTypes.join(separator); + } + + private formatOptionalType(type: OptionalType, depth: number): string { + const inner = this.generateTypeNode(type.type, depth, new Set()); + return `${inner} | undefined`; + } + + private needsParens(type: TypeNode): boolean { + return type.kind === 'union' || type.kind === 'optional'; + } + + generateStandalone(types: TypeDefinition[]): string { + const declarations = types.map((t) => this.generateTypeDeclaration(t)); + return declarations.join(this.newLine + this.newLine); + } +}