Skip to content

Commit d3bba15

Browse files
committed
chore: wip
1 parent e808e1a commit d3bba15

File tree

5 files changed

+163
-60
lines changed

5 files changed

+163
-60
lines changed

src/extractor.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ function buildFunctionSignature(node: ts.FunctionDeclaration): string {
217217

218218
// Add parameters (no space before)
219219
const params = node.parameters.map(param => {
220-
const name = param.name.getText()
220+
const name = getParameterName(param)
221221
const type = param.type?.getText() || 'any'
222-
const optional = param.questionToken ? '?' : ''
222+
const optional = param.questionToken || param.initializer ? '?' : ''
223223
return `${name}${optional}: ${type}`
224224
}).join(', ')
225225
result += `(${params})`
@@ -513,6 +513,41 @@ function getNodeText(node: ts.Node, sourceCode: string): string {
513513
return sourceCode.slice(node.getStart(), node.getEnd())
514514
}
515515

516+
/**
517+
* Get parameter name without default values for DTS
518+
*/
519+
function getParameterName(param: ts.ParameterDeclaration): string {
520+
if (ts.isObjectBindingPattern(param.name)) {
521+
// For destructured parameters like { name, cwd, defaultConfig }
522+
// We need to reconstruct without default values
523+
const elements = param.name.elements.map(element => {
524+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
525+
// Don't include default values in DTS
526+
return element.name.getText()
527+
}
528+
return ''
529+
}).filter(Boolean)
530+
531+
// Format on multiple lines if there are multiple elements
532+
if (elements.length > 3) {
533+
return `{\n ${elements.join(',\n ')},\n}`
534+
}
535+
return `{ ${elements.join(', ')} }`
536+
} else if (ts.isArrayBindingPattern(param.name)) {
537+
// For array destructuring parameters
538+
const elements = param.name.elements.map(element => {
539+
if (element && ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
540+
return element.name.getText()
541+
}
542+
return ''
543+
}).filter(Boolean)
544+
return `[${elements.join(', ')}]`
545+
} else {
546+
// Simple parameter name
547+
return param.name.getText()
548+
}
549+
}
550+
516551
/**
517552
* Check if a node has export modifier
518553
*/

src/processor.ts

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
import type { Declaration, ProcessingContext } from './types'
22

