|
1 | 1 | import { readFile } from 'node:fs/promises' |
2 | | -import { formatComment, formatDeclarations } from './utils' |
| 2 | +import { formatDeclarations } from './utils' |
3 | 3 |
|
4 | 4 | export async function extractTypeFromSource(filePath: string): Promise<string> { |
5 | 5 | const fileContent = await readFile(filePath, 'utf-8') |
| 6 | + let imports = '' |
6 | 7 | let declarations = '' |
7 | | - const usedTypes = new Set<string>() |
8 | | - const importMap = new Map<string, Set<string>>() |
| 8 | + let exports = '' |
| 9 | + const processedDeclarations = new Set() |
9 | 10 |
|
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] : '' |
16 | 16 | } |
17 | 17 |
|
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 | + } |
28 | 23 |
|
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 |
39 | 33 | } |
40 | 34 |
|
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 | + ) |
43 | 40 |
|
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 | + } |
48 | 49 | } |
49 | 50 |
|
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` |
59 | 58 |
|
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` |
68 | 79 | } |
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 | | - } |
74 | 80 |
|
75 | | - declarations += `${formattedComment}\n${formattedExport}\n\n` |
| 81 | + processedDeclarations.add(name) |
| 82 | + } |
| 83 | + } |
76 | 84 |
|
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}'` |
83 | 92 | } |
| 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(', ')} }` |
84 | 104 | } |
85 | 105 |
|
86 | 106 | // Handle default export |
87 | 107 | const defaultExportRegex = /export\s+default\s+(\w+)/ |
88 | 108 | const defaultExportMatch = fileContent.match(defaultExportRegex) |
89 | 109 | 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]}` |
104 | 111 | } |
105 | 112 |
|
106 | | - // Apply final formatting |
107 | | - return formatDeclarations(declarations) |
| 113 | + const output = [imports, declarations, exports].filter(Boolean).join('\n').trim() |
| 114 | + return formatDeclarations(output) |
108 | 115 | } |
0 commit comments