1
1
import type { Result } from 'neverthrow'
2
2
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'
5
5
import { err , ok } from 'neverthrow'
6
6
import { config } from './config'
7
+ import { writeToFile , getAllTypeScriptFiles , checkIsolatedDeclarations } from './utils'
7
8
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
11
15
}
12
16
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 ( / \. t s $ / , '.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 } )
14
66
}
15
67
16
68
async function extractTypeFromSource ( filePath : string ) : Promise < string > {
17
69
const fileContent = await readFile ( filePath , 'utf-8' )
18
70
let declarations = ''
19
- let imports = new Set ( )
71
+ let imports = new Set < string > ( )
20
72
21
- // Handle exports
22
- const exportRegex = / e x p o r t \s + ( (?: i n t e r f a c e | t y p e | c o n s t | f u n c t i o n | a s y n c f u n c t i o n ) \s + \w + (?: \s * = \s * [ ^ ; ] + | \s * \{ [ ^ } ] * \} | \s * \( [ ^ ) ] * \) [ ^ ; ] * ) ) ; ? / gs
73
+ // Handle imported types
74
+ const importRegex = / i m p o r t \s + t y p e \s * \{ ( [ ^ } ] + ) \} \s * f r o m \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 * ) ? ( e x p o r t \s + ( a s y n c \s + ) ? f u n c t i o n \s + ( \w + ) \s * \( ( [ ^ ) ] * ) \) \s * : \s * ( [ ^ { ] + ) ) / g
23
84
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 * ) ? ( e x p o r t \s + ( (?: i n t e r f a c e | t y p e | c o n s t ) \s + \w + (?: \s * = \s * [ ^ ; ] + | \s * \{ [ ^ } ] * \} ) ) ) ; ? / gs
100
+ while ( ( match = otherExportRegex . exec ( fileContent ) ) !== null ) {
101
+ const [ , comment , exportStatement , declaration ] = match
26
102
if ( declaration . startsWith ( 'interface' ) || declaration . startsWith ( 'type' ) ) {
27
- declarations += `export ${ declaration } \n\n`
103
+ declarations += `${ comment || '' } ${ exportStatement } \n\n`
28
104
} else if ( declaration . startsWith ( 'const' ) ) {
29
105
const [ , name , type ] = declaration . match ( / c o n s t \s + ( \w + ) : \s * ( [ ^ = ] + ) / ) || [ ]
30
106
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 ( / ( a s y n c \s + ) ? f u n c t i o n \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`
40
108
}
41
109
}
42
110
@@ -45,24 +113,25 @@ async function extractTypeFromSource(filePath: string): Promise<string> {
45
113
usedTypes . forEach ( type => imports . add ( type ) )
46
114
}
47
115
48
- // Only include imports for types that are actually used
49
- const importRegex = / i m p o r t \s + t y p e \s * \{ ( [ ^ } ] + ) \} \s * f r o m \s * [ ' " ] ( [ ^ ' " ] + ) [ ' " ] / g
116
+ // Generate import statements for used types
50
117
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 )
57
124
}
58
- }
125
+ } )
126
+ importMap . forEach ( ( types , path ) => {
127
+ importDeclarations += `import type { ${ Array . from ( types ) . join ( ', ' ) } } from '${ path } '\n`
128
+ } )
59
129
60
130
if ( importDeclarations ) {
61
- declarations = importDeclarations + '\n\n ' + declarations // Add two newlines here
131
+ declarations = importDeclarations + '\n' + declarations
62
132
}
63
133
64
- // Add a special marker between imports and exports
65
- return declarations . replace ( / \n ( e x p o r t ) / , '\n###LINEBREAK###$1' ) . trim ( ) + '\n'
134
+ return declarations . trim ( ) + '\n'
66
135
}
67
136
68
137
async function extractConfigTypeFromSource ( filePath : string ) : Promise < string > {
@@ -105,113 +174,30 @@ async function extractIndexTypeFromSource(filePath: string): Promise<string> {
105
174
106
175
function formatDeclarations ( declarations : string , isConfigFile : boolean ) : string {
107
176
if ( isConfigFile ) {
108
- // Special formatting for config.d.ts
109
177
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'
113
181
}
114
182
115
- // Regular formatting for other files
116
183
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 ( / e x p o r t i n t e r f a c e ( [ ^ \{ ] + ) \{ / 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 (? = e x p o r t ( i n t e r f a c e | t y p e ) ) / g, '}\n' ) // Remove extra newline between interface/type declarations
126
- . replace ( / ^ ( i m p o r t .* \n ) + / m, match => match . trim ( ) + '\n' ) // Ensure imports are grouped
127
- . replace ( / # # # L I N E B R E A K # # # / 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 ( / e x p o r t i n t e r f a c e ( [ ^ \{ ] + ) \{ / g, 'export interface $1{ ' )
191
+ . replace ( / ^ ( \s * \w + : .* (?: \n | $ ) ) / gm, ' $1' )
192
+ . replace ( / } \n \n (? = \/ \* \* | e x p o r t ( i n t e r f a c e | t y p e ) ) / g, '}\n' )
193
+ . replace ( / ^ ( i m p o r t .* \n ) + / m, match => match . trim ( ) + '\n' )
194
+ . trim ( ) + '\n'
130
195
}
131
196
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 ( / \. t s $ / , '.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 )
181
200
}
182
201
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' ) )
217
203
}
0 commit comments