borp 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +35 -0
- package/README.md +98 -0
- package/borp.js +68 -64
- package/fixtures/fails/test/wrong.test.js +6 -0
- package/fixtures/only-src/src/add.test.ts +7 -0
- package/fixtures/only-src/src/add2.test.ts +7 -0
- package/{fixture → fixtures/only-src}/tsconfig.json +2 -1
- package/fixtures/src-to-dist/src/lib/add.ts +4 -0
- package/fixtures/src-to-dist/src/test/add.test.ts +7 -0
- package/fixtures/src-to-dist/src/test/add2.test.ts +7 -0
- package/fixtures/src-to-dist/tsconfig.json +24 -0
- package/fixtures/ts-cjs/package.json +3 -0
- package/fixtures/ts-cjs/src/add.ts +4 -0
- package/fixtures/ts-cjs/test/add2.test.ts +7 -0
- package/fixtures/ts-cjs/tsconfig.json +24 -0
- package/fixtures/ts-esm/src/add.ts +4 -0
- package/fixtures/ts-esm/test/add2.test.ts +7 -0
- package/fixtures/ts-esm/tsconfig.json +24 -0
- package/lib/run.js +135 -0
- package/package.json +8 -2
- package/test/basic.test.js +167 -0
- package/test/cli.test.js +25 -0
- package/test/coverage.test.js +36 -0
- package/test/watch.test.js +111 -0
- /package/{fixture → fixtures/only-src}/src/add.ts +0 -0
- /package/{fixture → fixtures/ts-cjs}/test/add.test.ts +0 -0
- /package/{fixture/test/add2.test.ts → fixtures/ts-esm/test/add.test.ts} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
paths-ignore:
|
|
6
|
+
- 'docs/**'
|
|
7
|
+
- '*.md'
|
|
8
|
+
pull_request:
|
|
9
|
+
paths-ignore:
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- '*.md'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ${{matrix.os}}
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [18.x, 20.x, 21.x]
|
|
20
|
+
os: [ubuntu-latest, windows-latest]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v3
|
|
23
|
+
|
|
24
|
+
- name: Use Node.js
|
|
25
|
+
uses: actions/setup-node@v2
|
|
26
|
+
with:
|
|
27
|
+
node-version: ${{ matrix.node-version }}
|
|
28
|
+
|
|
29
|
+
- name: Install
|
|
30
|
+
run: |
|
|
31
|
+
npm install
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: |
|
|
35
|
+
npm run test
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# borp
|
|
2
|
+
|
|
3
|
+
Borp is a typescript-aware runner for tests written using `node:test`.
|
|
4
|
+
It also support code coverage via [c8](http://npm.im/c8).
|
|
5
|
+
|
|
6
|
+
Borp is self-hosted, i.e. Borp runs its own tests.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i borp --save-dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
borp --coverage
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Borp will autumatically run all tests files matching `*.test.{js|ts}`.
|
|
21
|
+
|
|
22
|
+
### Example project setup
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
.
|
|
26
|
+
├── src
|
|
27
|
+
│ ├── lib
|
|
28
|
+
│ │ └── add.ts
|
|
29
|
+
│ └── test
|
|
30
|
+
│ └── add.test.ts
|
|
31
|
+
└── tsconfig.json
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
As an example, consider having a `src/lib/add.ts` file
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
export function add (x: number, y: number): number {
|
|
39
|
+
return x + y
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
and a `src/test/add.test.ts` file:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { test } from 'node:test'
|
|
47
|
+
import { add } from '../lib/add.js'
|
|
48
|
+
import { strictEqual } from 'node:assert'
|
|
49
|
+
|
|
50
|
+
test('add', () => {
|
|
51
|
+
strictEqual(add(1, 2), 3)
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
and the following `tsconfig.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
60
|
+
"compilerOptions": {
|
|
61
|
+
"outDir": "dist",
|
|
62
|
+
"sourceMap": true,
|
|
63
|
+
"target": "ES2022",
|
|
64
|
+
"module": "NodeNext",
|
|
65
|
+
"moduleResolution": "NodeNext",
|
|
66
|
+
"esModuleInterop": true,
|
|
67
|
+
"strict": true,
|
|
68
|
+
"resolveJsonModule": true,
|
|
69
|
+
"removeComments": true,
|
|
70
|
+
"newLine": "lf",
|
|
71
|
+
"noUnusedLocals": true,
|
|
72
|
+
"noFallthroughCasesInSwitch": true,
|
|
73
|
+
"isolatedModules": true,
|
|
74
|
+
"forceConsistentCasingInFileNames": true,
|
|
75
|
+
"skipLibCheck": true,
|
|
76
|
+
"lib": [
|
|
77
|
+
"ESNext"
|
|
78
|
+
],
|
|
79
|
+
"incremental": true
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note the use of `incremental: true`, which speed up compilation massively.
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
* `--coverage` or `-C`, enables code coverage
|
|
89
|
+
* `--only` or `-o`, only run `node:test` with the `only` option set
|
|
90
|
+
* `--watch` or `-w`, re-run tests on changes
|
|
91
|
+
* `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms
|
|
92
|
+
* `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default.
|
|
93
|
+
* `--ignore` or `-i`, ignore a glob pattern, and not look for tests there
|
|
94
|
+
* `--pattern` or `-p`, run tests matching the given glob pattern
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/borp.js
CHANGED
|
@@ -2,27 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from 'node:util'
|
|
4
4
|
import { tap, spec } from 'node:test/reporters'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
async function isFileAccessible (filename, directory) {
|
|
14
|
-
try {
|
|
15
|
-
const filePath = directory ? resolve(directory, filename) : filename
|
|
16
|
-
await access(filePath)
|
|
17
|
-
return true
|
|
18
|
-
} catch (err) {
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
}
|
|
5
|
+
import { mkdtemp, rm, readFile } from 'node:fs/promises'
|
|
6
|
+
import { finished } from 'node:stream/promises'
|
|
7
|
+
import { join, relative } from 'node:path'
|
|
8
|
+
import posix from 'node:path/posix'
|
|
9
|
+
import runWithTypeScript from './lib/run.js'
|
|
10
|
+
import { Report } from 'c8'
|
|
11
|
+
import os from 'node:os'
|
|
22
12
|
|
|
23
13
|
let reporter
|
|
14
|
+
/* c8 ignore next 4 */
|
|
24
15
|
if (process.stdout.isTTY) {
|
|
25
|
-
|
|
16
|
+
/* eslint new-cap: "off" */
|
|
17
|
+
reporter = new spec()
|
|
26
18
|
} else {
|
|
27
19
|
reporter = tap
|
|
28
20
|
}
|
|
@@ -33,65 +25,77 @@ const args = parseArgs({
|
|
|
33
25
|
only: { type: 'boolean', short: 'o' },
|
|
34
26
|
watch: { type: 'boolean', short: 'w' },
|
|
35
27
|
pattern: { type: 'string', short: 'p' },
|
|
36
|
-
concurrency: { type: 'string', short: 'c' }
|
|
28
|
+
concurrency: { type: 'string', short: 'c', default: os.availableParallelism() - 1 + '' },
|
|
29
|
+
coverage: { type: 'boolean', short: 'C' },
|
|
30
|
+
timeout: { type: 'string', short: 't', default: '30000' },
|
|
31
|
+
'coverage-exclude': { type: 'string', short: 'X', multiple: true },
|
|
32
|
+
ignore: { type: 'string', short: 'i', multiple: true },
|
|
33
|
+
help: { type: 'boolean', short: 'h' }
|
|
37
34
|
},
|
|
38
35
|
allowPositionals: true
|
|
39
36
|
})
|
|
40
37
|
|
|
38
|
+
/* c8 ignore next 4 */
|
|
39
|
+
if (args.values.help) {
|
|
40
|
+
console.log(await readFile(new URL('./README.md', import.meta.url), 'utf8'))
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
if (args.values.concurrency) {
|
|
42
45
|
args.values.concurrency = parseInt(args.values.concurrency)
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let prefix = ''
|
|
48
|
-
|
|
49
|
-
if (tsconfigPath) {
|
|
50
|
-
try {
|
|
51
|
-
const _require = createRequire(process.cwd())
|
|
52
|
-
const typescriptPathCWD = _require.resolve('typescript')
|
|
53
|
-
const tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
|
|
54
|
-
if (tscPath) {
|
|
55
|
-
const isAccessible = await isFileAccessible(tscPath)
|
|
56
|
-
if (isAccessible) {
|
|
57
|
-
await execa(tscPath, { cwd: dirname(tsconfigPath), stdio: 'inherit' })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const tsconfig = JSON.parse(await readFile(tsconfigPath))
|
|
61
|
-
const outDir = tsconfig.compilerOptions.outDir
|
|
62
|
-
if (outDir) {
|
|
63
|
-
prefix = join(dirname(tsconfigPath), outDir)
|
|
64
|
-
}
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.log(err)
|
|
67
|
-
}
|
|
48
|
+
if (args.values.timeout) {
|
|
49
|
+
args.values.timeout = parseInt(args.values.timeout)
|
|
68
50
|
}
|
|
69
51
|
|
|
70
|
-
let
|
|
71
|
-
if (args.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} else {
|
|
75
|
-
files = args.positionals
|
|
76
|
-
}
|
|
77
|
-
} else if (args.values.pattern) {
|
|
78
|
-
if (prefix) {
|
|
79
|
-
args.values.pattern = join(prefix, args.values.pattern)
|
|
80
|
-
}
|
|
81
|
-
files = await glob(args.values.pattern, { ignore: 'node_modules/**' })
|
|
82
|
-
} else {
|
|
83
|
-
if (prefix) {
|
|
84
|
-
files = await glob(join(prefix, 'test/**/*.test.{cjs,mjs,js}'), { ignore: 'node_modules/**' })
|
|
85
|
-
} else {
|
|
86
|
-
files = await glob('test/**/*.test.{cjs,mjs,js}', { ignore: 'node_modules/**' })
|
|
87
|
-
}
|
|
52
|
+
let covDir
|
|
53
|
+
if (args.values.coverage) {
|
|
54
|
+
covDir = await mkdtemp(join(os.tmpdir(), 'coverage-'))
|
|
55
|
+
process.env.NODE_V8_COVERAGE = covDir
|
|
88
56
|
}
|
|
89
57
|
|
|
90
58
|
const config = {
|
|
91
59
|
...args.values,
|
|
92
|
-
files
|
|
60
|
+
files: args.positionals,
|
|
61
|
+
pattern: args.values.pattern,
|
|
62
|
+
cwd: process.cwd()
|
|
93
63
|
}
|
|
94
64
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
65
|
+
try {
|
|
66
|
+
const stream = await runWithTypeScript(config)
|
|
67
|
+
|
|
68
|
+
stream.on('test:fail', () => {
|
|
69
|
+
process.exitCode = 1
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
stream.compose(reporter).pipe(process.stdout)
|
|
73
|
+
|
|
74
|
+
await finished(stream)
|
|
75
|
+
|
|
76
|
+
if (covDir) {
|
|
77
|
+
let exclude = args.values['coverage-exclude']
|
|
78
|
+
|
|
79
|
+
if (exclude && config.prefix) {
|
|
80
|
+
const localPrefix = relative(process.cwd(), config.prefix)
|
|
81
|
+
exclude = exclude.map((file) => posix.join(localPrefix, file))
|
|
82
|
+
}
|
|
83
|
+
const report = Report({
|
|
84
|
+
reporter: ['text'],
|
|
85
|
+
tempDirectory: covDir,
|
|
86
|
+
exclude
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await report.run()
|
|
90
|
+
}
|
|
91
|
+
/* c8 ignore next 3 */
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(err)
|
|
94
|
+
} finally {
|
|
95
|
+
if (covDir) {
|
|
96
|
+
try {
|
|
97
|
+
await rm(covDir, { recursive: true, maxRetries: 10, retryDelay: 100 })
|
|
98
|
+
/* c8 ignore next 2 */
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"newLine": "lf",
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"lib": [
|
|
20
|
+
"ESNext"
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"newLine": "lf",
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"lib": [
|
|
20
|
+
"ESNext"
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "NodeNext",
|
|
8
|
+
"moduleResolution": "NodeNext",
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"newLine": "lf",
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"lib": [
|
|
20
|
+
"ESNext"
|
|
21
|
+
],
|
|
22
|
+
"incremental": true
|
|
23
|
+
}
|
|
24
|
+
}
|
package/lib/run.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { run } from 'node:test'
|
|
2
|
+
import { glob } from 'glob'
|
|
3
|
+
import { findUp } from 'find-up'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { access, readFile } from 'node:fs/promises'
|
|
7
|
+
import { execa } from 'execa'
|
|
8
|
+
|
|
9
|
+
function deferred () {
|
|
10
|
+
let resolve
|
|
11
|
+
let reject
|
|
12
|
+
const promise = new Promise((_resolve, _reject) => {
|
|
13
|
+
resolve = _resolve
|
|
14
|
+
reject = _reject
|
|
15
|
+
})
|
|
16
|
+
return { resolve, reject, promise }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async function runWithTypeScript (config) {
|
|
20
|
+
const { cwd } = config
|
|
21
|
+
let pushable = []
|
|
22
|
+
const tsconfigPath = await findUp('tsconfig.json', { cwd })
|
|
23
|
+
|
|
24
|
+
let prefix = ''
|
|
25
|
+
let tscPath
|
|
26
|
+
|
|
27
|
+
if (tsconfigPath) {
|
|
28
|
+
const _require = createRequire(cwd)
|
|
29
|
+
const typescriptPathCWD = _require.resolve('typescript')
|
|
30
|
+
tscPath = join(typescriptPathCWD, '..', '..', 'bin', 'tsc')
|
|
31
|
+
if (tscPath) {
|
|
32
|
+
// This will throw if we cannot find the `tsc` binary
|
|
33
|
+
await access(tscPath)
|
|
34
|
+
|
|
35
|
+
// Watch is handled aftterwards
|
|
36
|
+
if (!config.watch) {
|
|
37
|
+
const start = Date.now()
|
|
38
|
+
await execa('node', [tscPath], { cwd: dirname(tsconfigPath) })
|
|
39
|
+
pushable.push({
|
|
40
|
+
type: 'test:diagnostic',
|
|
41
|
+
data: {
|
|
42
|
+
nesting: 0,
|
|
43
|
+
message: `TypeScript compilation complete (${Date.now() - start}ms)`
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const tsconfig = JSON.parse(await readFile(tsconfigPath))
|
|
49
|
+
const outDir = tsconfig.compilerOptions.outDir
|
|
50
|
+
if (outDir) {
|
|
51
|
+
prefix = join(dirname(tsconfigPath), outDir)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
config.prefix = prefix
|
|
55
|
+
config.setup = (test) => {
|
|
56
|
+
for (const chunk of pushable) {
|
|
57
|
+
test.reporter.push(chunk)
|
|
58
|
+
}
|
|
59
|
+
pushable = test.reporter
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let tscChild
|
|
63
|
+
/* eslint prefer-const: "off" */
|
|
64
|
+
let stream
|
|
65
|
+
let p
|
|
66
|
+
|
|
67
|
+
if (config.watch) {
|
|
68
|
+
p = deferred()
|
|
69
|
+
const start = Date.now()
|
|
70
|
+
tscChild = execa('node', [tscPath, '--watch'], { cwd })
|
|
71
|
+
tscChild.stdout.setEncoding('utf8')
|
|
72
|
+
tscChild.stdout.on('data', (data) => {
|
|
73
|
+
if (data.includes('Watching for file changes')) {
|
|
74
|
+
pushable.push({
|
|
75
|
+
type: 'test:diagnostic',
|
|
76
|
+
data: {
|
|
77
|
+
nesting: 0,
|
|
78
|
+
message: `TypeScript compilation complete (${Date.now() - start}ms)`
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
p.resolve()
|
|
83
|
+
}
|
|
84
|
+
if (data.includes('error TS')) {
|
|
85
|
+
pushable.push({
|
|
86
|
+
type: 'test:fail',
|
|
87
|
+
data: {
|
|
88
|
+
nesting: 0,
|
|
89
|
+
name: data.trim()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
if (config.signal) {
|
|
95
|
+
config.signal.addEventListener('abort', () => {
|
|
96
|
+
tscChild.kill()
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (p) {
|
|
102
|
+
await p.promise
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let files = config.files || []
|
|
106
|
+
const ignore = config.ignore || []
|
|
107
|
+
ignore.unshift('node_modules/**/*')
|
|
108
|
+
if (files.length > 0) {
|
|
109
|
+
if (prefix) {
|
|
110
|
+
files = files.map((file) => join(prefix, file.replace(/ts$/, 'js')))
|
|
111
|
+
}
|
|
112
|
+
} else if (config.pattern) {
|
|
113
|
+
let pattern = config.pattern
|
|
114
|
+
if (prefix) {
|
|
115
|
+
pattern = join(prefix, pattern)
|
|
116
|
+
pattern = pattern.replace(/ts$/, 'js')
|
|
117
|
+
}
|
|
118
|
+
files = await glob(pattern, { ignore, cwd, windowsPathsNoEscape: true })
|
|
119
|
+
} else if (prefix) {
|
|
120
|
+
files = await glob(join(prefix, join('**', '*.test.{cjs,mjs,js}')), { ignore, cwd, windowsPathsNoEscape: true })
|
|
121
|
+
} else {
|
|
122
|
+
files = await glob(join('**', '*.test.{cjs,mjs,js}'), { ignore, cwd, windowsPathsNoEscape: true })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
config.files = files
|
|
126
|
+
|
|
127
|
+
stream = run(config)
|
|
128
|
+
|
|
129
|
+
stream.on('close', () => {
|
|
130
|
+
if (tscChild) {
|
|
131
|
+
tscChild.kill()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
return stream
|
|
135
|
+
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "node:test wrapper with TypeScript support",
|
|
6
6
|
"main": "borp.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"clean": "rm -rf fixtures/*/dist .test-*",
|
|
9
|
+
"lint": "standard | snazzy",
|
|
10
|
+
"unit": "node borp.js --ignore \"fixtures/**/*\" --coverage --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test/**/*\"",
|
|
11
|
+
"test": "npm run clean ; npm run lint && npm run unit"
|
|
9
12
|
},
|
|
10
13
|
"keywords": [],
|
|
11
14
|
"author": "Matteo Collina <hello@matteocollina.com>",
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"devDependencies": {
|
|
17
|
+
"@matteo.collina/tspl": "^0.1.0",
|
|
14
18
|
"@types/node": "^20.10.0",
|
|
19
|
+
"desm": "^1.3.0",
|
|
15
20
|
"snazzy": "^9.0.0",
|
|
16
21
|
"standard": "^17.1.0",
|
|
17
22
|
"typescript": "^5.3.2"
|
|
18
23
|
},
|
|
19
24
|
"dependencies": {
|
|
25
|
+
"c8": "^8.0.1",
|
|
20
26
|
"execa": "^8.0.1",
|
|
21
27
|
"find-up": "^7.0.0",
|
|
22
28
|
"glob": "^10.3.10"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { tspl } from '@matteo.collina/tspl'
|
|
3
|
+
import runWithTypeScript from '../lib/run.js'
|
|
4
|
+
import { join } from 'desm'
|
|
5
|
+
|
|
6
|
+
test('ts-esm', async (t) => {
|
|
7
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
8
|
+
const config = {
|
|
9
|
+
files: [],
|
|
10
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const stream = await runWithTypeScript(config)
|
|
14
|
+
|
|
15
|
+
const names = new Set(['add', 'add2'])
|
|
16
|
+
|
|
17
|
+
stream.once('data', (test) => {
|
|
18
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
19
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
stream.on('test:pass', (test) => {
|
|
23
|
+
strictEqual(names.has(test.name), true)
|
|
24
|
+
names.delete(test.name)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await completed
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ts-cjs', async (t) => {
|
|
31
|
+
const { strictEqual, completed } = tspl(t, { plan: 2 })
|
|
32
|
+
const config = {
|
|
33
|
+
files: [],
|
|
34
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-cjs')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stream = await runWithTypeScript(config)
|
|
38
|
+
|
|
39
|
+
const names = new Set(['add', 'add2'])
|
|
40
|
+
|
|
41
|
+
stream.on('test:pass', (test) => {
|
|
42
|
+
strictEqual(names.has(test.name), true)
|
|
43
|
+
names.delete(test.name)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await completed
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('ts-esm with named failes', async (t) => {
|
|
50
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
51
|
+
const config = {
|
|
52
|
+
files: ['test/add.test.ts'],
|
|
53
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const stream = await runWithTypeScript(config)
|
|
57
|
+
|
|
58
|
+
const names = new Set(['add'])
|
|
59
|
+
|
|
60
|
+
stream.once('data', (test) => {
|
|
61
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
62
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
stream.on('test:pass', (test) => {
|
|
66
|
+
strictEqual(names.has(test.name), true)
|
|
67
|
+
names.delete(test.name)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await completed
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('pattern', async (t) => {
|
|
74
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
75
|
+
const config = {
|
|
76
|
+
files: [],
|
|
77
|
+
pattern: 'test/*2.test.ts',
|
|
78
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stream = await runWithTypeScript(config)
|
|
82
|
+
|
|
83
|
+
const names = new Set(['add2'])
|
|
84
|
+
|
|
85
|
+
stream.once('data', (test) => {
|
|
86
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
87
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
stream.on('test:pass', (test) => {
|
|
91
|
+
strictEqual(names.has(test.name), true)
|
|
92
|
+
names.delete(test.name)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await completed
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('no files', async (t) => {
|
|
99
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
100
|
+
const config = {
|
|
101
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stream = await runWithTypeScript(config)
|
|
105
|
+
|
|
106
|
+
const names = new Set(['add', 'add2'])
|
|
107
|
+
|
|
108
|
+
stream.once('data', (test) => {
|
|
109
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
110
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
stream.on('test:pass', (test) => {
|
|
114
|
+
strictEqual(names.has(test.name), true)
|
|
115
|
+
names.delete(test.name)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await completed
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('src-to-dist', async (t) => {
|
|
122
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
123
|
+
const config = {
|
|
124
|
+
files: [],
|
|
125
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'src-to-dist')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stream = await runWithTypeScript(config)
|
|
129
|
+
|
|
130
|
+
const names = new Set(['add', 'add2'])
|
|
131
|
+
|
|
132
|
+
stream.once('data', (test) => {
|
|
133
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
134
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
stream.on('test:pass', (test) => {
|
|
138
|
+
strictEqual(names.has(test.name), true)
|
|
139
|
+
names.delete(test.name)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await completed
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('only-src', async (t) => {
|
|
146
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 4 })
|
|
147
|
+
const config = {
|
|
148
|
+
files: [],
|
|
149
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'only-src')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const stream = await runWithTypeScript(config)
|
|
153
|
+
|
|
154
|
+
const names = new Set(['add', 'add2'])
|
|
155
|
+
|
|
156
|
+
stream.once('data', (test) => {
|
|
157
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
158
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
stream.on('test:pass', (test) => {
|
|
162
|
+
strictEqual(names.has(test.name), true)
|
|
163
|
+
names.delete(test.name)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await completed
|
|
167
|
+
})
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { execa } from 'execa'
|
|
3
|
+
import { join } from 'desm'
|
|
4
|
+
import { rejects } from 'node:assert'
|
|
5
|
+
|
|
6
|
+
const borp = join(import.meta.url, '..', 'borp.js')
|
|
7
|
+
|
|
8
|
+
test('limit concurrency', async () => {
|
|
9
|
+
await execa('node', [
|
|
10
|
+
borp,
|
|
11
|
+
'--concurrency',
|
|
12
|
+
'1'
|
|
13
|
+
], {
|
|
14
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('failing test set correct status code', async () => {
|
|
19
|
+
// execa rejects if status code is not 0
|
|
20
|
+
await rejects(execa('node', [
|
|
21
|
+
borp
|
|
22
|
+
], {
|
|
23
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'fails')
|
|
24
|
+
}))
|
|
25
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { match, doesNotMatch } from 'node:assert'
|
|
3
|
+
import { execa } from 'execa'
|
|
4
|
+
import { join } from 'desm'
|
|
5
|
+
|
|
6
|
+
const borp = join(import.meta.url, '..', 'borp.js')
|
|
7
|
+
|
|
8
|
+
test('coverage', async () => {
|
|
9
|
+
const res = await execa('node', [
|
|
10
|
+
borp,
|
|
11
|
+
'--coverage'
|
|
12
|
+
], {
|
|
13
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
match(res.stdout, /% Stmts/)
|
|
17
|
+
match(res.stdout, /All files/)
|
|
18
|
+
match(res.stdout, /add\.ts/)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('coverage excludes', async () => {
|
|
22
|
+
const res = await execa('node', [
|
|
23
|
+
borp,
|
|
24
|
+
'--coverage',
|
|
25
|
+
'--coverage-exclude=src'
|
|
26
|
+
], {
|
|
27
|
+
cwd: join(import.meta.url, '..', 'fixtures', 'ts-esm')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
match(res.stdout, /% Stmts/)
|
|
31
|
+
match(res.stdout, /All files/)
|
|
32
|
+
doesNotMatch(res.stdout, /add\.ts/)
|
|
33
|
+
// The test files are shown
|
|
34
|
+
match(res.stdout, /add\.test\.ts/)
|
|
35
|
+
match(res.stdout, /add2\.test\.ts/)
|
|
36
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import { tspl } from '@matteo.collina/tspl'
|
|
3
|
+
import runWithTypeScript from '../lib/run.js'
|
|
4
|
+
import { join } from 'desm'
|
|
5
|
+
import { mkdtemp, cp, writeFile, rm } from 'node:fs/promises'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { once } from 'node:events'
|
|
8
|
+
|
|
9
|
+
test('watch', async (t) => {
|
|
10
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
11
|
+
|
|
12
|
+
const dir = path.resolve(await mkdtemp('.test-watch'))
|
|
13
|
+
await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
|
|
14
|
+
recursive: true
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const controller = new AbortController()
|
|
18
|
+
t.after(async () => {
|
|
19
|
+
controller.abort()
|
|
20
|
+
try {
|
|
21
|
+
await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
|
|
22
|
+
} catch {}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const config = {
|
|
26
|
+
files: [],
|
|
27
|
+
cwd: dir,
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
watch: true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stream = await runWithTypeScript(config)
|
|
33
|
+
|
|
34
|
+
const fn = (test) => {
|
|
35
|
+
if (test.type === 'test:fail') {
|
|
36
|
+
strictEqual(test.data.name, 'add')
|
|
37
|
+
stream.removeListener('data', fn)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
stream.on('data', fn)
|
|
41
|
+
|
|
42
|
+
const [test] = await once(stream, 'data')
|
|
43
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
44
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
45
|
+
|
|
46
|
+
const toWrite = `
|
|
47
|
+
import { test } from 'node:test'
|
|
48
|
+
import { add } from '../src/add.js'
|
|
49
|
+
import { strictEqual } from 'node:assert'
|
|
50
|
+
|
|
51
|
+
test('add', () => {
|
|
52
|
+
strictEqual(add(1, 2), 4)
|
|
53
|
+
})
|
|
54
|
+
`
|
|
55
|
+
const file = path.join(dir, 'test', 'add.test.ts')
|
|
56
|
+
await writeFile(file, toWrite)
|
|
57
|
+
|
|
58
|
+
await completed
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('watch file syntax error', async (t) => {
|
|
62
|
+
const { strictEqual, completed, match } = tspl(t, { plan: 3 })
|
|
63
|
+
|
|
64
|
+
const dir = path.resolve(await mkdtemp('.test-watch'))
|
|
65
|
+
await cp(join(import.meta.url, '..', 'fixtures', 'ts-esm'), dir, {
|
|
66
|
+
recursive: true
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
t.after(async () => {
|
|
71
|
+
controller.abort()
|
|
72
|
+
try {
|
|
73
|
+
await rm(dir, { recursive: true, retryDelay: 100, maxRetries: 10 })
|
|
74
|
+
} catch {}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const config = {
|
|
78
|
+
files: [],
|
|
79
|
+
cwd: dir,
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
watch: true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const stream = await runWithTypeScript(config)
|
|
85
|
+
|
|
86
|
+
const fn = (test) => {
|
|
87
|
+
if (test.type === 'test:fail') {
|
|
88
|
+
match(test.data.name, /add\.test\.ts/)
|
|
89
|
+
stream.removeListener('data', fn)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
stream.on('data', fn)
|
|
93
|
+
|
|
94
|
+
const [test] = await once(stream, 'data')
|
|
95
|
+
strictEqual(test.type, 'test:diagnostic')
|
|
96
|
+
match(test.data.message, /TypeScript compilation complete \(\d+ms\)/)
|
|
97
|
+
|
|
98
|
+
const toWrite = `
|
|
99
|
+
import { test } from 'node:test'
|
|
100
|
+
import { add } from '../src/add.js'
|
|
101
|
+
import { strictEqual } from 'node:assert'
|
|
102
|
+
|
|
103
|
+
test('add', () => {
|
|
104
|
+
strictEqual(add(1, 2), 3
|
|
105
|
+
})
|
|
106
|
+
`
|
|
107
|
+
const file = path.join(dir, 'test', 'add.test.ts')
|
|
108
|
+
await writeFile(file, toWrite)
|
|
109
|
+
|
|
110
|
+
await completed
|
|
111
|
+
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|