Skip to content

Commit 60d190c

Browse files
committed
chore: wip
chore: wip chore: wip chore: wip chore: wip
1 parent d8ea411 commit 60d190c

5 files changed

Lines changed: 100 additions & 86 deletions

File tree

fixtures/output/example-0003.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export declare const endpoints
1+
export declare const endpoints: {
2+
getUsers: '/users'
3+
getProducts: '/products'
4+
}
25

36
export interface Order {
47
orderId: number

fixtures/output/example-0004.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export declare const apiKeys
1+
export declare const apiKeys: {
2+
google: 'GOOGLE_API_KEY',
3+
facebook: 'FACEBOOK_API_KEY',
4+
}
25

36
export interface AuthResponse {
47
token: string

fixtures/output/example-0005.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export declare const defaultHeaders
1+
export declare const defaultHeaders: {
2+
'Content-Type': 'application/json',
3+
}
24

35
export interface Comment {
46
id: number

src/extract.ts

Lines changed: 87 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,115 @@
11
import { readFile } from 'node:fs/promises'
2-
import { formatComment, formatDeclarations } from './utils'
2+
import { formatDeclarations } from './utils'
33

44
export async function extractTypeFromSource(filePath: string): Promise<string> {
55
const fileContent = await readFile(filePath, 'utf-8')
6+
let imports = ''
67
let declarations = ''
7-
const usedTypes = new Set<string>()
8-
const importMap = new Map<string, Set<string>>()
8+
let exports = ''
9+
const processedDeclarations = new Set()
910

10-
// Handle re-exports
11-
const reExportRegex = /export\s*(?:\*|\{[^}]*\})\s*from\s*['"][^'"]+['"]/g
12-
let reExportMatch
13-
// eslint-disable-next-line no-cond-assign
14-
while ((reExportMatch = reExportRegex.exec(fileContent)) !== null) {
15-
declarations += `${reExportMatch[0]}\n`
11+
// Function to extract the body of a function
12+
const extractFunctionBody = (funcName: string) => {
13+
const funcRegex = new RegExp(`function\\s+${funcName}\\s*\\([^)]*\\)\\s*{([\\s\\S]*?)}`, 'g')
14+
const match = funcRegex.exec(fileContent)
15+
return match ? match[1] : ''
1616
}
1717

18-
// Capture all imports
19-
const importRegex = /import\s+(?:(type)\s+)?(?:(\{[^}]+\})|(\w+))(?:\s*,\s*(?:(\{[^}]+\})|(\w+)))?\s+from\s+['"]([^'"]+)['"]/g
20-
let importMatch
21-
// eslint-disable-next-line no-cond-assign
22-
while ((importMatch = importRegex.exec(fileContent)) !== null) {
23-
// eslint-disable-next-line unused-imports/no-unused-vars
24-
const [, isTypeImport, namedImports1, defaultImport1, namedImports2, defaultImport2, from] = importMatch
25-
if (!importMap.has(from)) {
26-
importMap.set(from, new Set())
27-
}
18+
// Function to check if an identifier is used in a given content
19+
const isIdentifierUsed = (identifier: string, content: string) => {
20+
const regex = new RegExp(`\\b${identifier}\\b`, 'g')
21+
return regex.test(content)
22+
}
2823

29-
const processImports = (imports: string | undefined) => {
30-
if (imports) {
31-
const types = imports.replace(/[{}]/g, '').split(',').map((t) => {
32-
const [name, alias] = t.split(' as ').map(s => s.trim())
33-
return { name: name.replace(/^type\s+/, ''), alias: alias || name.replace(/^type\s+/, '') }
34-
})
35-
types.forEach(({ name }) => {
36-
importMap.get(from)!.add(name)
37-
})
38-
}
24+
// Extract the body of the dts function
25+
const dtsFunctionBody = extractFunctionBody('dts')
26+
27+
// Handle imports
28+
const importRegex = /import\s+(type\s+)?(\{[^}]+\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(\{[^}]+\}|\w+))?\s+from\s+['"]([^'"]+)['"]/g
29+
const importMatches = Array.from(fileContent.matchAll(importRegex))
30+
for (const [fullImport, isType, import1, import2, from] of importMatches) {
31+
if (from === 'node:process' && !isIdentifierUsed('process', dtsFunctionBody)) {
32+
continue
3933
}
4034

41-
processImports(namedImports1)
42-
processImports(namedImports2)
35+
const importedItems = [...(import1.match(/\b\w+\b/g) || []), ...(import2?.match(/\b\w+\b/g) || [])]
36+
const usedImports = importedItems.filter(item =>
37+
isIdentifierUsed(item, dtsFunctionBody)
38+
|| isIdentifierUsed(item, fileContent.replace(/import[^;]+;/g, '')),
39+
)
4340

44-
if (defaultImport1)
45-
importMap.get(from)!.add(defaultImport1)
46-
if (defaultImport2)
47-
importMap.get(from)!.add(defaultImport2)
41+
if (usedImports.length > 0) {
42+
if (isType) {
43+
imports += `import type { ${usedImports.join(', ')} } from '${from}'\n`
44+
}
45+
else {
46+
imports += `import { ${usedImports.join(', ')} } from '${from}'\n`
47+
}
48+
}
4849
}
4950

50-
// Handle exports with comments
51-
// eslint-disable-next-line regexp/no-super-linear-backtracking
52-
const exportRegex = /(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(?:async\s+)?(?:function|const|let|var|class|interface|type)\s+\w[\s\S]*?)(?=\n\s*(?:\/\*\*|export|$))/g
53-
let match
54-
// eslint-disable-next-line no-cond-assign
55-
while ((match = exportRegex.exec(fileContent)) !== null) {
56-
const [, comment, exportStatement] = match
57-
const formattedComment = comment ? formatComment(comment.trim()) : ''
58-
let formattedExport = exportStatement.trim()
51+
// Handle all declarations
52+
const declarationRegex = /(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(const|interface|type|function)\s+(\w+)[\s\S]*?(?:;|\})\s*)/g
53+
const declarationMatches = Array.from(fileContent.matchAll(declarationRegex))
54+
for (const [, comment, declaration, declType, name] of declarationMatches) {
55+
if (!processedDeclarations.has(name)) {
56+
if (comment)
57+
declarations += `${comment.trim()}\n`
5958

60-
if (formattedExport.startsWith('export function') || formattedExport.startsWith('export async function')) {
61-
formattedExport = formattedExport.replace(/^export\s+(async\s+)?function/, 'export declare function')
62-
const functionSignature = formattedExport.match(/^.*?\)/)
63-
if (functionSignature) {
64-
let params = functionSignature[0].slice(functionSignature[0].indexOf('(') + 1, -1)
65-
params = params.replace(/\s*=[^,)]+/g, '') // Remove default values
66-
const returnType = formattedExport.match(/\):\s*([^{]+)/)
67-
formattedExport = `export declare function ${formattedExport.split('function')[1].split('(')[0].trim()}(${params})${returnType ? `: ${returnType[1].trim()}` : ''};`
59+
if (declType === 'const') {
60+
const constMatch = declaration.match(/export\s+const\s+(\w+)\s*:\s*([^=]+)=/)
61+
if (constMatch) {
62+
declarations += `export declare const ${constMatch[1]}: ${constMatch[2].trim()}\n\n`
63+
}
64+
else {
65+
declarations += `${declaration.trim()}\n\n`
66+
}
67+
}
68+
else if (declType === 'function') {
69+
const funcMatch = declaration.match(/export\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*([^{]+)/)
70+
if (funcMatch) {
71+
declarations += `export declare function ${funcMatch[1]}(${funcMatch[2]}): ${funcMatch[3].trim()}\n\n`
72+
}
73+
else {
74+
declarations += `${declaration.trim()}\n\n`
75+
}
76+
}
77+
else {
78+
declarations += `${declaration.trim()}\n\n`
6879
}
69-
}
70-
else if (formattedExport.startsWith('export const') || formattedExport.startsWith('export let') || formattedExport.startsWith('export var')) {
71-
formattedExport = formattedExport.replace(/^export\s+(const|let|var)/, 'export declare $1')
72-
formattedExport = `${formattedExport.split('=')[0].trim()};`
73-
}
7480

75-
declarations += `${formattedComment}\n${formattedExport}\n\n`
81+
processedDeclarations.add(name)
82+
}
83+
}
7684

77-
// Add types used in the export to usedTypes
78-
const typeRegex = /\b([A-Z]\w+)(?:<[^>]*>)?/g
79-
let typeMatch
80-
// eslint-disable-next-line no-cond-assign
81-
while ((typeMatch = typeRegex.exec(formattedExport)) !== null) {
82-
usedTypes.add(typeMatch[1])
85+
// Handle re-exports and standalone exports
86+
const reExportRegex = /export\s*\{([^}]+)\}(?:\s*from\s*['"]([^'"]+)['"])?\s*;?/g
87+
const reExportMatches = Array.from(fileContent.matchAll(reExportRegex))
88+
for (const [, exportList, from] of reExportMatches) {
89+
const exportItems = exportList.split(',').map(e => e.trim())
90+
if (from) {
91+
exports += `\nexport { ${exportItems.join(', ')} } from '${from}'`
8392
}
93+
else {
94+
exports += `\nexport { ${exportItems.join(', ')} }`
95+
}
96+
}
97+
98+
// Handle type exports
99+
const typeExportRegex = /export\s+type\s*\{([^}]+)\}/g
100+
const typeExportMatches = Array.from(fileContent.matchAll(typeExportRegex))
101+
for (const [, typeList] of typeExportMatches) {
102+
const types = typeList.split(',').map(t => t.trim())
103+
exports += `\n\nexport type { ${types.join(', ')} }`
84104
}
85105

86106
// Handle default export
87107
const defaultExportRegex = /export\s+default\s+(\w+)/
88108
const defaultExportMatch = fileContent.match(defaultExportRegex)
89109
if (defaultExportMatch) {
90-
declarations += `export default ${defaultExportMatch[1]}\n`
91-
}
92-
93-
// Generate import statements for used types
94-
let importDeclarations = ''
95-
importMap.forEach((types, path) => {
96-
const usedTypesFromPath = [...types].filter(type => usedTypes.has(type))
97-
if (usedTypesFromPath.length > 0) {
98-
importDeclarations += `import type { ${usedTypesFromPath.join(', ')} } from '${path}'\n`
99-
}
100-
})
101-
102-
if (importDeclarations) {
103-
declarations = `${importDeclarations}\n${declarations}`
110+
exports += `\n\nexport default ${defaultExportMatch[1]}`
104111
}
105112

106-
// Apply final formatting
107-
return formatDeclarations(declarations)
113+
const output = [imports, declarations, exports].filter(Boolean).join('\n').trim()
114+
return formatDeclarations(output)
108115
}

src/utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DtsGenerationConfig } from './types'
2-
import { readdir, readFile } from 'node:fs/promises'
2+
import { readdir } from 'node:fs/promises'
33
import { extname, join } from 'node:path'
44
import process from 'node:process'
55
import { config } from './config'
@@ -23,8 +23,7 @@ export async function getAllTypeScriptFiles(directory?: string): Promise<string[
2323
export async function checkIsolatedDeclarations(options?: DtsGenerationConfig): Promise<boolean> {
2424
try {
2525
const tsconfigPath = options?.tsconfigPath || join(options?.root ?? process.cwd(), 'tsconfig.json')
26-
const tsconfigContent = await readFile(tsconfigPath, 'utf-8')
27-
const tsconfig = JSON.parse(tsconfigContent)
26+
const tsconfig = await import(tsconfigPath)
2827

2928
return tsconfig.compilerOptions?.isolatedDeclarations === true
3029
}

0 commit comments

Comments
 (0)