Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
chore: wip
  • Loading branch information
chrisbbreuer committed Oct 23, 2024
1 parent 38c8c80 commit 1cea1fe
Showing 1 changed file with 141 additions and 116 deletions.
257 changes: 141 additions & 116 deletions src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ export function extractDtsTypes(sourceCode: string): string {

const lines = sourceCode.split('\n')
for (const line of lines) {
// Add explicitly re-exported types to usedTypes
if (line.trim().startsWith('export type {')) {
const typeMatch = line.match(/export\s+type\s*\{([^}]+)\}/)
if (typeMatch) {
const types = typeMatch[1].split(',').map(t => t.trim())
types.forEach(type => state.usedTypes.add(type))
}
}
processLine(line, state)
}

Expand Down Expand Up @@ -159,8 +167,9 @@ export function processImport(importLine: string, typeSources: Map<string, strin
*/
export function processImports(imports: string[], usedTypes: Set<string>): string[] {
const importMap = new Map<string, Set<string>>()
const reExportedTypes = new Set<string>()

// Process each import line to extract module and types
// First pass: process each import line
for (const line of imports) {
const typeImportMatch = line.match(REGEX.typeImport)
const regularImportMatch = line.match(REGEX.regularImport)
Expand All @@ -170,38 +179,48 @@ export function processImports(imports: string[], usedTypes: Set<string>): strin
const types = match[1].split(',').map(t => t.trim())
const module = match[2]

// Only track types that are actually used
const usedImports = types.filter((type) => {
// Track all imported types, including those that might be re-exported
types.forEach((type) => {
const baseName = type.split(' as ')[0].trim()
return usedTypes.has(baseName)
})

if (usedImports.length > 0) {
if (!importMap.has(module)) {
if (!importMap.has(module))
importMap.set(module, new Set())
}
usedImports.forEach(type => importMap.get(module)!.add(type))
}

importMap.get(module)!.add(type)

// If this is a type that's re-exported, mark it
if (usedTypes.has(baseName))
reExportedTypes.add(baseName)
})
}
}

// These are the only modules we want to keep imports from
const allowedModules = ['bun', '@stacksjs/dtsx']

// Format imports, filtering to allowed modules only
// Now include both used types and re-exported types
return Array.from(importMap.entries())
.filter(([module]) => allowedModules.includes(module))
.map(([module, types]) => {
const sortedTypes = Array.from(types).sort()
const relevantTypes = Array.from(types).filter((type) => {
const baseName = type.split(' as ')[0].trim()
return usedTypes.has(baseName) || reExportedTypes.has(baseName)
})

if (relevantTypes.length === 0)
return ''

const sortedTypes = relevantTypes.sort()
return `import type { ${sortedTypes.join(', ')} } from '${module}';`
})
.sort() // Sort imports alphabetically
.filter(Boolean)
.sort()
}

