Skip to content

Commit a91e6e7

Browse files
committed
Automate creating index.d.ts
1 parent 331eccc commit a91e6e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+552
-965
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ coverage
99
.vscode
1010
package-lock.json
1111
*tsbuildinfo
12+
/types/index.d.ts

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,27 @@
33
"version": "5.0.3",
44
"description": "Additional Jest matchers",
55
"main": "dist/index.js",
6-
"types": "types/index.d.ts",
76
"files": [
87
"dist",
98
"types/index.d.ts",
109
"all.js"
1110
],
1211
"scripts": {
13-
"clean": "node clean.js",
12+
"clean": "node scripts/clean.js",
1413
"prebuild": "yarn clean",
1514
"build": "tsc && tsc-alias",
15+
"postbuild": "yarn generate-types",
16+
"generate-types": "ts-node scripts/generate-types.ts",
1617
"lint": "eslint .",
1718
"lint:fix": "yarn lint --fix",
1819
"prepare": "husky",
1920
"prepublishOnly": "yarn build",
2021
"precommit": "lint-staged",
22+
"pretest": "yarn generate-types",
2123
"test": "jest --color=true",
2224
"test:clearCache": "yarn test --clearCache",
2325
"test:updateSnapshot": "yarn test --updateSnapshot",
26+
"pretest:coverage": "yarn generate-types",
2427
"test:coverage": "yarn test --coverage",
2528
"test:watch": "yarn test --watch",
2629
"typecheck": "tsc --noEmit",
@@ -61,6 +64,7 @@
6164
"lint-staged": "~15.5.0",
6265
"prettier": "^3.0.0",
6366
"ts-jest": "^29.0.0",
67+
"ts-node": "^10.9.2",
6468
"tsc-alias": "^1.8.0",
6569
"typescript": "^5.0.0"
6670
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const fs = require('fs');
22
const path = require('path');
3-
const distPath = path.join(__dirname, 'dist');
3+
const distPath = path.join(__dirname, '..', 'dist');
44
if (fs.existsSync(distPath)) {
55
fs.rmSync(distPath, { recursive: true, force: true });
66
}

scripts/generate-types.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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();

src/matchers/fail.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* Note: Currently unimplemented
3+
* Failing assertion
4+
*
5+
* @param {String} message
6+
*/
17
export function fail(_: unknown, message: string) {
28
return {
39
pass: false,

src/matchers/pass.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* Note: Currently unimplemented
3+
* Passing assertion
4+
*
5+
* @param {String} message
6+
*/
17
export function pass(_: unknown, message: string) {
28
return {
39
pass: true,

src/matchers/toBeAfter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* Use `.toBeAfter` when checking if a date occurs after `date`.
3+
* @param {Date} after
4+
*/
15
export function toBeAfter(actual: unknown, after: Date) {
26
// @ts-expect-error OK to have implicit any for this.utils
37
const { printReceived, matcherHint } = this.utils;

src/matchers/toBeAfterOrEqualTo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/**
2+
* Use `.toBeAfterOrEqualTo` when checking if a date equals to or occurs after `date`.
3+
* @param {Date} date
4+
*/
15
export function toBeAfterOrEqualTo(actual: unknown, expected: Date) {
26
// @ts-expect-error OK to have implicit any for this.utils
37
const { printReceived, matcherHint } = this.utils;

src/matchers/toBeArray.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
export function toBeArray(expected: unknown) {
1+
/**
2+
* Use `.toBeArray` when checking if a value is an `Array`.
3+
*/
4+
export function toBeArray(actual: unknown) {
25
// @ts-expect-error OK to have implicit any for this.utils
36
const { matcherHint, printReceived } = this.utils;
47

5-
const pass = Array.isArray(expected);
8+
const pass = Array.isArray(actual);
69

710
return {
811
pass,
@@ -11,10 +14,10 @@ export function toBeArray(expected: unknown) {
1114
? matcherHint('.not.toBeArray', 'received', '') +
1215
'\n\n' +
1316
'Expected value to not be an array received:\n' +
14-
` ${printReceived(expected)}`
17+
` ${printReceived(actual)}`
1518
: matcherHint('.toBeArray', 'received', '') +
1619
'\n\n' +
1720
'Expected value to be an array received:\n' +
18-
` ${printReceived(expected)}`,
21+
` ${printReceived(actual)}`,
1922
};
2023
}

src/matchers/toBeArrayOfSize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { determinePropertyMessage } from 'src/utils';
22

3+
/**
4+
* Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
5+
* @param {Number} expected
6+
*/
37
export function toBeArrayOfSize(actual: unknown, expected: number) {
48
// @ts-expect-error OK to have implicit any for this.utils
59
const { printExpected, printReceived, matcherHint } = this.utils;

0 commit comments

Comments
 (0)