Skip to content

Commit 9d33756

Browse files
committed
chore: wip
1 parent 77778b9 commit 9d33756

File tree

4 files changed

+191
-26
lines changed

4 files changed

+191
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ storage
1515
fixtures/generated
1616
bin/dtsx*
1717
/test/temp-output
18+
test/debug

src/extractor.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,53 @@ import { parseFunctionDeclaration, extractLeadingComments, isExportStatement, pa
77
export function extractDeclarations(sourceCode: string, filePath: string): Declaration[] {
88
const declarations: Declaration[] = []
99

10+
// Extract modules first to avoid extracting their contents separately
11+
const modules = extractModules(sourceCode)
12+
declarations.push(...modules)
13+
14+
// Create a set of lines that are inside modules to skip them in other extractions
15+
const moduleLines = new Set<number>()
16+
for (const module of modules) {
17+
const moduleText = module.text
18+
const lines = sourceCode.split('\n')
19+
for (let i = 0; i < lines.length; i++) {
20+
if (moduleText.includes(lines[i])) {
21+
moduleLines.add(i)
22+
}
23+
}
24+
}
25+
26+
// Extract other declarations, but skip lines that are inside modules
27+
const filteredSourceCode = sourceCode.split('\n')
28+
.map((line, index) => moduleLines.has(index) ? '' : line)
29+
.join('\n')
30+
1031
// Extract functions
11-
declarations.push(...extractFunctions(sourceCode))
32+
declarations.push(...extractFunctions(filteredSourceCode))
1233

1334
// Extract variables
14-
declarations.push(...extractVariables(sourceCode))
35+
declarations.push(...extractVariables(filteredSourceCode))
1536

1637
// Extract interfaces
17-
declarations.push(...extractInterfaces(sourceCode))
38+
declarations.push(...extractInterfaces(filteredSourceCode))
1839

1940
// Extract types
20-
declarations.push(...extractTypes(sourceCode))
41+
declarations.push(...extractTypes(filteredSourceCode))
2142

2243
// Extract classes
23-
declarations.push(...extractClasses(sourceCode))
44+
declarations.push(...extractClasses(filteredSourceCode))
2445

2546
// Extract enums
26-
declarations.push(...extractEnums(sourceCode))
47+
declarations.push(...extractEnums(filteredSourceCode))
2748

2849
// Extract namespaces
29-
declarations.push(...extractNamespaces(sourceCode))
30-
31-
// Extract modules
32-
declarations.push(...extractModules(sourceCode))
50+
declarations.push(...extractNamespaces(filteredSourceCode))
3351

3452
// Extract imports
35-
declarations.push(...extractImports(sourceCode))
53+
declarations.push(...extractImports(sourceCode)) // Use original source for imports
3654

3755
// Extract exports
38-
declarations.push(...extractExports(sourceCode))
56+
declarations.push(...extractExports(sourceCode)) // Use original source for exports
3957

4058
return declarations
4159
}
@@ -562,8 +580,8 @@ export function extractInterfaces(sourceCode: string): Declaration[] {
562580
lines.slice(commentStartIndex, i).join('\n').length
563581
)
564582

565-
// Extract generics and extends
566-
const headerMatch = declaration.match(/interface\s+\w+\s*(<[^>]+>)?\s*(extends\s+[^{]+)?/)
583+
// Extract generics and extends - improved regex to handle complex generics
584+
const headerMatch = declaration.match(/interface\s+\w+\s*(<[^{]+>)?\s*(extends\s+[^{]+)?/)
567585
let generics = ''
568586
let extendsClause = ''
569587

src/processor.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export function processDeclarations(
2323
// Process imports first
2424
for (const decl of imports) {
2525
const processed = processImportDeclaration(decl)
26-
if (processed) output.push(processed)
26+
if (processed && processed.trim()) output.push(processed)
2727
}
2828

29-
if (imports.length > 0 && output.length > 0) output.push('') // Add blank line after imports
29+
if (output.length > 0) output.push('') // Add blank line after imports
3030

3131
// Process other declarations
3232
const otherDecls = [...functions, ...variables, ...interfaces, ...types, ...classes, ...enums, ...modules]
@@ -65,10 +65,14 @@ export function processDeclarations(
6565
}
6666
}
6767

68-
// Process exports last
68+
// Process exports last - deduplicate similar exports
69+
const processedExports = new Set<string>()
6970
for (const decl of exports) {
7071
const processed = processExportDeclaration(decl)
71-
if (processed) output.push(processed)
72+
if (processed && processed.trim() && !processedExports.has(processed.trim())) {
73+
processedExports.add(processed.trim())
74+
output.push(processed)
75+
}
7276
}
7377

7478
return output.filter(line => line !== '').join('\n')
@@ -106,8 +110,14 @@ export function processFunctionDeclaration(decl: Declaration): string {
106110
cleanOverload = cleanOverload.replace(/^export\s+/, '')
107111
}
108112

109-
// Add the function signature
110-
result += cleanOverload
113+
// Remove any existing declare keyword to avoid duplication
114+
cleanOverload = cleanOverload.replace(/^declare\s+/, '')
115+
116+
// Remove function keyword if present since we'll add it back
117+
cleanOverload = cleanOverload.replace(/^function\s+/, '')
118+
119+
// Add the function signature with function keyword
120+
result += 'function ' + cleanOverload
111121

112122
// Ensure it ends with semicolon
113123
if (!result.endsWith(';')) {
@@ -308,12 +318,8 @@ export function processTypeDeclaration(decl: Declaration): string {
308318
result += 'export '
309319
}
310320

311-
// Special case: The first type declaration uses 'declare type'
312-
// This seems to be a quirk of the expected output
313-
if (decl.name === 'AuthStatus' && decl.isExported) {
314-
result += 'declare '
315-
} else if (!decl.isExported) {
316-
// Only add declare for non-exported types
321+
// Only add declare for non-exported type aliases
322+
if (!decl.isExported && !decl.text.includes(' from ')) {
317323
result += 'declare '
318324
}
319325

@@ -428,6 +434,11 @@ export function processEnumDeclaration(decl: Declaration): string {
428434
* Process import statement
429435
*/
430436
export function processImportDeclaration(decl: Declaration): string {
437+
// Only include type imports in .d.ts files
438+
if (!decl.isTypeOnly) {
439+
return '' // Filter out non-type imports
440+
}
441+
431442
// Import statements remain the same in .d.ts files
432443
// Just ensure they end with semicolon
433444
let result = decl.text.trim()

test/generator.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, it, expect, beforeAll } from 'bun:test'
2+
import { readFile } from 'fs/promises'
3+
import { join } from 'path'
4+
import { generate } from '../src/generator'
5+
6+
const fixturesDir = join(__dirname, 'fixtures')
7+
const inputDir = join(fixturesDir, 'input')
8+
const expectedOutputDir = join(fixturesDir, 'output')
9+
const actualOutputDir = join(__dirname, 'temp-output')
10+
11+
// List of test files to validate
12+
const testFiles = [
13+
'class.ts',
14+
'enum.ts',
15+
'exports.ts',
16+
'function.ts',
17+
'interface.ts',
18+
'imports.ts',
19+
'module.ts',
20+
'type.ts',
21+
'variable.ts'
22+
]
23+
24+
describe('DTS Generator', () => {
25+
beforeAll(async () => {
26+
// Generate all DTS files
27+
await generate({
28+
cwd: process.cwd(),
29+
root: inputDir,
30+
entrypoints: testFiles,
31+
outdir: actualOutputDir,
32+
clean: true,
33+
keepComments: true,
34+
tsconfigPath: '',
35+
verbose: false
36+
})
37+
})
38+
39+
// Test each file
40+
testFiles.forEach(testFile => {
41+
const baseName = testFile.replace('.ts', '')
42+
43+
it(`should generate correct output for ${testFile}`, async () => {
44+
const expectedPath = join(expectedOutputDir, `${baseName}.d.ts`)
45+
const actualPath = join(actualOutputDir, `${baseName}.d.ts`)
46+
47+
const [expectedContent, actualContent] = await Promise.all([
48+
readFile(expectedPath, 'utf-8'),
49+
readFile(actualPath, 'utf-8')
50+
])
51+
52+
// Normalize whitespace for comparison
53+
const normalizeContent = (content: string) => {
54+
return content
55+
.split('\n')
56+
.map(line => line.trimEnd()) // Remove trailing whitespace
57+
.filter(line => line !== '') // Remove empty lines
58+
.join('\n')
59+
}
60+
61+
const normalizedExpected = normalizeContent(expectedContent)
62+
const normalizedActual = normalizeContent(actualContent)
63+
64+
// For detailed error reporting, compare line by line
65+
const expectedLines = normalizedExpected.split('\n')
66+
const actualLines = normalizedActual.split('\n')
67+
68+
// First check if number of lines match
69+
if (expectedLines.length !== actualLines.length) {
70+
console.error(`\n❌ ${testFile}: Line count mismatch`)
71+
console.error(`Expected ${expectedLines.length} lines, got ${actualLines.length} lines`)
72+
console.error('\nExpected:\n', expectedContent)
73+
console.error('\nActual:\n', actualContent)
74+
}
75+
76+
// Compare line by line for better error messages
77+
expectedLines.forEach((expectedLine, index) => {
78+
const actualLine = actualLines[index] || ''
79+
if (expectedLine !== actualLine) {
80+
console.error(`\n❌ ${testFile}: Mismatch at line ${index + 1}`)
81+
console.error(`Expected: "${expectedLine}"`)
82+
console.error(`Actual: "${actualLine}"`)
83+
}
84+
})
85+
86+
expect(normalizedActual).toBe(normalizedExpected)
87+
})
88+
})
89+
90+
// Test for narrowness - ensure types are as narrow or narrower than expected
91+
describe('Type Narrowness', () => {
92+
it('should infer literal types for const declarations', async () => {
93+
const actualPath = join(actualOutputDir, 'variable.d.ts')
94+
const actualContent = await readFile(actualPath, 'utf-8')
95+
96+
// Check for literal types
97+
expect(actualContent).toContain("export declare let test: 'test'")
98+
expect(actualContent).toContain("export declare const someObject: {")
99+
expect(actualContent).toContain("someString: 'Stacks'")
100+
expect(actualContent).toContain("someNumber: 1000")
101+
expect(actualContent).toContain("someBoolean: true")
102+
expect(actualContent).toContain("someFalse: false")
103+
expect(actualContent).toContain("readonly ['google', 'github']")
104+
})
105+
106+
it('should use broader types for let and var declarations', async () => {
107+
const actualPath = join(actualOutputDir, 'variable.d.ts')
108+
const actualContent = await readFile(actualPath, 'utf-8')
109+
110+
// Test file has: export let test = 'test'
111+
// Should be: export declare let test: 'test' (according to expected output)
112+
// But this seems to be a special case in the expected output
113+
114+
// Check that var gets broader type
115+
expect(actualContent).toContain("export declare var helloWorld: 'Hello World'")
116+
})
117+
118+
it('should handle function overloads correctly', async () => {
119+
const actualPath = join(actualOutputDir, 'function.d.ts')
120+
const actualContent = await readFile(actualPath, 'utf-8')
121+
122+
// Check that all overloads have 'declare'
123+
const overloadLines = actualContent
124+
.split('\n')
125+
.filter(line => line.includes('processData'))
126+
127+
// First 4 should be overload signatures, last should be implementation
128+
expect(overloadLines[0]).toContain('export declare function processData(data: string): string')
129+
expect(overloadLines[1]).toContain('export declare function processData(data: number): number')
130+
expect(overloadLines[2]).toContain('export declare function processData(data: boolean): boolean')
131+
expect(overloadLines[3]).toContain('export declare function processData<T extends object>(data: T): T')
132+
expect(overloadLines[4]).toContain('export declare function processData(data: unknown): unknown')
133+
})
134+
})
135+
})

0 commit comments

Comments
 (0)