Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
chore: wip

chore: wip

chore: wip

chore: wip
  • Loading branch information
chrisbbreuer committed Oct 23, 2024
1 parent 1e863e2 commit 3464760
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 41 deletions.
35 changes: 28 additions & 7 deletions fixtures/output/example-0001.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { generate } from '@stacksjs/dtsx';
* Example of const declaration
*/
export declare const conf: { [key: string]: string };

export declare const someObject: {
someString: 'Stacks';
someNumber: 1000;
Expand Down Expand Up @@ -35,6 +36,7 @@ export declare const someObject: {
someInlineCall2: (...args: any[]) => void;
someInlineCall3: (...args: any[]) => void;
};

/**
* Example of interface declaration
* with another comment in an extra line
Expand All @@ -44,6 +46,7 @@ export declare interface User {
name: string;
email: string;
}

/**
* Example of type declaration
*
Expand All @@ -53,55 +56,73 @@ export declare interface ResponseData {
success: boolean;
data: User[];
}

/**
* Example of function declaration
*
*
* with multiple empty lines, including an empty lines
*/
export declare function fetchUsers(): Promise<ResponseData>;

export declare interface ApiResponse<T> {
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<ApiResponse<Product>>;

export declare interface AuthResponse {
token: string;
expiresIn: number;
}

export declare type AuthStatus = 'authenticated' | 'unauthenticated';

export declare function authenticate(user: string, password: string): Promise<AuthResponse>;

export declare const defaultHeaders: {
'Content-Type': 'application/json';
};

export declare function dts(options?: DtsGenerationOption): BunPlugin;

declare interface Options<T> {
name: string;
cwd?: string;
defaultConfig: T;
}
export declare async function loadConfig<T extends Record<string, unknown>>(options: Options<T>): Promise<T>;

export declare function loadConfig<T extends Record<string, unknown>>(options: Options<T>): Promise<T>;

declare const dtsConfig: DtsGenerationConfig;

export { generate, dtsConfig }
export declare type { DtsGenerationOption }
export { config } from './config'
export * from './extract'
export * from './generate'
export * from './types'
export * from './utils'

export type { DtsGenerationOption };

export { config } from './config';

export * from './extract';
export * from './generate';
export * from './types';
export * from './utils';

export default dts;
221 changes: 187 additions & 34 deletions src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,41 +315,55 @@ export function processImports(imports: string[], usedTypes: Set<string>): strin
export function processDeclaration(declaration: string, state: ProcessingState): string {
const trimmed = declaration.trim()

if (trimmed.startsWith('export const'))
// Handle different declaration types with proper formatting
if (trimmed.startsWith('export const')) {
return processConstDeclaration(trimmed)
}

if (trimmed.startsWith('const'))
if (trimmed.startsWith('const')) {
return processConstDeclaration(trimmed, false)
}

if (trimmed.startsWith('export interface'))
if (trimmed.startsWith('export interface')) {
return processInterfaceDeclaration(trimmed)
}

if (trimmed.startsWith('interface'))
if (trimmed.startsWith('interface')) {
return processInterfaceDeclaration(trimmed, false)
}

if (trimmed.startsWith('export type {'))
return processTypeOnlyExport(trimmed, state)

if (trimmed.startsWith('type {'))
return processTypeOnlyExport(trimmed, state, false)
if (trimmed.startsWith('export type {')) {
// Handle type-only exports without 'declare'
return trimmed
}

if (trimmed.startsWith('export type'))
if (trimmed.startsWith('export type')) {
return processTypeDeclaration(trimmed)
}

if (trimmed.startsWith('type'))
if (trimmed.startsWith('type')) {
return processTypeDeclaration(trimmed, false)
}

if (trimmed.startsWith('export function') || trimmed.startsWith('export async function'))
return processFunctionDeclaration(trimmed, state.usedTypes)
if (trimmed.startsWith('export function') || trimmed.startsWith('export async function')) {
// Remove async from ambient context
const processed = trimmed.replace(/\basync\s+/, '')
return processFunctionDeclaration(processed, state.usedTypes)
}

if (trimmed.startsWith('function') || trimmed.startsWith('async function'))
return processFunctionDeclaration(trimmed, state.usedTypes, false)
if (trimmed.startsWith('function') || trimmed.startsWith('async function')) {
// Remove async from ambient context
const processed = trimmed.replace(/\basync\s+/, '')
return processFunctionDeclaration(processed, state.usedTypes, false)
}

if (trimmed.startsWith('export default'))
if (trimmed.startsWith('export default')) {
return `${trimmed};`
}

if (trimmed.startsWith('export'))
if (trimmed.startsWith('export')) {
return trimmed
}

return `declare ${trimmed}`
}
Expand Down Expand Up @@ -1116,7 +1130,7 @@ export function isDeclarationLine(line: string): boolean {
export function processDeclarationLine(line: string, state: ProcessingState): void {
state.currentDeclaration += `${line}\n`

// Count brackets to track multi-line declarations
// Track brackets for multi-line declarations
const bracketMatch = line.match(/[[{(]/g)
const closeBracketMatch = line.match(/[\]})]/g)
const openCount = bracketMatch ? bracketMatch.length : 0
Expand All @@ -1130,9 +1144,13 @@ export function processDeclarationLine(line: string, state: ProcessingState): vo
state.dtsLines.push(state.lastCommentBlock.trimEnd())
state.lastCommentBlock = ''
}

// Process and format the declaration
const processed = processDeclaration(state.currentDeclaration.trim(), state)
if (processed)
if (processed) {
state.dtsLines.push(processed)
}

state.currentDeclaration = ''
state.bracketCount = 0
}
Expand All @@ -1156,38 +1174,173 @@ export function formatOutput(state: ProcessingState): string {
// Generate optimized imports
const imports = generateImports(state)

// Build the output sections
const sections = [
// Imports section (if any imports exist)
imports.length > 0 ? imports.join('\n') : null,
// Process declarations with proper grouping and spacing
const { regularDeclarations, starExports } = categorizeDeclarations(state.dtsLines)

// Main declarations
state.dtsLines
.filter(line => line.trim())
.join('\n'),
]
// Build sections with careful spacing
const sections: string[] = []

// Combine sections with proper spacing
// Add imports with proper spacing after
if (imports.length > 0) {
sections.push(`${imports.join('\n')}\n`)
}

// Add regular declarations with proper spacing between them
if (regularDeclarations.length > 0) {
sections.push(regularDeclarations.join('\n\n'))
}

// Add export * declarations grouped together
if (starExports.length > 0) {
sections.push(starExports.join('\n'))
}

// Combine sections
let result = sections
.filter(Boolean)
.join('\n\n')
.trim()

// Add final newline and handle default export
// Handle default export
if (state.defaultExport) {
const exportIdentifier = state.defaultExport
.replace(/^export\s+default\s+/, '')
.replace(/export\s+default\s+/, '')
.replace(/;+$/, '')
.trim()

result += `\nexport default ${exportIdentifier};\n`
// Ensure blank line before default export if there's content before it
result = result.replace(/\n*$/, '\n\n')
result += `export default ${exportIdentifier};`
}
else {
result += '\n'

// Ensure final newline
result += '\n'

return fixDtsOutput(result)
}

/**
* Categorize declarations into different types
*/
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 a single declaration with proper spacing and fixes
*/
function formatSingleDeclaration(declaration: string): string {
if (!declaration.trim())
return ''

let formatted = declaration

// Fix 'export declare type' statements
if (formatted.includes('export declare type {')) {
formatted = formatted.replace('export declare type', 'export type')
}

return result
// Remove async from ambient declarations
if (formatted.includes('declare') && formatted.includes('async')) {
formatted = formatted
.replace(/declare\s+async\s+/, 'declare ')
.replace(/export\s+declare\s+async\s+/, 'export declare ')
}

// Only add semicolon if it's needed and not after an opening brace
if (!formatted.endsWith(';') && !formatted.endsWith('{') && shouldAddSemicolon(formatted)) {
formatted = `${formatted.trimEnd()};`
}

return formatted
}

/**
* Determine if a semicolon should be added to the declaration
*/
function shouldAddSemicolon(declaration: string): boolean {
const trimmed = declaration.trim()

// Skip comments and formatting-only lines
if (trimmed.startsWith('/*') || trimmed.startsWith('*') || trimmed.startsWith('//')) {
return false
}

// Skip interface/type declarations ending with opening or closing braces
if (trimmed.endsWith('{') || trimmed.endsWith('}')) {
return false
}

// Skip declarations that already have semicolons
if (trimmed.endsWith(';')) {
return false
}

return true
}

/**
* Ensure declaration ends with semicolon
*/
function ensureSemicolon(declaration: string): string {
return declaration.trim()
.replace(/;+$/, '') // Remove any existing semicolons first
.replace(/\{\s*$/, '{') // Remove any spaces after opening brace
+ (declaration.trim().endsWith('{') ? '' : ';') // Add semicolon only if not ending with brace
}

/**
* Apply final fixes to the complete DTS output
*/
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;')
// Ensure proper spacing for export * declarations (without duplicate semicolons)
.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')
}

/**
Expand Down

0 comments on commit 3464760

Please sign in to comment.