3+
/**
4+
* Extract all imported items from an import statement
5+
*/
6+
function extractAllImportedItems(importText: string): string[] {
7+
const items: string[] = []
8+
9+
// Handle mixed imports: import defaultName, { a, b } from 'module'
10+
const mixedMatch = importText.match(/import\s+([^{,\s]+),\s*\{?\s*([^}]+)\s*\}?\s+from/)
11+
if (mixedMatch) {
12+
// Add default import
13+
items.push(mixedMatch[1].trim())
14+
// Add named imports
15+
const namedItems = mixedMatch[2].split(',').map(item => item.replace(/^type\s+/, '').trim())
16+
items.push(...namedItems)
17+
return items
18+
}
19+
20+
// Handle named imports: import { a, b } from 'module'
21+
const namedMatch = importText.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
22+
if (namedMatch) {
23+
const namedItems = namedMatch[1].split(',').map(item => item.replace(/^type\s+/, '').trim())
24+
items.push(...namedItems)
25+
return items
26+
}
27+
28+
// Handle default imports: import defaultName from 'module'
29+
const defaultMatch = importText.match(/import\s+(?:type\s+)?([^{,\s]+)\s+from/)
30+
if (defaultMatch) {
31+
items.push(defaultMatch[1].trim())
32+
return items
33+
}
34+
35+
return items
36+
}
37+
338
/**
439
* Process declarations and convert them to narrow DTS format
540
*/
@@ -60,16 +95,16 @@ export function processDeclarations(
6095
// Check which imports are needed based on exported functions and types
6196
for (const func of functions) {
6297
if (func.isExported) {
63-
// Check function signature for imported types (only in the function signature, not the body)
64-
const funcDeclaration = func.text.split('{')[0] // Only check the signature part
98+
// Check the entire function signature for imported types
99+
const funcDeclaration = func.text
65100
for (const imp of imports) {
66-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
67-
if (importMatch) {
68-
const importedItems = importMatch[1].split(',').map(item => item.trim())
69-
for (const item of importedItems) {
70-
if (funcDeclaration.includes(item)) {
71-
usedImportItems.add(item)
72-
}
101+
// Handle all import patterns: named, default, and mixed
102+
const allImportedItems = extractAllImportedItems(imp.text)
103+
for (const item of allImportedItems) {
104+
// Use word boundary regex to match exact identifiers, not substrings
105+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
106+
if (regex.test(funcDeclaration)) {
107+
usedImportItems.add(item)
73108
}
74109
}
75110
}
@@ -85,11 +120,12 @@ export function processDeclarations(
85120
const typeMatches = importText.match(/type\s+([A-Za-z_$][A-Za-z0-9_$]*)/g)
86121
const valueMatches = importText.match(/import\s+\{([^}]+)\}/)
87122

88-
// Check type imports
123+
// Check type imports
89124
if (typeMatches) {
90125
for (const typeMatch of typeMatches) {
91126
const typeName = typeMatch.replace('type ', '').trim()
92-
if (variable.text.includes(typeName)) {
127+
const regex = new RegExp(`\\b${typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
128+
if (regex.test(variable.text)) {
93129
usedImportItems.add(typeName)
94130
}
95131
}
@@ -101,7 +137,8 @@ export function processDeclarations(
101137
item.replace(/type\s+/, '').trim()
102138
)
103139
for (const importName of imports) {
104-
if (variable.text.includes(importName)) {
140+
const regex = new RegExp(`\\b${importName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
141+
if (regex.test(variable.text)) {
105142
usedImportItems.add(importName)
106143
}
107144
}
@@ -119,13 +156,11 @@ export function processDeclarations(
119156

120157
if (iface.isExported || isReferencedByExports) {
121158
for (const imp of imports) {
122-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
123-
if (importMatch) {
124-
const importedItems = importMatch[1].split(',').map(item => item.trim())
125-
for (const item of importedItems) {
126-
if (iface.text.includes(item)) {
127-
usedImportItems.add(item)
128-
}
159+
const allImportedItems = extractAllImportedItems(imp.text)
160+
for (const item of allImportedItems) {
161+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
162+
if (regex.test(iface.text)) {
163+
usedImportItems.add(item)
129164
}
130165
}
131166
}
@@ -135,13 +170,11 @@ export function processDeclarations(
135170
for (const type of types) {
136171
if (type.isExported) {
137172
for (const imp of imports) {
138-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
139-
if (importMatch) {
140-
const importedItems = importMatch[1].split(',').map(item => item.trim())
141-
for (const item of importedItems) {
142-
if (type.text.includes(item)) {
143-
usedImportItems.add(item)
144-
}
173+
const allImportedItems = extractAllImportedItems(imp.text)
174+
for (const item of allImportedItems) {
175+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
176+
if (regex.test(type.text)) {
177+
usedImportItems.add(item)
145178
}
146179
}
147180
}
@@ -151,16 +184,10 @@ export function processDeclarations(
151184
// Check which imports are needed for re-exports
152185
for (const item of exportedItems) {
153186
for (const imp of imports) {
154-
if (imp.text.includes(item)) {
155-
// Extract the specific items from this import
156-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
157-
if (importMatch) {
158-
const importedItems = importMatch[1].split(',').map(item => item.trim())
159-
for (const importedItem of importedItems) {
160-
if (item === importedItem) {
161-
usedImportItems.add(importedItem)
162-
}
163-
}
187+
const allImportedItems = extractAllImportedItems(imp.text)
188+
for (const importedItem of allImportedItems) {
189+
if (item === importedItem) {
190+
usedImportItems.add(importedItem)
164191
}
165192
}
166193
}
@@ -169,33 +196,63 @@ export function processDeclarations(
169196
// Also check for value imports that are re-exported
170197
for (const exp of exports) {
171198
for (const imp of imports) {
172-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from/)
173-
if (importMatch) {
174-
const importedItems = importMatch[1].split(',').map(item => item.trim())
175-
for (const importedItem of importedItems) {
176-
if (exp.text.includes(importedItem)) {
177-
usedImportItems.add(importedItem)
178-
}
199+
const allImportedItems = extractAllImportedItems(imp.text)
200+
for (const importedItem of allImportedItems) {
201+
// Use word boundary regex to match exact identifiers in export statements
202+
const regex = new RegExp(`\\b${importedItem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
203+
if (regex.test(exp.text)) {
204+
usedImportItems.add(importedItem)
179205
}
180206
}
181207
}
182208
}
183209

184-
// Create filtered imports based on actually used items
210+
// Create filtered imports based on actually used items
185211
const processedImports: string[] = []
186212
for (const imp of imports) {
187-
const importMatch = imp.text.match(/import\s+(?:type\s+)?\{?\s*([^}]+)\s*\}?\s+from\s+['"]([^'"]+)['"]/)
188-
if (importMatch) {
189-
const importedItems = importMatch[1].split(',').map(item => item.trim())
213+
// Handle different import patterns - check mixed imports first
214+
const mixedImportMatch = imp.text.match(/import\s+([^{,\s]+),\s*\{?\s*([^}]+)\s*\}?\s+from\s+['"]([^'"]+)['"]/)
215+
const namedImportMatch = imp.text.match(/import\s+(?:type\s+)?\{\s*([^}]+)\s*\}\s+from\s+['"]([^'"]+)['"]/)
216+
const defaultImportMatch = imp.text.match(/import\s+(?:type\s+)?([^{,\s]+)\s+from\s+['"]([^'"]+)['"]/)
217+
218+
if (mixedImportMatch) {
219+
// Mixed import: import defaultName, { a, b } from 'module'
220+
const defaultName = mixedImportMatch[1].trim()
221+
const namedItems = mixedImportMatch[2].split(',').map(item => item.trim())
222+
const source = mixedImportMatch[3]
223+
224+
const usedDefault = usedImportItems.has(defaultName)
225+
const usedNamed = namedItems.filter(item => {
226+
const cleanItem = item.replace(/^type\s+/, '').trim()
227+
return usedImportItems.has(cleanItem)
228+
})
229+
230+
if (usedDefault || usedNamed.length > 0) {
231+
const isOriginalTypeOnly = imp.text.includes('import type')
232+
233+
let importStatement = 'import '
234+
if (isOriginalTypeOnly) {
235+
importStatement += 'type '
236+
}
237+
238+
const parts = []
239+
if (usedDefault) parts.push(defaultName)
240+
if (usedNamed.length > 0) parts.push(`{ ${usedNamed.join(', ')} }`)
241+
242+
importStatement += `${parts.join(', ')} from '${source}';`
243+
244+
processedImports.push(importStatement)
245+
}
246+
} else if (namedImportMatch && !defaultImportMatch) {
247+
// Named imports only: import { a, b } from 'module'
248+
const importedItems = namedImportMatch[1].split(',').map(item => item.trim())
190249
const usedItems = importedItems.filter(item => {
191250
const cleanItem = item.replace(/^type\s+/, '').trim()
192251
return usedImportItems.has(cleanItem)
193252
})
194253

195254
if (usedItems.length > 0) {
196-
const source = importMatch[2]
197-
198-
// Check if original import was type-only
255+
const source = namedImportMatch[2]
199256
const isOriginalTypeOnly = imp.text.includes('import type')
200257

201258
let importStatement = 'import '
@@ -204,6 +261,22 @@ export function processDeclarations(
204261
}
205262
importStatement += `{ ${usedItems.join(', ')} } from '${source}';`
206263

264+
processedImports.push(importStatement)
265+
}
266+
} else if (defaultImportMatch && !namedImportMatch) {
267+
// Default import only: import defaultName from 'module'
268+
const defaultName = defaultImportMatch[1].trim()
269+
const source = defaultImportMatch[2]
270+
271+
if (usedImportItems.has(defaultName)) {
272+
const isOriginalTypeOnly = imp.text.includes('import type')
273+
274+
let importStatement = 'import '
275+
if (isOriginalTypeOnly) {
276+
importStatement += 'type '
277+
}
278+
importStatement += `${defaultName} from '${source}';`
279+
207280
processedImports.push(importStatement)
208281
}
209282
}

test/fixtures/output/example/0007.d.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { forge, tls } from 'node-forge';
2-
import { os } from 'node:os';
1+
import forge, { pki, tls } from 'node-forge';
32
import type { CAOptions, CertificateOptions, GenerateCertReturn, TlsOption } from './types';
43
export declare function generateRandomSerial(verbose?: boolean): string;
54
export declare function calculateValidityDates(options: {
@@ -8,7 +7,7 @@ export declare function calculateValidityDates(options: {
87
notBeforeDays?: number
98
verbose?: boolean
109
}): { notBefore: Date, notAfter: Date };
11-
export declare function createRootCA(options: CAOptions): Promise<GenerateCertReturn>;
10+
export declare function createRootCA(options?: CAOptions): Promise<GenerateCertReturn>;
1211
export declare function generateCertificate(options: CertificateOptions): Promise<GenerateCertReturn>;
1312
export declare function addCertToSystemTrustStoreAndSaveCert(cert: Cert, caCert: string, options?: TlsOption): Promise<string>;
1413
export declare function storeCertificate(cert: Cert, options?: TlsOption): string;

test/fixtures/output/example/0008.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { os } from 'node:os';
21
import { pki } from 'node-forge';
32
import type { CertDetails } from './types';
43
export declare function isCertValidForDomain(certPemOrPath: string, domain: string): boolean;

test/fixtures/output/example/0009.d.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ export declare function loadConfig<T>({
44
cwd,
55
defaultConfig,
66
endpoint,
7-
headers = {
8-
'Accept': 'application/json',
9-
'Content-Type': 'application/json',
10-
},
7+
headers,
118
}: Config<T>): Promise<T>;
129
export * from './types';
1310
export * from './utils';

0 commit comments

Comments
 (0)