Skip to content

Commit

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

chore: wip
  • Loading branch information
chrisbbreuer committed Oct 17, 2024
1 parent d3a42cf commit 8598729
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 96 deletions.
74 changes: 43 additions & 31 deletions src/extract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from 'node:fs/promises'
import { formatComment } from './utils'
import { formatComment, formatDeclarations } from './utils'

export async function extractTypeFromSource(filePath: string): Promise<string> {
const fileContent = await readFile(filePath, 'utf-8')
Expand All @@ -12,13 +12,16 @@ export async function extractTypeFromSource(filePath: string): Promise<string> {
let importMatch
while ((importMatch = importRegex.exec(fileContent)) !== null) {
const [, isTypeImport, namedImports1, defaultImport1, namedImports2, defaultImport2, from] = importMatch
if (!importMap.has(from)) {
importMap.set(from, new Set())
}

const processImports = (imports: string | undefined, isType: boolean) => {
if (imports) {
const types = imports.replace(/[{}]/g, '').split(',').map(t => {
const [name, alias] = t.split(' as ').map(s => s.trim())
return { name: name.replace(/^type\s+/, ''), alias: alias || name.replace(/^type\s+/, '') }
})
if (!importMap.has(from)) importMap.set(from, new Set())
types.forEach(({ name, alias }) => {
importMap.get(from)!.add(name)
})
Expand All @@ -32,11 +35,11 @@ export async function extractTypeFromSource(filePath: string): Promise<string> {
}

// Handle exports with comments
const exportRegex = /(\/\*\*[\s\S]*?\*\/)?\s*(export\s+(?:async\s+)?(?:function|const|let|var|class|interface|type)\s+\w+[\s\S]*?)(?=\n\s*(?:\/\*\*|export|$))/g;
const exportRegex = /(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:async\s+)?(?:function|const|let|var|class|interface|type)\s+\w+[\s\S]*?)(?=\n\s*(?:\/\*\*|export|$))/g;
let match
while ((match = exportRegex.exec(fileContent)) !== null) {
const [, comment, exportStatement] = match
const formattedComment = comment ? formatComment(comment) : ''
const formattedComment = comment ? formatComment(comment.trim()) : ''
let formattedExport = exportStatement.trim()

if (formattedExport.startsWith('export function') || formattedExport.startsWith('export async function')) {
Expand All @@ -51,17 +54,9 @@ export async function extractTypeFromSource(filePath: string): Promise<string> {
} else if (formattedExport.startsWith('export const') || formattedExport.startsWith('export let') || formattedExport.startsWith('export var')) {
formattedExport = formattedExport.replace(/^export\s+(const|let|var)/, 'export declare $1')
formattedExport = formattedExport.split('=')[0].trim() + ';'
} else if (formattedExport.startsWith('export interface')) {
// Keep interface declarations as they are, including their content
formattedExport = formattedExport.replace(/\s*\{\s*$/m, ' {')
.replace(/^\s*}/m, '}')
.replace(/;\s*$/g, '')
} else if (formattedExport.startsWith('export type')) {
// Keep type declarations as they are
formattedExport = formattedExport.replace(/;\s*$/, '')
}

declarations += `${formattedComment}${formattedExport}\n\n`
declarations += `${formattedComment}\n${formattedExport}\n\n`

// Add types used in the export to usedTypes
const typeRegex = /\b([A-Z]\w+)(?:<[^>]*>)?/g
Expand All @@ -76,39 +71,55 @@ export async function extractTypeFromSource(filePath: string): Promise<string> {
importMap.forEach((types, path) => {
const usedTypesFromPath = [...types].filter(type => usedTypes.has(type))
if (usedTypesFromPath.length > 0) {
importDeclarations += `import type { ${usedTypesFromPath.join(', ')} } from '${path}';\n`
importDeclarations += `import type { ${usedTypesFromPath.join(', ')} } from '${path}'\n`
}
})

if (importDeclarations) {
declarations = importDeclarations + '\n' + declarations
}

return declarations.trim() + '\n'
// Apply final formatting
const formattedDeclarations = formatDeclarations(declarations, false)

console.log(`Unformatted declarations for ${filePath}:`, declarations)
console.log(`Extracted declarations for ${filePath}:`, formattedDeclarations)

return formattedDeclarations
}

export async function extractConfigTypeFromSource(filePath: string): Promise<string> {
const fileContent = await readFile(filePath, 'utf-8')
let declarations = ''

// Handle type imports
const importRegex = /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g
let importMatch
while ((importMatch = importRegex.exec(fileContent)) !== null) {
const types = importMatch[1].split(',').map(t => t.trim())
const from = importMatch[2]
declarations += `import type { ${types.join(', ')} } from '${from}'\n\n` // Add two newlines here
}
try {
// Handle type imports
const importRegex = /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g
let importMatch
while ((importMatch = importRegex.exec(fileContent)) !== null) {
const [, types, from] = importMatch
const typeList = types.split(',').map(t => t.trim())
declarations += `import type { ${typeList.join(', ')} } from '${from}'\n`
}

// Handle exports
const exportRegex = /export\s+const\s+(\w+)\s*:\s*([^=]+)\s*=/g
let exportMatch
while ((exportMatch = exportRegex.exec(fileContent)) !== null) {
const [, name, type] = exportMatch
declarations += `export declare const ${name}: ${type.trim()}\n`
}
if (declarations) {
declarations += '\n'
}

return declarations.trim() + '\n'
// Handle exports
const exportRegex = /export\s+const\s+(\w+)\s*:\s*([^=]+)\s*=/g
let exportMatch
while ((exportMatch = exportRegex.exec(fileContent)) !== null) {
const [, name, type] = exportMatch
declarations += `export declare const ${name}: ${type.trim()}\n`
}

console.log(`Extracted config declarations for ${filePath}:`, declarations)
return declarations.trim() + '\n'
} catch (error) {
console.error(`Error extracting config declarations from ${filePath}:`, error)
return ''
}
}

export async function extractIndexTypeFromSource(filePath: string): Promise<string> {
Expand All @@ -122,5 +133,6 @@ export async function extractIndexTypeFromSource(filePath: string): Promise<stri
declarations += `${match[0]}\n`
}

console.log(`Extracted index declarations for ${filePath}:`, declarations)
return declarations.trim() + '\n'
}
73 changes: 31 additions & 42 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,46 @@ import { writeToFile, getAllTypeScriptFiles, checkIsolatedDeclarations, formatDe
import { extractTypeFromSource, extractConfigTypeFromSource, extractIndexTypeFromSource } from './extract'

export async function generateDeclarationsFromFiles(options: DtsGenerationConfig = config): Promise<void> {
// Check for isolatedModules setting
const isIsolatedDeclarations = await checkIsolatedDeclarations(options)
if (!isIsolatedDeclarations) {
console.error('Error: isolatedModules must be set to true in your tsconfig.json. Ensure `tsc --noEmit` does not output any errors.')
return
}

if (options.clean) {
console.log('Cleaning output directory...')
await rm(options.outdir, { recursive: true, force: true })
}

const validationResult = validateOptions(options)
try {
// Check for isolatedModules setting
const isIsolatedDeclarations = await checkIsolatedDeclarations(options)
if (!isIsolatedDeclarations) {
console.error('Error: isolatedModules must be set to true in your tsconfig.json. Ensure `tsc --noEmit` does not output any errors.')
return
}

if (validationResult.isErr()) {
console.error(validationResult.error.message)
return
}
if (options.clean) {
console.log('Cleaning output directory...')
await rm(options.outdir, { recursive: true, force: true })
}

const files = await getAllTypeScriptFiles(options.root)
console.log('Found the following TypeScript files:', files)
const files = await getAllTypeScriptFiles(options.root)
console.log('Found the following TypeScript files:', files)

for (const file of files) {
console.log(`Processing file: ${file}`)
let fileDeclarations
const isConfigFile = file.endsWith('config.ts')
const isIndexFile = file.endsWith('index.ts')
if (isConfigFile) {
fileDeclarations = await extractConfigTypeFromSource(file)
} else if (isIndexFile) {
fileDeclarations = await extractIndexTypeFromSource(file)
} else {
fileDeclarations = await extractTypeFromSource(file)
}
for (const file of files) {
console.log(`Processing file: ${file}`)
const fileDeclarations = await extractTypeFromSource(file)

if (fileDeclarations) {
const relativePath = relative(options.root, file)
const outputPath = join(options.outdir, relativePath.replace(/\.ts$/, '.d.ts'))
if (fileDeclarations) {
const relativePath = relative(options.root, file)
const outputPath = join(options.outdir, relativePath.replace(/\.ts$/, '.d.ts'))

// Ensure the directory exists
await mkdir(dirname(outputPath), { recursive: true })
// Ensure the directory exists
await mkdir(dirname(outputPath), { recursive: true })

// Format and write the declarations
const formattedDeclarations = formatDeclarations(fileDeclarations, isConfigFile)
await writeToFile(outputPath, formattedDeclarations)
// Write the declarations without additional formatting
await writeToFile(outputPath, fileDeclarations)

console.log(`Generated ${outputPath}`)
console.log(`Generated ${outputPath}`)
} else {
console.warn(`No declarations extracted for ${file}`)
}
}
}

console.log('Declaration file generation complete')
console.log('Declaration file generation complete')
} catch (error) {
console.error('Error generating declarations:', error)
}
}

export async function generate(options?: DtsGenerationOption): Promise<void> {
Expand Down
40 changes: 17 additions & 23 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readdir, readFile } from 'node:fs/promises'
import { extname, join } from 'node:path'
import { formatComment } from './utils'
import { config } from './config'
import { type DtsGenerationConfig } from './types'

Expand Down Expand Up @@ -33,36 +34,29 @@ export async function checkIsolatedDeclarations(options: DtsGenerationConfig): P

export function formatDeclarations(declarations: string, isConfigFile: boolean): string {
if (isConfigFile) {
return declarations
.replace(/\n{3,}/g, '\n\n')
.replace(/(\w+):\s+/g, '$1: ')
.trim() + '\n'
return declarations.trim() + '\n'
}

return declarations
.replace(/\n{3,}/g, '\n\n')
.replace(/(\w+):\s+/g, '$1: ')
.replace(/\s*\n\s*/g, '\n')
.replace(/\{\s*\n\s*\n/g, '{\n')
.replace(/;\n/g, '\n')
.replace(/export (interface|type) ([^\{]+)\s*\{\s*\n/g, 'export $1 $2 {\n')
.replace(/\n\s*\}/g, '\n}')
.replace(/;\s*\n/g, '\n')
.replace(/export interface ([^\{]+)\{/g, 'export interface $1{ ')
.replace(/^(\s*\w+:.*(?:\n|$))/gm, ' $1')
.replace(/}\n\n(?=\/\*\*|export (interface|type))/g, '}\n\n')
.replace(/^(import .*\n)+/m, match => match.trim() + '\n\n')
.replace(/(\/\*\*[\s\S]*?\*\/\s*)(export\s+(?:interface|type|const))/g, '$1\n$2')
.replace(/\*\/\n\n/g, '*/\n') // Remove extra newline after comment
.replace(/\* \//g, '*/') // Ensure proper closing of comments
.replace(/\/\*\*\n([^*]*)(\n \*\/)/g, (match, content) => {
const formattedContent = content.split('\n').map(line => ` *${line.trim() ? ' ' + line.trim() : ''}`).join('\n')
return `/**\n${formattedContent}\n */`
})
.trim() + '\n'
}

export function formatComment(comment: string): string {
return comment
.replace(/^\/\*\*\s*\n?/, '/**\n * ')
.replace(/^[\t ]*\*[\t ]?/gm, ' * ')
.replace(/\s*\*\/\s*$/, '\n */')
.replace(/^\s*\* $/gm, ' *')
.replace(/\* \//g, '*/')
.replace(/\n \* \n/g, '\n *\n')
.trim();
const lines = comment.split('\n')
return lines
.map((line, index) => {
if (index === 0) return '/**'
if (index === lines.length - 1) return ' */'
const trimmedLine = line.replace(/^\s*\*?\s?/, '').trim()
return ` * ${trimmedLine}`
})
.join('\n')
}

0 comments on commit 8598729

Please sign in to comment.