From 3a19f8be509effa8a5673cd84eabee138c4fbab8 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 24 Oct 2024 13:29:16 +0200 Subject: [PATCH] chore: wip --- fixtures/output/example-0001.d.ts | 54 +++---- src/extract.ts | 224 +++++++++--------------------- 2 files changed, 84 insertions(+), 194 deletions(-) diff --git a/fixtures/output/example-0001.d.ts b/fixtures/output/example-0001.d.ts index a13bc0a..a8ae5d9 100644 --- a/fixtures/output/example-0001.d.ts +++ b/fixtures/output/example-0001.d.ts @@ -1,8 +1,14 @@ +import type { BunPlugin } from 'bun' +import process from 'node:process' +import { generate, deepMerge } from '@stacksjs/dtsx' +import type { DtsGenerationConfig, DtsGenerationOption } from '@stacksjs/dtsx' +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' + /** * Example of const declaration */ export declare const conf: { [key: string]: string }; - export declare const someObject: { someString: 'Stacks'; someNumber: 1000; @@ -32,7 +38,6 @@ export declare const someObject: { someInlineCall2: (...args: any[]) => void; someInlineCall3: (...args: any[]) => void; }; - /** * Example of interface declaration * with another comment in an extra line @@ -42,7 +47,6 @@ export declare interface User { name: string email: string } - /** * Example of type declaration * @@ -52,7 +56,6 @@ export declare interface ResponseData { success: boolean data: User[] } - /** * Example of function declaration * @@ -60,62 +63,50 @@ export declare interface ResponseData { * with multiple empty lines, including an empty lines */ export declare function fetchUsers(): Promise; - export declare interface ApiResponse { status: number message: string data: T } - /** * Example of another const declaration * * with multiple empty lines, including being poorly formatted */ declare const settings: { [key: string]: any }; - export declare interface Product { id: number name: string price: number } - /** * Example of function declaration */ export declare function getProduct(id: number): Promise>; - export declare interface AuthResponse { token: string expiresIn: number } - export declare type AuthStatus = 'authenticated' | 'unauthenticated'; - export declare function authenticate(user: string, password: string): Promise; - export declare const defaultHeaders: { 'Content-Type': 'application/json'; }; - export declare function dts(options?: DtsGenerationOption): BunPlugin; - declare interface Options { name: string cwd?: string defaultConfig: T } - -export declare function loadConfig>(options: Options): Promise; - +export declare async function loadConfig>(options: Options): Promise; declare const dtsConfig: DtsGenerationConfig; - export { generate, dtsConfig } - -export type { DtsGenerationOption }; - -export { config } from './config'; - +export type { DtsGenerationOption } +export { config } from './config' +export * from './extract' +export * from './generate' +export * from './types' +export * from './utils' export declare interface ComplexGeneric, K extends keyof T> { data: T key: K @@ -123,20 +114,11 @@ export declare interface ComplexGeneric, K ext transform: (input: T[K]) => string nested: Array> } - -export declare type ComplexUnionIntersection =; - - | (User & { role: 'admin' }); - - | (Product & { category: string }); - +export declare type ComplexUnionIntersection = + | (User & { role: 'admin' }) + | (Product & { category: string }) & { metadata: Record } -export default dts; - -export * from './extract'; -export * from './generate'; -export * from './types'; -export * from './utils'; +export default dts diff --git a/src/extract.ts b/src/extract.ts index b1a8f14..cbb1f61 100644 --- a/src/extract.ts +++ b/src/extract.ts @@ -1232,20 +1232,39 @@ function processTypeDeclaration(declaration: string, isExported = true): string function processSourceFile(content: string, state: ProcessingState): void { const lines = content.split('\n') - - // First pass: collect imports - const imports = lines.filter(line => line.trim().startsWith('import')).join('\n') - if (imports) { - state.imports = processImports(imports.split('\n'), state.usedTypes) - } - - // Second pass: process everything else + const importLines: string[] = [] + const exportDefaultLines: string[] = [] let currentBlock: string[] = [] let currentComments: string[] = [] let isInMultilineDeclaration = false + // First pass: collect imports and export defaults for (const line of lines) { const trimmedLine = line.trim() + if (trimmedLine.startsWith('import')) { + importLines.push(line) + continue + } + if (trimmedLine.startsWith('export default')) { + exportDefaultLines.push(line) + continue + } + } + + // Process imports + if (importLines.length > 0) { + state.dtsLines.push(...importLines) + state.dtsLines.push('') // Add single line break after imports + } + + // Process rest of the content + for (const line of lines) { + const trimmedLine = line.trim() + + // Skip imports and export defaults as they're handled separately + if (trimmedLine.startsWith('import') || trimmedLine.startsWith('export default')) { + continue + } // Skip empty lines between declarations if (!trimmedLine && !isInMultilineDeclaration) { @@ -1301,6 +1320,14 @@ function processSourceFile(content: string, state: ProcessingState): void { if (currentBlock.length > 0) { processDeclarationBlock(currentBlock, currentComments, state) } + + // Add export default at the end with proper spacing + if (exportDefaultLines.length > 0) { + if (state.dtsLines[state.dtsLines.length - 1] !== '') { + state.dtsLines.push('') // Add line break before export default + } + state.dtsLines.push(...exportDefaultLines) + } } /** @@ -1420,6 +1447,28 @@ export function cleanParameters(params: string): string { .trim() } +function cleanImports(imports: string[]): string[] { + const seen = new Set() + return imports + .filter((imp) => { + const normalized = imp.trim() + if (seen.has(normalized)) + return false + seen.add(normalized) + return true + }) + .sort((a, b) => { + // Sort type imports before regular imports + const aIsType = a.includes('import type') + const bIsType = b.includes('import type') + if (aIsType && !bIsType) + return -1 + if (!aIsType && bIsType) + return 1 + return a.localeCompare(b) + }) +} + /** * Normalize type references */ @@ -1634,103 +1683,17 @@ function getDeclarationType(line: string): 'interface' | 'type' | 'const' | 'fun /** * Format the final output with proper spacing and organization */ -export function formatOutput(state: ProcessingState): string { - const imports = generateImports(state) - const { regularDeclarations, starExports } = categorizeDeclarations(state.dtsLines) - const sections: string[] = [] - - if (imports.length > 0) { - sections.push(`${imports.join('\n')}\n`) - } - - if (regularDeclarations.length > 0) { - sections.push(regularDeclarations.join('\n\n')) - } - - if (starExports.length > 0) { - sections.push(starExports.join('\n')) - } - - let result = sections - .filter(Boolean) - .join('\n\n') - .trim() - - if (state.defaultExport) { - const exportIdentifier = state.defaultExport - .replace(REGEX.exportCleanup, '') - .replace(REGEX.defaultExport, '') - .replace(/;+$/, '') - .trim() - - result = result.replace(/\n*$/, '\n\n') - result += `export default ${exportIdentifier};` - } - - result += '\n' +function formatOutput(state: ProcessingState): string { + let output = state.dtsLines.join('\n') - return fixDtsOutput(result) -} - -/** - * Categorize declarations by type - */ -function categorizeDeclarations(declarations: string[]): { - regularDeclarations: string[] - starExports: string[] -} { - const regularDeclarations: string[] = [] - const starExports: string[] = [] - let currentComment = '' - - declarations.forEach((declaration) => { - const trimmed = declaration.trim() - - if (trimmed.startsWith('/**') || trimmed.startsWith('*')) { - currentComment = currentComment ? `${currentComment}\n${declaration}` : declaration - return - } - - if (trimmed.startsWith('export *')) { - starExports.push(ensureSemicolon(trimmed)) - } - else if (trimmed) { - const formattedDeclaration = formatSingleDeclaration( - currentComment ? `${currentComment}\n${declaration}` : declaration, - ) - regularDeclarations.push(formattedDeclaration) - } - - currentComment = '' - }) - - return { regularDeclarations, starExports } -} - -/** - * Format individual declarations - */ -function formatSingleDeclaration(declaration: string): string { - if (!declaration.trim()) - return '' - - let formatted = declaration - - if (formatted.includes('export declare type {')) { - formatted = formatted.replace('export declare type', 'export type') - } - - if (formatted.includes('declare') && formatted.includes('async')) { - formatted = formatted - .replace(/declare\s+async\s+/, 'declare ') - .replace(/export\s+declare\s+async\s+/, 'export declare ') - } - - if (!formatted.endsWith(';') && !formatted.endsWith('{') && shouldAddSemicolon(formatted)) { - formatted = `${formatted.trimEnd()};` - } + // Ensure proper formatting + output = `${output + // Remove multiple consecutive empty lines + .replace(/\n{3,}/g, '\n\n') + // Ensure file ends with single newline + .trim()}\n` - return formatted + return output } function getIndentation(line: string): string { @@ -1738,27 +1701,6 @@ function getIndentation(line: string): string { return match ? match[1] : '' } -/** - * Check if semicolon should be added - */ -function shouldAddSemicolon(declaration: string): boolean { - const trimmed = declaration.trim() - - if (trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('//')) { - return false - } - - if (trimmed.endsWith('{') || trimmed.endsWith('}')) { - return false - } - - if (trimmed.endsWith(';')) { - return false - } - - return true -} - function needsExport(line: string): boolean { const trimmed = line.trim() return ( @@ -1768,37 +1710,3 @@ function needsExport(line: string): boolean { || trimmed.startsWith('export interface ') ) } - -/** - * Ensure proper semicolon placement - */ -function ensureSemicolon(declaration: string): string { - return declaration.trim() - .replace(/;+$/, '') - .replace(/\{\s*$/, '{') - + (declaration.trim().endsWith('{') ? '' : ';') -} - -/** - * Final output formatting and cleanup - */ -function fixDtsOutput(content: string): string { - return content - // First ensure all line endings are consistent - .replace(/\r\n/g, '\n') - // Remove semicolons after opening braces - .replace(/\{\s*;/g, '{') - // Fix any duplicate semicolons - .replace(/;+/g, ';') - // Normalize empty lines (no more than 2 consecutive newlines) - .replace(/\n{3,}/g, '\n\n') - // Add semicolons to declarations if missing (but not after opening braces) - .replace(/^(export (?!.*\{$)[^*{}\n].*[^;\n])$/gm, '$1;') - .replace(/^(export \* from [^;\n]+);*$/gm, '$1;') - // Fix export statements with duplicated semicolons - .replace(/^(export \{[^}]+\} from [^;\n]+);*$/gm, '$1;') - // Remove any trailing whitespace - .replace(/[ \t]+$/gm, '') - // Ensure single newline at the end - .replace(/\n*$/, '\n') -}