11import type { Result } from 'neverthrow'
22import 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'
55import { err , ok } from 'neverthrow'
66import { 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 ( / \. 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 } )
1466}
1567
1668async 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 = / 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
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 * ) ? ( 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
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 ( / c o n s t \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 ( / ( 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`
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 = / 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
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 ( e x p o r t ) / , '\n###LINEBREAK###$1' ) . trim ( ) + '\n'
134+ return declarations . trim ( ) + '\n'
66135}
67136
68137async function extractConfigTypeFromSource ( filePath : string ) : Promise < string > {
@@ -105,113 +174,30 @@ async function extractIndexTypeFromSource(filePath: string): Promise<string> {
105174
106175function 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 ( / 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'
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 ( / \. 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 )
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}
0 commit comments