Skip to content

Commit e251852

Browse files
authoredSep 14, 2022
Update CLI documentation and add --typings and --files flags (#158)
1 parent c3d0949 commit e251852

File tree

6 files changed

+209
-53
lines changed

6 files changed

+209
-53
lines changed
 

‎readme.md

+93-41
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
## Install
66

77
```sh
8-
npm install tsd
8+
npm install --save-dev tsd
99
```
1010

1111
## Overview
@@ -14,6 +14,16 @@ This tool lets you write tests for your type definitions (i.e. your `.d.ts` file
1414

1515
These `.test-d.ts` files will not be executed, and not even compiled in the standard way. Instead, these files will be parsed for special constructs such as `expectError<Foo>(bar)` and then statically analyzed against your type definitions.
1616

17+
The `tsd` CLI will search for the main `.d.ts` file in the current or specified directory, and test it with any `.test-d.ts` files in either the same directory or a test sub-directory (default: `test-d`):
18+
19+
```sh
20+
[npx] tsd [path]
21+
```
22+
23+
Use `tsd --help` for usage information. See [Order of Operations](#order-of-operations) for more details on how `tsd` finds and executes tests.
24+
25+
*Note: the CLI is primarily used to test an entire project, not a specific file. For more specific configuration and advanced usage, see [Configuration](#configuration) and [Programmatic API](#programmatic-api).*
26+
1727
## Usage
1828

1929
Let's assume we wrote a `index.d.ts` type definition for our concat module.
@@ -106,9 +116,81 @@ expectType<string>(await concat('foo', 'bar'));
106116
expectError(await concat(true, false));
107117
```
108118

109-
### Test directory
119+
## Order of Operations
120+
121+
When searching for `.test-d.ts` files and executing them, `tsd` does the following:
122+
123+
1. Locates the project's `package.json`, which needs to be in the current or specified directory (e.g. `/path/to/project` or `process.cwd()`). Fails if none is found.
124+
125+
2. Finds a `.d.ts` file, checking to see if one was specified manually or in the `types` field of the `package.json`. If neither is found, attempts to find one in the project directory named the same as the `main` field of the `package.json` or `index.d.ts`. Fails if no `.d.ts` file is found.
126+
127+
3. Finds `.test-d.ts` and `.test-d.tsx` files, which can either be in the project's root directory, a [specific folder](#test-directory) (by default `/[project-root]/test-d`), or specified individually [programatically](#testfiles) or via [the CLI](#via-the-cli). Fails if no test files are found.
128+
129+
4. Runs the `.test-d.ts` files through the TypeScript compiler and statically analyzes them for errors.
130+
131+
5. Checks the errors against [assertions](#assertions) and reports any mismatches.
132+
133+
## Assertions
134+
135+
### expectType&lt;T&gt;(expression: T)
136+
137+
Asserts that the type of `expression` is identical to type `T`.
138+
139+
### expectNotType&lt;T&gt;(expression: any)
140+
141+
Asserts that the type of `expression` is not identical to type `T`.
142+
143+
### expectAssignable&lt;T&gt;(expression: T)
144+
145+
Asserts that the type of `expression` is assignable to type `T`.
146+
147+
### expectNotAssignable&lt;T&gt;(expression: any)
148+
149+
Asserts that the type of `expression` is not assignable to type `T`.
150+
151+
### expectError&lt;T = any&gt;(expression: T)
152+
153+
Asserts that `expression` throws an error.
154+
155+
### expectDeprecated(expression: any)
156+
157+
Asserts that `expression` is marked as [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
158+
159+
### expectNotDeprecated(expression: any)
160+
161+
Asserts that `expression` is not marked as [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
162+
163+
### printType(expression: any)
164+
165+
Prints the type of `expression` as a warning.
166+
167+
Useful if you don't know the exact type of the expression passed to `printType()` or the type is too complex to write out by hand.
168+
169+
### expectNever(expression: never)
170+
171+
Asserts that the type and return type of `expression` is `never`.
172+
173+
Useful for checking that all branches are covered.
174+
175+
### expectDocCommentIncludes&lt;T&gt;(expression: any)
176+
177+
Asserts that the documentation comment of `expression` includes string literal type `T`.
178+
179+
## Configuration
180+
181+
`tsd` is designed to be used with as little configuration as possible. However, if you need a bit more control, a project's `package.json` and the `tsd` CLI offer a limited set of configurations.
182+
183+
For more advanced use cases (such as integrating `tsd` with testing frameworks), see [Programmatic API](#programmatic-api).
184+
185+
### Via `package.json`
186+
187+
`tsd` uses a project's `package.json` to find types and test files as well as for some configuration. It must exist in the path given to `tsd`.
188+
189+
For more information on how `tsd` finds a `package.json`, see [Order of Operations](#order-of-operations).
110190

111-
When you have spread your tests over multiple files, you can store all those files in a test directory called `test-d`. If you want to use another directory name, you can change it in `package.json`.
191+
#### Test Directory
192+
193+
When you have spread your tests over multiple files, you can store all those files in a test directory called `test-d`. If you want to use another directory name, you can change it in your project's `package.json`:
112194

113195
```json
114196
{
@@ -121,7 +203,7 @@ When you have spread your tests over multiple files, you can store all those fil
121203

122204
Now you can put all your test files in the `my-test-dir` directory.
123205

124-
### Custom TypeScript config
206+
#### Custom TypeScript Config
125207

126208
By default, `tsd` applies the following configuration:
127209

@@ -157,51 +239,21 @@ These options will be overridden if a `tsconfig.json` file is found in your proj
157239

158240
*Default options will apply if you don't override them explicitly.* You can't override the `moduleResolution` option.
159241

160-
## Assertions
242+
### Via the CLI
161243

162-
### expectType&lt;T&gt;(expression: T)
244+
The `tsd` CLI is designed to test a whole project at once, and as such only offers a couple of flags for configuration.
163245

164246
Asserts that the type of `expression` is identical to type `T`.
165247

166-
### expectNotType&lt;T&gt;(expression: any)
248+
Alias: `-t`
167249

168-
Asserts that the type of `expression` is not identical to type `T`.
250+
Path to the type definition file you want to test. Same as [`typingsFile`](#typingsfile).
169251

170-
### expectAssignable&lt;T&gt;(expression: T)
252+
#### --files
171253

172-
Asserts that the type of `expression` is assignable to type `T`.
173-
174-
### expectNotAssignable&lt;T&gt;(expression: any)
175-
176-
Asserts that the type of `expression` is not assignable to type `T`.
177-
178-
### expectError&lt;T = any&gt;(expression: T)
254+
Alias: `-f`
179255

180-
Asserts that `expression` throws an error.
181-
182-
### expectDeprecated(expression: any)
183-
184-
Asserts that `expression` is marked as [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
185-
186-
### expectNotDeprecated(expression: any)
187-
188-
Asserts that `expression` is not marked as [`@deprecated`](https://jsdoc.app/tags-deprecated.html).
189-
190-
### printType(expression: any)
191-
192-
Prints the type of `expression` as a warning.
193-
194-
Useful if you don't know the exact type of the expression passed to `printType()` or the type is too complex to write out by hand.
195-
196-
### expectNever(expression: never)
197-
198-
Asserts that the type and return type of `expression` is `never`.
199-
200-
Useful for checking that all branches are covered.
201-
202-
### expectDocCommentIncludes&lt;T&gt;(expression: any)
203-
204-
Asserts that the documentation comment of `expression` includes string literal type `T`.
256+
An array of test files with their path. Same as [`testFiles`](#testfiles).
205257

206258
## Programmatic API
207259

‎source/cli.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,46 @@ const cli = meow(`
77
Usage
88
$ tsd [path]
99
10+
The given directory must contain a package.json and a typings file.
11+
12+
Info
13+
--help Display help text
14+
--version Display version info
15+
16+
Options
17+
--typings -t Type definition file to test [Default: "types" property in package.json]
18+
--files -f Glob of files to test [Default: '/path/test-d/**/*.test-d.ts' or '.tsx']
19+
1020
Examples
1121
$ tsd /path/to/project
1222
23+
$ tsd --files /test/some/folder/*.ts --files /test/other/folder/*.tsx
24+
1325
$ tsd
1426
1527
index.test-d.ts
1628
✖ 10:20 Argument of type string is not assignable to parameter of type number.
17-
`);
29+
`, {
30+
flags: {
31+
typings: {
32+
type: 'string',
33+
alias: 't',
34+
},
35+
files: {
36+
type: 'string',
37+
alias: 'f',
38+
isMultiple: true,
39+
},
40+
},
41+
});
1842

1943
(async () => {
2044
try {
21-
const options = cli.input.length > 0 ? {cwd: cli.input[0]} : undefined;
45+
const cwd = cli.input.length > 0 ? cli.input[0] : process.cwd();
46+
const typingsFile = cli.flags.typings;
47+
const testFiles = cli.flags.files;
48+
49+
const options = {cwd, typingsFile, testFiles};
2250

2351
const diagnostics = await tsd(options);
2452

‎source/lib/index.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ const findTypingsFile = async (pkg: PackageJsonWithTsdConfig, options: Options):
2020
pkg.typings ||
2121
(pkg.main && path.parse(pkg.main).name + '.d.ts') ||
2222
'index.d.ts';
23-
const typingsExist = await pathExists(path.join(options.cwd, typings));
23+
24+
const typingsPath = path.join(options.cwd, typings);
25+
const typingsExist = await pathExists(typingsPath);
2426

2527
if (!typingsExist) {
26-
throw new Error(`The type definition \`${typings}\` does not exist. Create one and try again.`);
28+
throw new Error(`The type definition \`${typings}\` does not exist at \`${typingsPath}\`. Is the path correct? Create one and try again.`);
2729
}
2830

2931
return typings;
@@ -41,7 +43,7 @@ const findCustomTestFiles = async (testFilesPattern: readonly string[], cwd: str
4143
const testFiles = await globby(testFilesPattern, {cwd});
4244

4345
if (testFiles.length === 0) {
44-
throw new Error('Could not find any test files. Create one and try again');
46+
throw new Error('Could not find any test files with the given pattern(s). Create one and try again.');
4547
}
4648

4749
return testFiles.map(file => path.join(cwd, file));
@@ -63,7 +65,7 @@ const findTestFiles = async (typingsFilePath: string, options: Options & {config
6365
const testDirExists = await pathExists(path.join(options.cwd, testDir));
6466

6567
if (testFiles.length === 0 && !testDirExists) {
66-
throw new Error(`The test file \`${testFile}\` or \`${tsxTestFile}\` does not exist. Create one and try again.`);
68+
throw new Error(`The test file \`${testFile}\` or \`${tsxTestFile}\` does not exist in \`${options.cwd}\`. Create one and try again.`);
6769
}
6870

6971
if (testFiles.length === 0) {
@@ -82,7 +84,7 @@ export default async (options: Options = {cwd: process.cwd()}): Promise<Diagnost
8284
const pkgResult = await readPkgUp({cwd: options.cwd});
8385

8486
if (!pkgResult) {
85-
throw new Error('No `package.json` file found. Make sure you are running the command in a Node.js project.');
87+
throw new Error(`No \`package.json\` file found in \`${options.cwd}\`. Make sure you are running the command in a Node.js project.`);
8688
}
8789

8890
const pkg = pkgResult.packageJson as PackageJsonWithTsdConfig;

‎source/test/cli.ts

+63
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'path';
22
import test from 'ava';
33
import execa from 'execa';
4+
import readPkgUp from 'read-pkg-up';
45

56
interface ExecaError extends Error {
67
readonly exitCode: number;
@@ -32,3 +33,65 @@ test('provide a path', async t => {
3233
t.is(exitCode, 1);
3334
t.regex(stderr, /5:19[ ]{2}Argument of type number is not assignable to parameter of type string./);
3435
});
36+
37+
test('cli help flag', async t => {
38+
const {exitCode} = await execa('dist/cli.js', ['--help']);
39+
40+
t.is(exitCode, 0);
41+
});
42+
43+
test('cli version flag', async t => {
44+
const pkg = readPkgUp.sync({normalize: false})?.packageJson ?? {};
45+
46+
const {exitCode, stdout} = await execa('dist/cli.js', ['--version']);
47+
48+
t.is(exitCode, 0);
49+
t.is(stdout, pkg.version);
50+
});
51+
52+
test('cli typings flag', async t => {
53+
const runTest = async (arg: '--typings' | '-t') => {
54+
const {exitCode, stderr} = await t.throwsAsync<ExecaError>(execa('../../../cli.js', [arg, 'utils/index.d.ts'], {
55+
cwd: path.join(__dirname, 'fixtures/typings-custom-dir')
56+
}));
57+
58+
t.is(exitCode, 1);
59+
t.true(stderr.includes('✖ 5:19 Argument of type number is not assignable to parameter of type string.'));
60+
};
61+
62+
await runTest('--typings');
63+
await runTest('-t');
64+
});
65+
66+
test('cli files flag', async t => {
67+
const runTest = async (arg: '--files' | '-f') => {
68+
const {exitCode, stderr} = await t.throwsAsync<ExecaError>(execa('../../../cli.js', [arg, 'unknown.test.ts'], {
69+
cwd: path.join(__dirname, 'fixtures/specify-test-files')
70+
}));
71+
72+
t.is(exitCode, 1);
73+
t.true(stderr.includes('✖ 5:19 Argument of type number is not assignable to parameter of type string.'));
74+
};
75+
76+
await runTest('--files');
77+
await runTest('-f');
78+
});
79+
80+
test('cli files flag array', async t => {
81+
const {exitCode, stderr} = await t.throwsAsync<ExecaError>(execa('../../../cli.js', ['--files', 'unknown.test.ts', '--files', 'second.test.ts'], {
82+
cwd: path.join(__dirname, 'fixtures/specify-test-files')
83+
}));
84+
85+
t.is(exitCode, 1);
86+
t.true(stderr.includes('✖ 5:19 Argument of type number is not assignable to parameter of type string.'));
87+
});
88+
89+
test('cli typings and files flags', async t => {
90+
const typingsFile = 'dist/test/fixtures/typings-custom-dir/utils/index.d.ts';
91+
const testFile = 'dist/test/fixtures/typings-custom-dir/index.test-d.ts';
92+
93+
const {exitCode, stderr} = t.throws<ExecaError>(() => execa.commandSync(`dist/cli.js -t ${typingsFile} -f ${testFile}`));
94+
95+
t.is(exitCode, 1);
96+
t.true(stderr.includes('✖ 5:19 Argument of type number is not assignable to parameter of type string.'));
97+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {expectType} from '../../..';
2+
import one from '.';
3+
4+
expectType<number>(one(1, 1));

‎source/test/test.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ import tsd from '..';
55
import {Diagnostic} from '../lib/interfaces';
66

77
test('throw if no type definition was found', async t => {
8-
await t.throwsAsync(tsd({cwd: path.join(__dirname, 'fixtures/no-tsd')}), {message: 'The type definition `index.d.ts` does not exist. Create one and try again.'});
8+
const cwd = path.join(__dirname, 'fixtures/no-tsd');
9+
const index = path.join(cwd, 'index.d.ts');
10+
11+
await t.throwsAsync(tsd({cwd}), {message: `The type definition \`index.d.ts\` does not exist at \`${index}\`. Is the path correct? Create one and try again.`});
912
});
1013

1114
test('throw if no test is found', async t => {
12-
await t.throwsAsync(tsd({cwd: path.join(__dirname, 'fixtures/no-test')}), {message: 'The test file `index.test-d.ts` or `index.test-d.tsx` does not exist. Create one and try again.'});
15+
const cwd = path.join(__dirname, 'fixtures/no-test');
16+
await t.throwsAsync(tsd({cwd}), {message: `The test file \`index.test-d.ts\` or \`index.test-d.tsx\` does not exist in \`${cwd}\`. Create one and try again.`});
1317
});
1418

1519
test('return diagnostics', async t => {
@@ -365,7 +369,8 @@ test('specify test files manually', async t => {
365369
const diagnostics = await tsd({
366370
cwd: path.join(__dirname, 'fixtures/specify-test-files'),
367371
testFiles: [
368-
'unknown.test.ts'
372+
'unknown.test.ts',
373+
'second.test.ts'
369374
]
370375
});
371376

@@ -375,12 +380,14 @@ test('specify test files manually', async t => {
375380
});
376381

377382
test('fails if typings file is not found in the specified path', async t => {
383+
const cwd = path.join(__dirname, 'fixtures/typings-custom-dir');
384+
378385
const error = await t.throwsAsync(tsd({
379-
cwd: path.join(__dirname, 'fixtures/typings-custom-dir'),
386+
cwd,
380387
typingsFile: 'unknown.d.ts'
381388
}));
382389

383-
t.is(error.message, 'The type definition `unknown.d.ts` does not exist. Create one and try again.');
390+
t.is(error.message, `The type definition \`unknown.d.ts\` does not exist at \`${path.join(cwd, 'unknown.d.ts')}\`. Is the path correct? Create one and try again.`);
384391
});
385392

386393
test('includes extended config files along with found ones', async t => {

0 commit comments

Comments
 (0)
Please sign in to comment.