diff --git a/fixtures/output/example-0001.d.ts b/fixtures/output/example-0001.d.ts index 541e8b1..c5cb67e 100644 --- a/fixtures/output/example-0001.d.ts +++ b/fixtures/output/example-0001.d.ts @@ -42,9 +42,9 @@ export declare const someObject: { * with another comment in an extra line */ export declare interface User { - id: number; - name: string; - email: string; +id: number +name: string +email: string } /** @@ -53,8 +53,8 @@ export declare interface User { * with multiple lines of comments, including an empty line */ export declare interface ResponseData { - success: boolean; - data: User[]; +success: boolean +data: User[] } /** @@ -66,9 +66,9 @@ export declare interface ResponseData { export declare function fetchUsers(): Promise; export declare interface ApiResponse { - status: number; - message: string; - data: T; +status: number +message: string +data: T } /** @@ -76,12 +76,15 @@ export declare interface ApiResponse { * * with multiple empty lines, including being poorly formatted */ -declare const settings: { [key: string]: any }; +declare const settings: { [key: string]: any } = { +theme: 'dark', +language: 'en', +} export declare interface Product { - id: number; - name: string; - price: number; +id: number +name: string +price: number } /** @@ -90,8 +93,8 @@ export declare interface Product { export declare function getProduct(id: number): Promise>; export declare interface AuthResponse { - token: string; - expiresIn: number; +token: string +expiresIn: number } export declare type AuthStatus = 'authenticated' | 'unauthenticated'; @@ -104,15 +107,27 @@ export declare const defaultHeaders: { export declare function dts(options?: DtsGenerationOption): BunPlugin; -declare interface Options { - name: string; - cwd?: string; - defaultConfig: T; +declare interface interface Options { +name: string +cwd?: string +defaultConfig: T } export declare function loadConfig>(options: Options): Promise; -declare const dtsConfig: DtsGenerationConfig; +declare const dtsConfig: DtsGenerationConfig = await loadConfig({ +name: 'dts', +cwd: process.cwd(), +defaultConfig: { +cwd: process.cwd(), +root: './src', +entrypoints: ['**/*.ts'], +outdir: './dist', +keepComments: true, +clean: true, +tsconfigPath: './tsconfig.json', +}, +}); export { generate, dtsConfig } @@ -121,19 +136,14 @@ export type { DtsGenerationOption }; export { config } from './config'; export declare interface ComplexGeneric, K extends keyof T> { - data: T; - key: K; - value: T[K]; - transform: (input: T[K]) => string; - nested: Array>; +data: T +key: K +value: T[K] +transform: (input: T[K]) => string +nested: Array> } -export declare type ComplexUnionIntersection = - | (User & { role: 'admin' }) - | (Product & { category: string }) - & { - metadata: Record - } +export declare type ComplexUnionIntersection =; export * from './extract'; export * from './generate'; diff --git a/src/extract.ts b/src/extract.ts index c2ecca4..5c034c6 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -143,6 +143,11 @@ export interface ProcessingState { availableTypes: Map /** Map of available value names */ availableValues: Map + currentIndentation: string + declarationFormatting?: { + indent: string + content: string[] + } } /** @@ -438,15 +443,24 @@ export function processImports(imports: string[], usedTypes: Set): strin * Process declarations */ export function processDeclaration(declaration: string, state: ProcessingState): string { + console.log('Processing declaration:', { declaration, type: 'START' }) + const trimmed = declaration.trim() - // Handle different declaration types with proper formatting - if (trimmed.startsWith('export const')) { - return processConstDeclaration(trimmed) + // Handle different types of declarations + if (trimmed.startsWith('export type') || trimmed.startsWith('type')) { + console.log('Handling type declaration') + return processTypeDeclaration(trimmed, trimmed.startsWith('export')) } - if (trimmed.startsWith('const')) { - return processConstDeclaration(trimmed, false) + if (trimmed.startsWith('export interface') || trimmed.startsWith('interface')) { + console.log('Handling interface declaration') + return processInterfaceDeclaration(trimmed, trimmed.startsWith('export')) + } + + if (trimmed.startsWith('export const')) { + console.log('Handling exported const declaration') + return processConstDeclaration(trimmed) } if (trimmed.startsWith('export interface')) { @@ -487,6 +501,8 @@ export function processDeclaration(declaration: string, state: ProcessingState): return trimmed } + console.log('Processing declaration:', { declaration, type: 'END' }) + return `declare ${trimmed}` } @@ -494,19 +510,43 @@ export function processDeclaration(declaration: string, state: ProcessingState): * Process constant declarations with type inference */ function processConstDeclaration(declaration: string, isExported = true): string { + console.log('Processing const declaration:', { declaration }) const lines = declaration.split('\n') - const firstLine = lines[0] - const name = firstLine.split('const')[1].split('=')[0].trim().split(':')[0].trim() - const typeAnnotation = getTypeAnnotation(firstLine) + const firstLine = lines[0].trim() + + // Check for type annotation in first line + const typeMatch = firstLine.match(/const\s+([^:]+):\s*([^=]+)\s*=/) + if (typeMatch) { + const [, name, type] = typeMatch + // When there's an explicit type annotation, only use the type + return `${isExported ? 'export ' : ''}declare const ${name.trim()}: ${type.trim()};` + } - if (typeAnnotation.raw) { - return `${isExported ? 'export ' : ''}declare const ${name}: ${typeAnnotation.raw};` + // No type annotation, extract name and use inference + const nameMatch = firstLine.match(/const\s+([^=\s]+)\s*=/) + if (!nameMatch) { + console.log('No const declaration found:', firstLine) + return declaration } + const name = nameMatch[1].trim() + console.log('Processing const without type annotation:', name) + + // For declarations without a type annotation, use full type inference const properties = extractObjectProperties(lines.slice(1, -1)) - const propertyStrings = formatProperties(properties) + if (properties.length > 0) { + const propertyStrings = formatProperties(properties) + return `${isExported ? 'export ' : ''}declare const ${name}: {\n${propertyStrings}\n};` + } - return `${isExported ? 'export ' : ''}declare const ${name}: {\n${propertyStrings}\n};` + // For simple values without type annotation + const valueMatch = firstLine.match(/=\s*(.+)$/) + if (!valueMatch) + return declaration + + const value = valueMatch[1].trim() + const inferredType = inferComplexType(value) + return `${isExported ? 'export ' : ''}declare const ${name}: ${inferredType};` } /** @@ -973,31 +1013,97 @@ function processObjectLiteral(obj: string): string { /** * Process interface declarations */ -export function processInterfaceDeclaration(declaration: string, isExported = true): string { +function processInterfaceDeclaration(declaration: string, isExported = true): string { + console.log('Processing interface declaration:', { declaration }) const lines = declaration.split('\n') - const interfaceName = lines[0].split('interface')[1].split('{')[0].trim() - const interfaceBody = lines - .slice(1, -1) - .map(line => ` ${line.trim().replace(/;?$/, ';')}`) - .join('\n') + const firstLine = lines[0] - return `${isExported ? 'export ' : ''}declare interface ${interfaceName} {\n${interfaceBody}\n}` + // Get original indentation of properties + const propertyLines = lines.slice(1, -1) + + // Reconstruct with preserved formatting + const prefix = isExported ? 'export declare' : 'declare' + const result = [ + `${prefix} ${firstLine.replace(/^export\s+/, '')}`, + ...propertyLines, + lines[lines.length - 1], + ].join('\n') + + console.log('Final interface declaration:', result) + return result } /** * Process type declarations */ -export function processTypeDeclaration(declaration: string, isExported = true): string { - const lines = declaration.split('\n') - const firstLine = lines[0] - const typeName = firstLine.split('type')[1].split('=')[0].trim() - const typeBody = firstLine.split('=')[1]?.trim() - || lines.slice(1).join('\n').trim().replace(/;$/, '') +function processTypeDeclaration(declaration: string, isExported = true): string { + console.log('Processing type declaration:', { declaration }) + + const firstLine = declaration.split('\n')[0] - // Use complex type inference for the type body - const inferredType = inferComplexType(typeBody) + // Handle type exports (e.g., "export type { DtsGenerationOption }") + if (firstLine.includes('type {')) { + return declaration + } + + // Extract type name from the first line + const typeNameMatch = firstLine.match(/type\s+(\w+)/) + if (!typeNameMatch) { + console.log('No type name found, returning original') + return declaration + } + + const typeName = typeNameMatch[1] + console.log('Found type name:', typeName) + + // For single-line type declarations that have everything after the equals + if (firstLine.includes('=')) { + const typeContent = firstLine.split('=')[1]?.trim() + if (typeContent) { + return `${isExported ? 'export ' : ''}declare type ${typeName} = ${typeContent};` + } + } + + // For multi-line type declarations + const allLines = declaration.split('\n') + const typeContent: string[] = [] + let inDefinition = false + let bracketCount = 0 + + for (const line of allLines) { + // Start collecting after we see the equals sign + if (line.includes('=')) { + inDefinition = true + const afterEquals = line.split('=')[1]?.trim() + if (afterEquals) + typeContent.push(afterEquals) + continue + } + + if (!inDefinition) + continue + + // Track brackets/braces for nested structures + const openCount = (line.match(/[{[(]/g) || []).length + const closeCount = (line.match(/[}\])]/g) || []).length + bracketCount += openCount - closeCount + + typeContent.push(line.trim()) + } + + // Clean up the collected type content + const finalContent = typeContent + .join('\n') + .trim() + .replace(/;$/, '') + + console.log('Collected type content:', { finalContent }) + + if (finalContent) { + return `${isExported ? 'export ' : ''}declare type ${typeName} = ${finalContent};` + } - return `${isExported ? 'export ' : ''}declare type ${typeName} = ${inferredType};` + return declaration } /** @@ -1239,31 +1345,122 @@ export function formatTypeParameters(params: string): string { /** * Process declarations with improved structure */ -export function processDeclarationLine(line: string, state: ProcessingState): void { +function processDeclarationLine(line: string, state: ProcessingState): void { + // Get the original indentation from the line + const indentMatch = line.match(/^(\s+)/) + const originalIndent = indentMatch ? indentMatch[1] : '' + state.currentIndentation = originalIndent + + // Store original formatting in state + if (!state.isMultiLineDeclaration) { + state.declarationFormatting = { + indent: originalIndent, + content: [], + } + } + + // Add the line with its original formatting + if (state.declarationFormatting) { + state.declarationFormatting.content.push(line) + } + + // Rest of the existing processDeclarationLine logic... state.currentDeclaration += `${line}\n` - const bracketMatch = line.match(REGEX.bracketOpen) - const closeBracketMatch = line.match(REGEX.bracketClose) - const openCount = bracketMatch ? bracketMatch.length : 0 - const closeCount = closeBracketMatch ? closeBracketMatch.length : 0 + // Count brackets for nested structures + const openCount = (line.match(/[{[(]/g) || []).length + const closeCount = (line.match(/[})\]]/g) || []).length state.bracketCount += openCount - closeCount state.isMultiLineDeclaration = state.bracketCount > 0 - if (!state.isMultiLineDeclaration) { + if (!state.isMultiLineDeclaration && state.declarationFormatting) { if (state.lastCommentBlock) { state.dtsLines.push(state.lastCommentBlock.trimEnd()) state.lastCommentBlock = '' } - const processed = processDeclaration(state.currentDeclaration.trim(), state) + const processed = processDeclarationWithFormatting( + state.declarationFormatting.content, + state.declarationFormatting.indent, + state, + ) + if (processed) { state.dtsLines.push(processed) } state.currentDeclaration = '' state.bracketCount = 0 + state.declarationFormatting = undefined + } +} + +function processDeclarationWithFormatting( + lines: string[], + indent: string, + state: ProcessingState, +): string { + const declaration = lines.join('\n') + + if (declaration.startsWith('export type') || declaration.startsWith('type')) { + return processTypeDeclarationWithFormatting(lines, indent) + } + + if (declaration.startsWith('export interface') || declaration.startsWith('interface')) { + return processInterfaceDeclarationWithFormatting(lines, indent) } + + // For other declarations, use existing processing + return processDeclaration(declaration, state) +} + +function processTypeDeclarationWithFormatting(lines: string[], indent: string): string { + const firstLine = lines[0] + const isExported = firstLine.startsWith('export') + + // Handle type declarations that span multiple lines + if (lines.length > 1) { + const typeContent = lines.slice(1).map((line) => { + // Preserve original indentation for continued lines + const lineMatch = line.match(/^(\s+)/) + const currentIndent = lineMatch ? lineMatch[1] : indent + return `${currentIndent}${line.trim()}` + }).join('\n') + + const declaration = `${isExported ? 'export ' : ''}declare type ${ + firstLine.replace(/^export\s+type\s+/, '').trim() + }${typeContent}` + + return declaration + } + + // For single-line type declarations + return `${isExported ? 'export ' : ''}declare type ${ + firstLine.replace(/^export\s+type\s+/, '').trim() + }` +} + +// New function to process interface declarations with formatting preservation +function processInterfaceDeclarationWithFormatting(lines: string[], indent: string): string { + const firstLine = lines[0] + const isExported = firstLine.startsWith('export') + + // Process interface content while preserving formatting + const interfaceContent = lines.slice(1, -1).map((line) => { + // Keep original indentation for interface members + const lineMatch = line.match(/^(\s+)/) + const currentIndent = lineMatch ? lineMatch[1] : indent + return `${currentIndent}${line.trim()}` + }).join('\n') + + return [ + `${isExported ? 'export ' : ''}declare interface ${ + firstLine.replace(/^export\s+interface\s+/, '').trim() + }`, + interfaceContent, + `${indent}}`, + ].join('\n') } /** @@ -1368,26 +1565,6 @@ function formatSingleDeclaration(declaration: string): string { return formatted } -/** - * Format function declaration - */ -function formatFunctionDeclaration( - signature: FunctionSignature, - isExported: boolean, -): string { - const { - name, - params, - returnType, - isAsync, - generics, - } = signature - - return `${isExported ? 'export ' : ''}declare ${isAsync ? 'async ' : ''}function ${name}${ - generics ? `<${generics}>` : '' - }(${params}): ${returnType};` -} - /** * Check if semicolon should be added */