Skip to content

Commit caedf34

Browse files
committed
chore: wip
1 parent a92e32b commit caedf34

File tree

3 files changed

+152
-134
lines changed

3 files changed

+152
-134
lines changed

bun.lockb

44.9 KB
Binary file not shown.

src/generate.ts

+120-134
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,110 @@
11
import type { Result } from 'neverthrow'
22
import type { DtsGenerationConfig, DtsGenerationOption } from './types'
3-
import { readdir, readFile, rm, mkdir } from 'node:fs/promises'
4-
import { extname, join, relative, dirname } from 'node:path'
3+
import { readFile, rm, mkdir } from 'node:fs/promises'
4+
import { join, relative, dirname } from 'node:path'
55
import { err, ok } from 'neverthrow'
66
import { config } from './config'
7+
import { writeToFile, getAllTypeScriptFiles, checkIsolatedDeclarations } from './utils'
78

8-
function validateOptions(options: unknown): Result<DtsGenerationOption, Error> {
9-
if (typeof options === 'object' && options !== null) {
10-
return ok(options as DtsGenerationOption)
9+
export async function generateDeclarationsFromFiles(options: DtsGenerationConfig = config): Promise<void> {
10+
// Check for isolatedModules setting
11+
const isIsolatedDeclarations = await checkIsolatedDeclarations(options)
12+
if (!isIsolatedDeclarations) {
13+
console.error('Error: isolatedModules must be set to true in your tsconfig.json. Ensure `tsc --noEmit` does not output any errors.')
14+
return
1115
}
1216

13-
return err(new Error('Invalid options'))
17+
if (options.clean) {
18+
console.log('Cleaning output directory...')
19+
await rm(options.outdir, { recursive: true, force: true })
20+
}
21+
22+
const validationResult = validateOptions(options)
23+
24+
if (validationResult.isErr()) {
25+
console.error(validationResult.error.message)
26+
return
27+
}
28+
29+
const files = await getAllTypeScriptFiles(options.root)
30+
console.log('Found the following TypeScript files:', files)
31+
32+
for (const file of files) {
33+
console.log(`Processing file: ${file}`)
34+
let fileDeclarations
35+
const isConfigFile = file.endsWith('config.ts')
36+
const isIndexFile = file.endsWith('index.ts')
37+
if (isConfigFile) {
38+
fileDeclarations = await extractConfigTypeFromSource(file)
39+
} else if (isIndexFile) {
40+
fileDeclarations = await extractIndexTypeFromSource(file)
41+
} else {
42+
fileDeclarations = await extractTypeFromSource(file)
43+
}
44+
45+
if (fileDeclarations) {
46+
const relativePath = relative(options.root, file)
47+
const outputPath = join(options.outdir, relativePath.replace(/\.ts$/, '.d.ts'))
48+
49+
// Ensure the directory exists
50+
await mkdir(dirname(outputPath), { recursive: true })
51+
52+
// Format and write the declarations
53+
const formattedDeclarations = formatDeclarations(fileDeclarations, isConfigFile)
54+
await writeToFile(outputPath, formattedDeclarations)
55+
56+
console.log(`Generated ${outputPath}`)
57+
}
58+
}
59+
60+
61+
console.log('Declaration file generation complete')
62+
}
63+
64+
export async function generate(options?: DtsGenerationOption): Promise<void> {
65+
await generateDeclarationsFromFiles({ ...config, ...options })
1466
}
1567

1668
async function extractTypeFromSource(filePath: string): Promise<string> {
1769
const fileContent = await readFile(filePath, 'utf-8')
1870
let declarations = ''
19-
let imports = new Set()
71+
let imports = new Set<string>()
2072

21-
// Handle exports
22-
const exportRegex = /export\s+((?:interface|type|const|function|async function)\s+\w+(?:\s*=\s*[^;]+|\s*\{[^}]*\}|\s*\([^)]*\)[^;]*));?/gs
73+
// Handle imported types
74+
const importRegex = /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g
75+
let importMatch
76+
while ((importMatch = importRegex.exec(fileContent)) !== null) {
77+
const types = importMatch[1].split(',').map(t => t.trim())
78+
const from = importMatch[2]
79+
types.forEach(type => imports.add(`${type}:${from}`))
80+
}
81+
82+
// Handle exported functions with comments
83+
const exportedFunctionRegex = /(\/\*\*[\s\S]*?\*\/\s*)?(export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*:\s*([^{]+))/g
2384
let match
24-
while ((match = exportRegex.exec(fileContent)) !== null) {
25-
const declaration = match[1].trim()
85+
while ((match = exportedFunctionRegex.exec(fileContent)) !== null) {
86+
const [, comment, , isAsync, name, params, returnType] = match
87+
const cleanParams = params.replace(/\s*=\s*[^,)]+/g, '')
88+
const declaration = `${comment || ''}export declare ${isAsync || ''}function ${name}(${cleanParams}): ${returnType.trim()}`
89+
declarations += `${declaration}\n\n`
90+
91+
// Check for types used in the declaration and add them to imports
92+
const usedTypes = [...params.matchAll(/(\w+):\s*([A-Z]\w+)/g), ...returnType.matchAll(/\b([A-Z]\w+)\b/g)]
93+
usedTypes.forEach(([, , type]) => {
94+
if (type) imports.add(type)
95+
})
96+
}
97+
98+
// Handle other exports (interface, type, const)
99+
const otherExportRegex = /(\/\*\*[\s\S]*?\*\/\s*)?(export\s+((?:interface|type|const)\s+\w+(?:\s*=\s*[^;]+|\s*\{[^}]*\})));?/gs
100+
while ((match = otherExportRegex.exec(fileContent)) !== null) {
101+
const [, comment, exportStatement, declaration] = match
26102
if (declaration.startsWith('interface') || declaration.startsWith('type')) {
27-
declarations += `export ${declaration}\n\n`
103+
declarations += `${comment || ''}${exportStatement}\n\n`
28104
} else if (declaration.startsWith('const')) {
29105
const [, name, type] = declaration.match(/const\s+(\w+):\s*([^=]+)/) || []
30106
if (name && type) {
31-
declarations += `export declare const ${name}: ${type.trim()}\n\n`
32-
}
33-
} else if (declaration.startsWith('function') || declaration.startsWith('async function')) {
34-
const funcMatch = declaration.match(/(async\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*:\s*([^{]+)/)
35-
if (funcMatch) {
36-
const [, isAsync, name, params, returnType] = funcMatch
37-
// Remove default values in parameters
38-
const cleanParams = params.replace(/\s*=\s*[^,)]+/g, '')
39-
declarations += `export declare ${isAsync || ''}function ${name}(${cleanParams}): ${returnType.trim()}\n\n`
107+
declarations += `${comment || ''}export declare const ${name}: ${type.trim()}\n\n`
40108
}
41109
}
42110

@@ -45,24 +113,25 @@ async function extractTypeFromSource(filePath: string): Promise<string> {
45113
usedTypes.forEach(type => imports.add(type))
46114
}
47115

48-
// Only include imports for types that are actually used
49-
const importRegex = /import\s+type\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g
116+
// Generate import statements for used types
50117
let importDeclarations = ''
51-
while ((match = importRegex.exec(fileContent)) !== null) {
52-
const types = match[1].split(',').map(t => t.trim())
53-
const from = match[2]
54-
const usedTypes = types.filter(type => imports.has(type))
55-
if (usedTypes.length > 0) {
56-
importDeclarations += `import type { ${usedTypes.join(', ')} } from '${from}'\n`
118+
const importMap = new Map()
119+
imports.forEach(typeWithPath => {
120+
const [type, path] = typeWithPath.split(':')
121+
if (path) {
122+
if (!importMap.has(path)) importMap.set(path, new Set())
123+
importMap.get(path).add(type)
57124
}
58-
}
125+
})
126+
importMap.forEach((types, path) => {
127+
importDeclarations += `import type { ${Array.from(types).join(', ')} } from '${path}'\n`
128+
})
59129

60130
if (importDeclarations) {
61-
declarations = importDeclarations + '\n\n' + declarations // Add two newlines here
131+
declarations = importDeclarations + '\n' + declarations
62132
}
63133

64-
// Add a special marker between imports and exports
65-
return declarations.replace(/\n(export)/, '\n###LINEBREAK###$1').trim() + '\n'
134+
return declarations.trim() + '\n'
66135
}
67136

68137
async function extractConfigTypeFromSource(filePath: string): Promise<string> {
@@ -105,113 +174,30 @@ async function extractIndexTypeFromSource(filePath: string): Promise<string> {
105174

106175
function formatDeclarations(declarations: string, isConfigFile: boolean): string {
107176
if (isConfigFile) {
108-
// Special formatting for config.d.ts
109177
return declarations
110-
.replace(/\n{3,}/g, '\n\n') // Remove excess newlines, but keep doubles
111-
.replace(/(\w+):\s+/g, '$1: ') // Ensure single space after colon
112-
.trim() + '\n' // Ensure final newline
178+
.replace(/\n{3,}/g, '\n\n')
179+
.replace(/(\w+):\s+/g, '$1: ')
180+
.trim() + '\n'
113181
}
114182

115-
// Regular formatting for other files
116183
return declarations
117-
.replace(/\n{3,}/g, '\n\n') // Remove excess newlines, but keep doubles
118-
.replace(/(\w+):\s+/g, '$1: ') // Ensure single space after colon
119-
.replace(/\s*\n\s*/g, '\n') // Remove extra spaces around newlines
120-
.replace(/\{\s*\n\s*\n/g, '{\n') // Remove extra newline after opening brace
121-
.replace(/\n\s*\}/g, '\n}') // Remove extra space before closing brace
122-
.replace(/;\s*\n/g, '\n') // Remove semicolons at end of lines
123-
.replace(/export interface ([^\{]+)\{/g, 'export interface $1{ ') // Add space after opening brace for interface
124-
.replace(/^(\s*\w+:.*(?:\n|$))/gm, ' $1') // Ensure all properties in interface are indented
125-
.replace(/}\n\n(?=export (interface|type))/g, '}\n') // Remove extra newline between interface/type declarations
126-
.replace(/^(import .*\n)+/m, match => match.trim() + '\n') // Ensure imports are grouped
127-
.replace(/###LINEBREAK###/g, '\n') // Replace the special marker with a newline
128-
.replace(/\n{3,}/g, '\n\n') // Final pass to remove any triple newlines
129-
.trim() + '\n' // Ensure final newline
184+
.replace(/\n{3,}/g, '\n\n')
185+
.replace(/(\w+):\s+/g, '$1: ')
186+
.replace(/\s*\n\s*/g, '\n')
187+
.replace(/\{\s*\n\s*\n/g, '{\n')
188+
.replace(/\n\s*\}/g, '\n}')
189+
.replace(/;\s*\n/g, '\n')
190+
.replace(/export interface ([^\{]+)\{/g, 'export interface $1{ ')
191+
.replace(/^(\s*\w+:.*(?:\n|$))/gm, ' $1')
192+
.replace(/}\n\n(?=\/\*\*|export (interface|type))/g, '}\n')
193+
.replace(/^(import .*\n)+/m, match => match.trim() + '\n')
194+
.trim() + '\n'
130195
}
131196

132-
export async function generateDeclarationsFromFiles(options: DtsGenerationConfig = config): Promise<void> {
133-
// Check for isolatedModules setting
134-
const isIsolatedDeclarations = await checkIsolatedDeclarations(options)
135-
if (!isIsolatedDeclarations) {
136-
console.error('Error: isolatedModules must be set to true in your tsconfig.json. Ensure `tsc --noEmit` does not output any errors.')
137-
return
138-
}
139-
140-
if (options.clean) {
141-
console.log('Cleaning output directory...')
142-
await rm(options.outdir, { recursive: true, force: true })
143-
}
144-
145-
const validationResult = validateOptions(options)
146-
147-
if (validationResult.isErr()) {
148-
console.error(validationResult.error.message)
149-
return
150-
}
151-
152-
const files = await getAllTypeScriptFiles(options.root)
153-
console.log('Found the following TypeScript files:', files)
154-
155-
for (const file of files) {
156-
console.log(`Processing file: ${file}`)
157-
let fileDeclarations
158-
const isConfigFile = file.endsWith('config.ts')
159-
const isIndexFile = file.endsWith('index.ts')
160-
if (isConfigFile) {
161-
fileDeclarations = await extractConfigTypeFromSource(file)
162-
} else if (isIndexFile) {
163-
fileDeclarations = await extractIndexTypeFromSource(file)
164-
} else {
165-
fileDeclarations = await extractTypeFromSource(file)
166-
}
167-
168-
if (fileDeclarations) {
169-
const relativePath = relative(options.root, file)
170-
const outputPath = join(options.outdir, relativePath.replace(/\.ts$/, '.d.ts'))
171-
172-
// Ensure the directory exists
173-
await mkdir(dirname(outputPath), { recursive: true })
174-
175-
// Format and write the declarations
176-
const formattedDeclarations = formatDeclarations(fileDeclarations, isConfigFile)
177-
await writeToFile(outputPath, formattedDeclarations)
178-
179-
console.log(`Generated ${outputPath}`)
180-
}
197+
function validateOptions(options: unknown): Result<DtsGenerationOption, Error> {
198+
if (typeof options === 'object' && options !== null) {
199+
return ok(options as DtsGenerationOption)
181200
}
182201

183-
184-
console.log('Declaration file generation complete')
185-
}
186-
187-
async function getAllTypeScriptFiles(directory?: string): Promise<string[]> {
188-
const dir = directory ?? config.root
189-
const entries = await readdir(dir, { withFileTypes: true })
190-
191-
const files = await Promise.all(entries.map((entry) => {
192-
const res = join(dir, entry.name)
193-
return entry.isDirectory() ? getAllTypeScriptFiles(res) : res
194-
}))
195-
196-
return Array.prototype.concat(...files).filter(file => extname(file) === '.ts')
197-
}
198-
199-
export async function generate(options?: DtsGenerationOption): Promise<void> {
200-
await generateDeclarationsFromFiles({ ...config, ...options })
201-
}
202-
203-
async function writeToFile(filePath: string, content: string): Promise<void> {
204-
await Bun.write(filePath, content)
205-
}
206-
207-
async function checkIsolatedDeclarations(options: DtsGenerationConfig): Promise<boolean> {
208-
try {
209-
const tsconfigPath = options.tsconfigPath || join(options.root, 'tsconfig.json')
210-
const tsconfigContent = await readFile(tsconfigPath, 'utf-8')
211-
const tsconfig = JSON.parse(tsconfigContent)
212-
213-
return tsconfig.compilerOptions?.isolatedDeclarations === true
214-
} catch (error) {
215-
return false
216-
}
202+
return err(new Error('Invalid options'))
217203
}

src/utils.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { readdir, readFile } from 'node:fs/promises'
2+
import { extname, join } from 'node:path'
3+
import { config } from './config'
4+
import { type DtsGenerationConfig } from './types'
5+
6+
export async function writeToFile(filePath: string, content: string): Promise<void> {
7+
await Bun.write(filePath, content)
8+
}
9+
10+
export async function getAllTypeScriptFiles(directory?: string): Promise<string[]> {
11+
const dir = directory ?? config.root
12+
const entries = await readdir(dir, { withFileTypes: true })
13+
14+
const files = await Promise.all(entries.map((entry) => {
15+
const res = join(dir, entry.name)
16+
return entry.isDirectory() ? getAllTypeScriptFiles(res) : res
17+
}))
18+
19+
return Array.prototype.concat(...files).filter(file => extname(file) === '.ts')
20+
}
21+
22+
export async function checkIsolatedDeclarations(options: DtsGenerationConfig): Promise<boolean> {
23+
try {
24+
const tsconfigPath = options.tsconfigPath || join(options.root, 'tsconfig.json')
25+
const tsconfigContent = await readFile(tsconfigPath, 'utf-8')
26+
const tsconfig = JSON.parse(tsconfigContent)
27+
28+
return tsconfig.compilerOptions?.isolatedDeclarations === true
29+
} catch (error) {
30+
return false
31+
}
32+
}

0 commit comments

Comments
 (0)