/**
* Process declarations (const, interface, type, function)
*/
export function processDeclaration(declaration: string, usedTypes: Set<string>): string {
export function processDeclaration(declaration: string, state: ProcessingState): string {
const trimmed = declaration.trim()

if (trimmed.startsWith('export const'))
Expand All @@ -217,10 +236,10 @@ export function processDeclaration(declaration: string, usedTypes: Set<string>):
return processInterfaceDeclaration(trimmed, false)

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

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

if (trimmed.startsWith('export type'))
return processTypeDeclaration(trimmed)
Expand All @@ -229,10 +248,10 @@ export function processDeclaration(declaration: string, usedTypes: Set<string>):
return processTypeDeclaration(trimmed, false)

if (trimmed.startsWith('export function') || trimmed.startsWith('export async function'))
return processFunctionDeclaration(trimmed, usedTypes)
return processFunctionDeclaration(trimmed, state.usedTypes)

if (trimmed.startsWith('function') || trimmed.startsWith('async function'))
return processFunctionDeclaration(trimmed, usedTypes, false)
return processFunctionDeclaration(trimmed, state.usedTypes, false)

if (trimmed.startsWith('export default'))
return `${trimmed};`
Expand Down Expand Up @@ -739,7 +758,14 @@ export function processInterfaceDeclaration(declaration: string, isExported = tr
/**
* Process type-only exports
*/
export function processTypeOnlyExport(declaration: string, isExported = true): string {
export function processTypeOnlyExport(declaration: string, state: ProcessingState, isExported = true): string {
// When processing "export type { X }", add X to usedTypes
const typeMatch = declaration.match(/export\s+type\s*\{([^}]+)\}/)
if (typeMatch) {
const types = typeMatch[1].split(',').map(t => t.trim())
types.forEach(type => state.usedTypes.add(type))
}

return declaration
.replace('export type', `${isExported ? 'export ' : ''}declare type`)
.replace(/;$/, '')
Expand All @@ -760,59 +786,39 @@ export function processTypeDeclaration(declaration: string, isExported = true):
/**
* Extract complete function signature handling multi-line declarations
*/
export function extractFunctionSignature(declaration: string): string {
// First, normalize line breaks and whitespace
const normalized = declaration
.split('\n')
.map(line => line.trim())
.join(' ')

// Match function declaration including parameters and return type
const match = normalized.match(/^(export\s+)?(async\s+)?function\s+([^{]+)/)
if (!match)
return ''

let signature = match[0]
let depth = 0
let genericDepth = 0
let foundBody = false
export function extractFunctionSignature(declaration: string): {
name: string
params: string
returnType: string
isAsync: boolean
generics: string
} {
const isAsync = declaration.includes('async')
const cleanDeclaration = declaration
.replace('export ', '')
.replace('async ', '')
.replace('function ', '')
.trim()

// Process character by character to find the complete signature
for (let i = match[0].length; i < normalized.length && !foundBody; i++) {
const char = normalized[i]
const nameMatch = cleanDeclaration.match(/^([^<(\s]+)/)
const name = nameMatch ? nameMatch[1] : ''

if (char === '<') {
genericDepth++
}
else if (char === '>') {
genericDepth--
}
else if (genericDepth === 0) {
if (char === '(') {
depth++
}
else if (char === ')') {
depth--
}
else if (char === '{') {
foundBody = true
break
}
}
const genericsMatch = cleanDeclaration.match(/<([^>]+)>/)
const generics = genericsMatch ? `<${genericsMatch[1]}>` : ''

if (!foundBody) {
signature += char
}
}
const paramsMatch = cleanDeclaration.match(/\((.*?)\)/)
const params = paramsMatch ? paramsMatch[1] : ''

// Clean up the signature
signature = signature
.replace(/\s+/g, ' ')
.replace(/:\s+/g, ': ')
.trim()
const returnTypeMatch = cleanDeclaration.match(/\):\s*([^{;]+)/)
const returnType = returnTypeMatch ? returnTypeMatch[1].trim() : 'void'

console.log('Extracted raw signature:', signature)
return signature
return {
name,
params,
returnType,
isAsync,
generics,
}
}

/**
Expand All @@ -823,61 +829,76 @@ export function processFunctionDeclaration(
usedTypes: Set<string>,
isExported = true,
): string {
console.log('Processing declaration:', declaration)

const signature = extractFunctionSignature(declaration)
if (!signature) {
console.log('No valid signature found')
return declaration
}
const functionSignature = declaration.split('{')[0].trim()
const asyncKeyword = functionSignature.includes('async') ? 'async ' : ''

console.log('Using signature:', signature)
const parseResult = parseFunctionDeclaration(signature)
console.log('Parse result:', parseResult)
// Extract function name and generic parameters
const nameAndGenerics = functionSignature
.replace('export ', '')
.replace('async ', '')
.replace('function ', '')
.split('(')[0]
.trim()

// Add types to usedTypes set
const addTypeToUsed = (type: string) => {
if (!type)
return
// Handle generic type parameters
const genericMatch = nameAndGenerics.match(/<([^>]+)>/)?.[1]
const functionName = nameAndGenerics.split('<')[0].trim()
const genericParams = genericMatch ? `<${genericMatch}>` : ''

const typeMatches = type.match(/([A-Z_]\w*)/gi) || []
typeMatches.forEach((t) => {
if (!t.match(/^(void|any|number|string|boolean|null|undefined|never|unknown|Promise)$/)) {
usedTypes.add(t)
console.log('Added type to used:', t)
}
// Extract parameters
const paramsMatch = functionSignature.match(/\((.*?)\)/)?.[1] || ''

// Get return type
const returnTypeMatch = functionSignature.match(/\):\s*([^{;]+)/)?.[1]?.trim()
const returnType = returnTypeMatch || 'void'

// Add used types
if (genericMatch) {
genericMatch.split(',').forEach((type) => {
const cleanType = type.split('extends')[0].trim()
if (cleanType)
usedTypes.add(cleanType)
})
}

// Process all types
addTypeToUsed(parseResult.returnType)
addTypeToUsed(parseResult.parameters)

if (parseResult.genericParams) {
const genericContent = parseResult.genericParams.slice(1, -1)
genericContent.split(',').forEach((param) => {
const [, constraint] = param.split(' extends ')
if (constraint) {
addTypeToUsed(constraint)
console.log('Added generic constraint:', constraint)
}
})
if (returnType && returnType !== 'void') {
// Add base type and any generic parameters to usedTypes
const baseType = returnType.split('<')[0].trim()
usedTypes.add(baseType)

// Extract types from generic parameters if present
const returnGenericMatch = returnType.match(/<([^>]+)>/)?.[1]
if (returnGenericMatch) {
returnGenericMatch.split(',').forEach((type) => {
const cleanType = type.trim().split('<')[0].trim()
if (cleanType)
usedTypes.add(cleanType)
})
}
}

// Construct the declaration, ensuring proper spacing and no duplicate colons
return [
// Build the function declaration string
const functionDeclaration = [
isExported ? 'export ' : '',
'declare ',
parseResult.isAsync ? 'async ' : '',
asyncKeyword,
'function ',
parseResult.functionName,
parseResult.genericParams,
functionName,
genericParams,
'(',
parseResult.parameters,
paramsMatch,
'): ',
parseResult.returnType,
returnType,
';',
].join('')

return functionDeclaration
.replace(/\s+/g, ' ')
.replace(/\s*([<>(),;])\s*/g, '$1')
.replace(/,([^,\s])/g, ', $1')
.replace(/>\s*\(/g, '>(')
.replace(/\(\s*\)/g, '()')
.replace(/function\s+function/, 'function')
}

// Helper functions for line processing
Expand Down Expand Up @@ -907,17 +928,22 @@ export function isDeclarationLine(line: string): boolean {

export function processDeclarationLine(line: string, state: ProcessingState): void {
state.currentDeclaration += `${line}\n`
const opens = (line.match(REGEX.bracketOpen) || []).length
const closes = (line.match(REGEX.bracketClose) || []).length
state.bracketCount += opens - closes

// Count brackets to track multi-line declarations
const bracketMatch = line.match(/[[{(]/g)
const closeBracketMatch = line.match(/[\]})]/g)
const openCount = bracketMatch ? bracketMatch.length : 0
const closeCount = closeBracketMatch ? closeBracketMatch.length : 0
state.bracketCount += openCount - closeCount

state.isMultiLineDeclaration = state.bracketCount > 0

if (!state.isMultiLineDeclaration) {
if (state.lastCommentBlock) {
state.dtsLines.push(state.lastCommentBlock.trimEnd())
state.lastCommentBlock = ''
}
const processed = processDeclaration(state.currentDeclaration.trim(), state.usedTypes)
const processed = processDeclaration(state.currentDeclaration.trim(), state)
if (processed)
state.dtsLines.push(processed)
state.currentDeclaration = ''
Expand Down Expand Up @@ -956,15 +982,14 @@ export function formatOutput(state: ProcessingState): string {
line.startsWith('*') ? ` ${line}` : line,
)

// Ensure double newline after imports
const importSection = allImports.length > 0 ? [...allImports, '', ''] : []

let result = [
...importSection,
...allImports,
'',
'', // Extra newline after imports
...declarations,
].filter(Boolean).join('\n')

// Clean up default export if present
// Clean up default export
if (state.defaultExport) {
const exportIdentifier = state.defaultExport
.replace(/^export\s+default\s+/, '')
Expand Down

0 comments on commit 1cea1fe

Please sign in to comment.