diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93f18c1..b261a9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: - windows-latest node-version: - 18 + - 19 name: test (${{ matrix.os }}) - Node.js ${{ matrix.node-version }} runs-on: ${{ matrix.os }} steps: diff --git a/README.md b/README.md index 94326a0..87d967e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ You need to have `typescript` installed as a (dev) dependency and must be using Imagine it like `ts-node --test`, if that command existed. -### How to use +## Usage Install as a dev dependency: @@ -29,9 +29,10 @@ The command syntax is similar to `node --test`. Multiple paths can be passed. Di for any files with supported extensions (currently: `.js`, `.mjs`, `.cjs`; `.ts`, `.mts`, `.cts`). Then, Node's test runner will be started on all files that were found in this process. -You can also override the list of extensions by setting an environment variable (`TEST_EXTENSIONS`). -Then, the default list of extensions will not be used during discovery and instead only listed extensions will be included. -For example: +### Extensions + +You can override the list of extensions by setting an environment variable (`TEST_EXTENSIONS`). This list will then be +used instead of the default extensions. For example: ``` TEST_EXTENSIONS=.test.ts,.test.js ts-node-test test/ @@ -39,7 +40,17 @@ TEST_EXTENSIONS=.test.ts,.test.js ts-node-test test/ The above will recursively look for files in the `test/` directory ending in `.test.ts` or `.test.js`. -### Why this is needed +### Watch mode + +On Node.js versions that support watch mode in conjunction with the test runner, you can use it by passing the `--watch` +argument. At time of writing, this is only possible on Node.js 19, but will likely be backported to Node.js 18 in the +future. + +``` +ts-node-test --watch test/ +``` + +## Why this is needed TL;DR: Node.js (at the time of writing) does not allow to override the list of extensions that are used when searching for test files. The official recommendation is to list all files explicitly. That is precisely what this CLI wrapper diff --git a/package-lock.json b/package-lock.json index 6933765..d4daa55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.2.0", "license": "MIT", "dependencies": { - "ts-node": "10.9.1" + "ts-node": "10.9.1", + "yargs": "17.6.2" }, "bin": { "ts-node-test": "dist/bin.js" @@ -18,6 +19,7 @@ "@meyfa/eslint-config": "2.1.2", "@types/node": "18.11.17", "@types/sinon": "10.0.13", + "@types/yargs": "17.0.17", "eslint": "8.30.0", "rimraf": "3.0.2", "sinon": "15.0.1", @@ -273,6 +275,21 @@ "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", + "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.43.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz", @@ -508,7 +525,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -517,7 +533,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -660,11 +675,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -675,8 +702,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -775,6 +801,11 @@ "node": ">=6.0.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/es-abstract": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", @@ -839,6 +870,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1431,6 +1470,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -1743,6 +1790,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2330,6 +2385,14 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -2495,6 +2558,19 @@ "node": ">=8" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -2527,7 +2603,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2795,18 +2870,67 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 8369ab2..6eb4fc5 100644 --- a/package.json +++ b/package.json @@ -43,13 +43,15 @@ "@meyfa/eslint-config": "2.1.2", "@types/node": "18.11.17", "@types/sinon": "10.0.13", + "@types/yargs": "17.0.17", "eslint": "8.30.0", "rimraf": "3.0.2", "sinon": "15.0.1", "typescript": "4.9.4" }, "dependencies": { - "ts-node": "10.9.1" + "ts-node": "10.9.1", + "yargs": "17.6.2" }, "peerDependencies": { "typescript": "^4.0.0" diff --git a/src/bin.ts b/src/bin.ts index ee321a5..36082d4 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,5 +1,21 @@ #!/usr/bin/env node import { main } from './index.js' +import yargs from 'yargs' -await main() +const parsedArgs = yargs(process.argv.slice(2)).command('* ', 'Run tests', (yargs) => { + return yargs.options({ + watch: { + type: 'boolean', + default: false, + description: 'Run in watch mode' + } + }).positional('paths', { + type: 'string', + array: true, + demandOption: true, + description: 'Paths to test files' + }) +}).parseSync() + +await main(parsedArgs.paths, parsedArgs) diff --git a/src/index.ts b/src/index.ts index 28574d0..e6da5f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,16 +12,20 @@ export function getTestExtensions (): string[] { return DEFAULT_TEST_EXTENSIONS } -export async function main (): Promise { +export interface Options { + watch: boolean +} + +export async function main (paths: string[], options: Options): Promise { const require = createRequire(import.meta.url) const esmLoader = pathToFileURL(require.resolve('ts-node/esm')).toString() const extensions = getTestExtensions() - const resolvedPaths = await resolveTestPaths(process.argv.slice(2), extensions) + const resolvedPaths = await resolveTestPaths(paths, extensions) if (resolvedPaths.length === 0) { throw new Error('no test files found') } - spawnChild(esmLoader, resolvedPaths) + spawnChild(esmLoader, resolvedPaths, options) } /** @@ -29,21 +33,18 @@ export async function main (): Promise { * * @param loader The loader to use (fully resolved path to the loader script). * @param resolvedTestPaths The files under test. + * @param options Additional options. */ -function spawnChild (loader: string, resolvedTestPaths: string[]): void { - const child = spawn( - process.execPath, - [ - '--loader', - loader, - '--test', - ...resolvedTestPaths - ], - { - stdio: 'inherit', - argv0: process.argv0 - } - ) +function spawnChild (loader: string, resolvedTestPaths: string[], options: Options): void { + const args = ['--loader', loader, '--test'] + if (options.watch) { + args.push('--watch') + } + args.push(...resolvedTestPaths) + const child = spawn(process.execPath, args, { + stdio: 'inherit', + argv0: process.argv0 + }) child.on('error', (error) => { console.error(error) process.exit(1)