1+ import * as ts from 'typescript' ;
2+ import * as fs from 'fs' ;
3+ import * as path from 'path' ;
4+
5+ interface MatcherInfo {
6+ name : string ;
7+ docComment : string ;
8+ parameters : {
9+ name : string ;
10+ type : string ;
11+ optional ?: boolean ;
12+ rest ?: boolean ;
13+ } [ ] ;
14+ returnType : string ;
15+ }
16+
17+ function extractMatcherInfo ( sourceFile : ts . SourceFile ) : MatcherInfo [ ] {
18+ const matchers : MatcherInfo [ ] = [ ] ;
19+ ts . forEachChild ( sourceFile , node => {
20+ if ( ts . isFunctionDeclaration ( node ) && node . name ) {
21+ const matcherName = node . name . text ;
22+ // Extract the full JSDoc block (not just tags)
23+ let docComment = '' ;
24+ const jsDocs = ( node as any ) . jsDoc ;
25+ if ( jsDocs && jsDocs . length > 0 && jsDocs [ 0 ] . comment ) {
26+ docComment = jsDocs [ 0 ] . comment ;
27+ // If there are tags, add them as well
28+ if ( jsDocs [ 0 ] . tags && jsDocs [ 0 ] . tags . length > 0 ) {
29+ const tags = jsDocs [ 0 ] . tags . map ( ( tag : any ) => {
30+ let tagLine = `@${ tag . tagName . escapedText } ` ;
31+ if ( tag . typeExpression && tag . typeExpression . type ) {
32+ tagLine += ` {${ tag . typeExpression . type . getText ( ) } }` ;
33+ }
34+ if ( tag . name ) tagLine += ` ${ tag . name . escapedText } ` ;
35+ if ( tag . comment ) tagLine += ` ${ tag . comment } ` ;
36+ return tagLine ;
37+ } ) ;
38+ docComment += '\n' + tags . join ( '\n' ) ;
39+ }
40+ }
41+ // Skip the first parameter (actual) as it's implicit in Jest matchers
42+ const parameters = node . parameters . slice ( 1 ) . map ( param => {
43+ const paramName = param . name . getText ( sourceFile ) ;
44+ const paramType = param . type ? param . type . getText ( sourceFile ) : 'unknown' ;
45+ // Check if parameter is optional (has default value or is marked with ?)
46+ const isOptional = param . initializer !== undefined || param . questionToken !== undefined ;
47+ // Check if parameter is a rest parameter
48+ const isRest = param . dotDotDotToken !== undefined ;
49+ return {
50+ name : paramName ,
51+ type : paramType ,
52+ optional : isOptional ,
53+ rest : isRest
54+ } ;
55+ } ) ;
56+ const returnType = node . type ? node . type . getText ( sourceFile ) : 'unknown' ;
57+ matchers . push ( { name : matcherName , docComment, parameters, returnType } ) ;
58+ }
59+ } ) ;
60+ return matchers ;
61+ }
62+
63+ function generateTypeDefinition ( matcher : MatcherInfo ) : string {
64+ // Split docComment into lines, trim, and wrap in JSDoc
65+ let docBlock = '' ;
66+ if ( matcher . docComment && matcher . docComment . trim ( ) . length > 0 ) {
67+ const lines = matcher . docComment . split ( '\n' ) . map ( line => ` * ${ line . trim ( ) } ` ) ;
68+ docBlock = [ ' /**' , ...lines , ' */' ] . join ( '\n' ) ;
69+ }
70+ const params = matcher . parameters . map ( p => {
71+ const prefix = p . rest ? '...' : '' ;
72+ const suffix = p . optional ? '?' : '' ;
73+ return `${ prefix } ${ p . name } ${ suffix } : ${ p . type } ` ;
74+ } ) . join ( ', ' ) ;
75+
76+ // Check if the function uses the E type parameter
77+ const needsGenericE = matcher . parameters . some ( p => p . type . includes ( 'E' ) ) ||
78+ matcher . returnType . includes ( 'E' ) ;
79+
80+ // Add generic type parameter if needed
81+ const genericParams = needsGenericE ? '<E>' : '' ;
82+
83+ // Add two newlines after each method for clarity
84+ return `\n${ docBlock } \n ${ matcher . name } ${ genericParams } (${ params } ): R;\n` ;
85+ }
86+
87+ function generateTypeFile ( matchers : MatcherInfo [ ] ) : string {
88+ return `interface CustomMatchers<R> extends Record<string, any> {${ matchers
89+ . map ( generateTypeDefinition )
90+ . join ( '' ) }
91+ }
92+
93+ declare namespace jest {
94+ interface Matchers<R> {${ matchers
95+ . map ( generateTypeDefinition )
96+ . join ( '' ) }
97+ }
98+
99+ interface Expect extends CustomMatchers<any> {}
100+ interface InverseAsymmetricMatchers extends Expect {}
101+ }
102+
103+ declare module 'jest-extended' {
104+ const matchers: CustomMatchers<any>;
105+ export = matchers;
106+ }` ;
107+ }
108+
109+ function main ( ) {
110+ const matchersDir = path . join ( __dirname , '../src/matchers' ) ;
111+ const typesDir = path . join ( __dirname , '../types' ) ;
112+ const outputFile = path . join ( typesDir , 'index.d.ts' ) ;
113+
114+ // Read all matcher files
115+ const matcherFiles = fs . readdirSync ( matchersDir )
116+ . filter ( file => file . endsWith ( '.ts' ) && file !== 'index.ts' ) ;
117+
118+ const matchers : MatcherInfo [ ] = [ ] ;
119+
120+ // Process each matcher file
121+ for ( const file of matcherFiles ) {
122+ const filePath = path . join ( matchersDir , file ) ;
123+ const sourceFile = ts . createSourceFile (
124+ filePath ,
125+ fs . readFileSync ( filePath , 'utf8' ) . replace ( / \r / g, '' ) ,
126+ ts . ScriptTarget . Latest ,
127+ true
128+ ) ;
129+
130+ const matcherInfos = extractMatcherInfo ( sourceFile ) ;
131+ matchers . push ( ...matcherInfos ) ;
132+ }
133+
134+ // Generate and write type definitions
135+ const typeDefinitions = generateTypeFile ( matchers ) ;
136+ fs . mkdirSync ( typesDir , { recursive : true } ) ;
137+ fs . writeFileSync ( outputFile , typeDefinitions ) ;
138+ console . log ( `Generated type definitions in ${ outputFile } ` ) ;
139+ }
140+
141+ main ( ) ;
0 commit comments