borp 0.19.0 → 0.20.1

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/CLAUDE.md ADDED
@@ -0,0 +1,61 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Borp is a TypeScript-aware test runner for `node:test` with built-in code coverage support via c8. It's self-hosted and uses ESM modules throughout.
8
+
9
+ ## Development Commands
10
+
11
+ ### Primary Commands
12
+ - `npm test` - Run complete test suite (clean, lint, and unit tests)
13
+ - `npm run unit` - Run unit tests with coverage, excluding fixtures
14
+ - `npm run lint` - Run standard linter with snazzy formatter
15
+ - `npm run clean` - Remove build artifacts and test directories
16
+
17
+ ### Running Individual Tests
18
+ - Use borp directly: `node borp.js [options] [test-files]`
19
+ - With coverage: `node borp.js --coverage`
20
+ - Single test file: `node borp.js test/basic.test.js`
21
+
22
+ ## Architecture
23
+
24
+ ### Core Structure
25
+ - `borp.js` - Main CLI entry point with argument parsing and orchestration
26
+ - `lib/run.js` - Core test runner with TypeScript compilation support
27
+ - `lib/conf.js` - Configuration file loading (`.borp.yaml` or `.borp.yml`)
28
+
29
+ ### Key Features
30
+ - Automatic TypeScript compilation detection via `tsconfig.json`
31
+ - Multiple reporter support (spec, tap, dot, junit, github)
32
+ - Code coverage via c8 with customizable thresholds
33
+ - Watch mode for development
34
+ - Post-compilation hooks
35
+ - Configuration file support
36
+
37
+ ### Test Structure
38
+ - Tests use `node:test` with `@matteo.collina/tspl` for planning
39
+ - Test files follow `*.test.{js|ts}` pattern
40
+ - Fixtures in `fixtures/` directory demonstrate various scenarios
41
+ - Coverage excludes test files and fixtures by default
42
+
43
+ ### TypeScript Support
44
+ - Automatically compiles TypeScript when `tsconfig.json` found
45
+ - Supports both ESM and CJS module formats
46
+ - Source map support for debugging
47
+ - Incremental compilation for performance
48
+
49
+ ## Configuration
50
+
51
+ ### CLI Options
52
+ - Coverage: `--coverage` or `-C`
53
+ - Concurrency: `--concurrency` or `-c` (defaults to CPU count - 1)
54
+ - Timeout: `--timeout` or `-t` (default 30s)
55
+ - Watch: `--watch` or `-w`
56
+ - Reporter: `--reporter` or `-r`
57
+
58
+ ### Config File
59
+ Supports `.borp.yaml`/`.borp.yml` with:
60
+ - `files`: Array of test file globs
61
+ - `reporters`: Array of reporter configurations
package/borp.js CHANGED
@@ -23,45 +23,93 @@ process.on('unhandledRejection', (err) => {
23
23
  process.exit(1)
24
24
  })
25
25
 
26
+ function showHelp () {
27
+ console.log(`Usage: borp [options] [files...]
28
+
29
+ Options:
30
+ -h, --help Show this help message
31
+ -o, --only Only run tests with the 'only' option set
32
+ -w, --watch Re-run tests on changes
33
+ -p, --pattern <pattern> Run tests matching the given glob pattern
34
+ -c, --concurrency <num> Set number of concurrent tests (default: ${os.availableParallelism() - 1 || 1})
35
+ -C, --coverage Enable code coverage
36
+ -t, --timeout <ms> Set test timeout in milliseconds (default: 30000)
37
+ --no-timeout Disable test timeout
38
+ -X, --coverage-exclude Exclude patterns from coverage (can be used multiple times)
39
+ -i, --ignore <pattern> Ignore glob pattern (can be used multiple times)
40
+ --expose-gc Expose the gc() function to tests
41
+ -T, --no-typescript Disable automatic TypeScript compilation
42
+ -P, --post-compile <file> Execute file after TypeScript compilation
43
+ -r, --reporter <name> Set reporter (can be used multiple times, default: spec)
44
+ --check-coverage Enable coverage threshold checking
45
+ --lines <threshold> Set lines coverage threshold (default: 100)
46
+ --branches <threshold> Set branches coverage threshold (default: 100)
47
+ --functions <threshold> Set functions coverage threshold (default: 100)
48
+ --statements <threshold> Set statements coverage threshold (default: 100)
49
+
50
+ Examples:
51
+ borp # Run all tests
52
+ borp --coverage # Run tests with coverage
53
+ borp --watch # Run tests in watch mode
54
+ borp test/specific.test.js # Run specific test file
55
+ borp --reporter tap --reporter gh # Use multiple reporters`)
56
+ }
57
+
26
58
  const foundConfig = await loadConfig()
27
59
  if (foundConfig.length > 0) {
28
60
  Array.prototype.push.apply(process.argv, foundConfig)
29
61
  }
30
62
 
31
- const args = parseArgs({
32
- args: process.argv.slice(2),
33
- options: {
34
- only: { type: 'boolean', short: 'o' },
35
- watch: { type: 'boolean', short: 'w' },
36
- pattern: { type: 'string', short: 'p' },
37
- concurrency: { type: 'string', short: 'c', default: os.availableParallelism() - 1 + '' },
38
- coverage: { type: 'boolean', short: 'C' },
39
- timeout: { type: 'string', short: 't', default: '30000' },
40
- 'no-timeout': { type: 'boolean' },
41
- 'coverage-exclude': { type: 'string', short: 'X', multiple: true },
42
- ignore: { type: 'string', short: 'i', multiple: true },
43
- 'expose-gc': { type: 'boolean' },
44
- help: { type: 'boolean', short: 'h' },
45
- 'no-typescript': { type: 'boolean', short: 'T' },
46
- 'post-compile': { type: 'string', short: 'P' },
47
- reporter: {
48
- type: 'string',
49
- short: 'r',
50
- default: ['spec'],
51
- multiple: true
52
- },
53
- 'check-coverage': { type: 'boolean' },
54
- lines: { type: 'string', default: '100' },
55
- branches: { type: 'string', default: '100' },
56
- functions: { type: 'string', default: '100' },
57
- statements: { type: 'string', default: '100' }
63
+ const optionsConfig = {
64
+ only: { type: 'boolean', short: 'o' },
65
+ watch: { type: 'boolean', short: 'w' },
66
+ pattern: { type: 'string', short: 'p' },
67
+ concurrency: { type: 'string', short: 'c', default: (os.availableParallelism() - 1 || 1) + '' },
68
+ coverage: { type: 'boolean', short: 'C' },
69
+ timeout: { type: 'string', short: 't', default: '30000' },
70
+ 'no-timeout': { type: 'boolean' },
71
+ 'coverage-exclude': { type: 'string', short: 'X', multiple: true },
72
+ ignore: { type: 'string', short: 'i', multiple: true },
73
+ 'expose-gc': { type: 'boolean' },
74
+ help: { type: 'boolean', short: 'h' },
75
+ 'no-typescript': { type: 'boolean', short: 'T' },
76
+ 'post-compile': { type: 'string', short: 'P' },
77
+ reporter: {
78
+ type: 'string',
79
+ short: 'r',
80
+ default: ['spec'],
81
+ multiple: true
58
82
  },
59
- allowPositionals: true
60
- })
83
+ 'check-coverage': { type: 'boolean' },
84
+ lines: { type: 'string', default: '100' },
85
+ branches: { type: 'string', default: '100' },
86
+ functions: { type: 'string', default: '100' },
87
+ statements: { type: 'string', default: '100' }
88
+ }
61
89
 
62
- /* c8 ignore next 5 */
90
+ let args
91
+ try {
92
+ args = parseArgs({
93
+ args: process.argv.slice(2),
94
+ options: optionsConfig,
95
+ allowPositionals: true
96
+ })
97
+ } catch (error) {
98
+ if (error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION') {
99
+ console.error(`Error: ${error.message}\n`)
100
+ // Send help to stderr when showing error
101
+ const originalConsoleLog = console.log
102
+ console.log = console.error
103
+ showHelp()
104
+ console.log = originalConsoleLog
105
+ process.exit(1)
106
+ }
107
+ throw error
108
+ }
109
+
110
+ /* c8 ignore next 4 */
63
111
  if (args.values.help) {
64
- console.log(await readFile(new URL('./README.md', import.meta.url), 'utf8'))
112
+ showHelp()
65
113
  process.exit(0)
66
114
  }
67
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borp",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "description": "node:test wrapper with TypeScript support",
6
6
  "main": "borp.js",
@@ -23,8 +23,8 @@
23
23
  "devDependencies": {
24
24
  "@matteo.collina/tspl": "^0.1.0",
25
25
  "@reporters/silent": "^1.2.4",
26
- "@sinonjs/fake-timers": "^13.0.2",
27
- "@types/node": "^22.2.0",
26
+ "@sinonjs/fake-timers": "^14.0.0",
27
+ "@types/node": "^24.0.14",
28
28
  "desm": "^1.3.0",
29
29
  "semver": "^7.6.3",
30
30
  "snazzy": "^9.0.0",
package/test/cli.test.js CHANGED
@@ -7,6 +7,15 @@ import path from 'node:path'
7
7
 
8
8
  const borp = join(import.meta.url, '..', 'borp.js')
9
9
 
10
+ // Debug logging for Windows troubleshooting
11
+ console.log('[DEBUG] Environment info:', {
12
+ platform: process.platform,
13
+ nodeVersion: process.version,
14
+ cwd: process.cwd(),
15
+ borpPath: borp,
16
+ __dirname: import.meta.url
17
+ })
18
+
10
19
  delete process.env.GITHUB_ACTION
11
20
 
12
21
  test('limit concurrency', async () => {
@@ -154,3 +163,216 @@ test('Post compile script should be executed when --post-compile is sent with c
154
163
 
155
164
  strictEqual(stdout.indexOf('Post compile hook complete') >= 0, true, 'Post compile message should be found in stdout')
156
165
  })
166
+
167
+ test('invalid option shows help text', async () => {
168
+ console.log('[DEBUG] Starting invalid option test')
169
+ const testCwd = join(import.meta.url, '..', 'fixtures', 'js-esm')
170
+ console.log('[DEBUG] Test CWD:', testCwd)
171
+ console.log('[DEBUG] Borp path:', borp)
172
+ console.log('[DEBUG] Command:', 'node', [borp, '--invalid-option'])
173
+
174
+ await rejects(async () => {
175
+ console.log('[DEBUG] About to execute execa')
176
+ const startTime = Date.now()
177
+ try {
178
+ const result = await execa('node', [borp, '--invalid-option'], {
179
+ cwd: testCwd,
180
+ timeout: 15000, // 15 second timeout
181
+ windowsHide: process.platform === 'win32'
182
+ })
183
+ console.log('[DEBUG] Unexpected success:', result)
184
+ throw new Error('Expected command to fail')
185
+ } catch (error) {
186
+ const elapsed = Date.now() - startTime
187
+ console.log(`[DEBUG] Execa threw error after ${elapsed}ms:`, {
188
+ exitCode: error.exitCode,
189
+ timedOut: error.timedOut,
190
+ stderr: error.stderr?.substring(0, 200) + '...',
191
+ stdout: error.stdout?.substring(0, 200) + '...',
192
+ message: error.message
193
+ })
194
+
195
+ throw error
196
+ }
197
+ }, (error) => {
198
+ console.log('[DEBUG] In error handler, validating error:', {
199
+ exitCode: error.exitCode,
200
+ stderrLength: error.stderr?.length,
201
+ stdoutLength: error.stdout?.length
202
+ })
203
+
204
+ // Should exit with code 1
205
+ console.log('[DEBUG] Checking exit code:', error.exitCode)
206
+ strictEqual(error.exitCode, 1)
207
+
208
+ // Should show error message
209
+ const hasErrorMessage = error.stderr.includes('Error: Unknown option \'--invalid-option\'')
210
+ console.log('[DEBUG] Has error message:', hasErrorMessage)
211
+ console.log('[DEBUG] Stderr content:', error.stderr)
212
+ strictEqual(hasErrorMessage, true, 'Should show error message')
213
+
214
+ // Should show help text
215
+ const hasUsage = error.stderr.includes('Usage: borp [options] [files...]')
216
+ console.log('[DEBUG] Has usage line:', hasUsage)
217
+ strictEqual(hasUsage, true, 'Should show usage line')
218
+
219
+ const hasHelp = error.stderr.includes('--help')
220
+ console.log('[DEBUG] Has help option:', hasHelp)
221
+ strictEqual(hasHelp, true, 'Should show help option')
222
+
223
+ const hasCoverage = error.stderr.includes('--coverage')
224
+ console.log('[DEBUG] Has coverage option:', hasCoverage)
225
+ strictEqual(hasCoverage, true, 'Should show coverage option')
226
+
227
+ const hasExamples = error.stderr.includes('Examples:')
228
+ console.log('[DEBUG] Has examples section:', hasExamples)
229
+ strictEqual(hasExamples, true, 'Should show examples section')
230
+
231
+ console.log('[DEBUG] All assertions passed')
232
+ return true
233
+ })
234
+ console.log('[DEBUG] Test completed successfully')
235
+ })
236
+
237
+ test('multiple invalid options show help text', async () => {
238
+ console.log('[DEBUG] Starting multiple invalid options test')
239
+ const testCwd = join(import.meta.url, '..', 'fixtures', 'js-esm')
240
+ console.log('[DEBUG] Test CWD:', testCwd)
241
+
242
+ await rejects(async () => {
243
+ console.log('[DEBUG] About to execute execa with multiple invalid options')
244
+ const result = await execa('node', [borp, '--foo', '--bar'], {
245
+ cwd: testCwd,
246
+ timeout: 15000,
247
+ windowsHide: false
248
+ })
249
+ console.log('[DEBUG] Unexpected success:', result)
250
+ throw new Error('Expected command to fail')
251
+ }, (error) => {
252
+ console.log('[DEBUG] Multiple options error handler:', {
253
+ exitCode: error.exitCode,
254
+ stderrLength: error.stderr?.length
255
+ })
256
+
257
+ // Should exit with code 1 and show help for first invalid option
258
+ strictEqual(error.exitCode, 1)
259
+ const hasFooError = error.stderr.includes('Error: Unknown option \'--foo\'')
260
+ console.log('[DEBUG] Has foo error:', hasFooError)
261
+ strictEqual(hasFooError, true, 'Should show error for first invalid option')
262
+
263
+ const hasUsage = error.stderr.includes('Usage: borp [options] [files...]')
264
+ console.log('[DEBUG] Has usage in multiple options:', hasUsage)
265
+ strictEqual(hasUsage, true, 'Should show help text')
266
+ return true
267
+ })
268
+ console.log('[DEBUG] Multiple options test completed')
269
+ })
270
+
271
+ test('invalid short option shows help text', async () => {
272
+ console.log('[DEBUG] Starting invalid short option test')
273
+ const testCwd = join(import.meta.url, '..', 'fixtures', 'js-esm')
274
+ console.log('[DEBUG] Test CWD:', testCwd)
275
+
276
+ await rejects(async () => {
277
+ console.log('[DEBUG] About to execute execa with invalid short option')
278
+ const result = await execa('node', [borp, '-z'], {
279
+ cwd: testCwd,
280
+ timeout: 15000,
281
+ windowsHide: process.platform === 'win32'
282
+ })
283
+ console.log('[DEBUG] Unexpected success:', result)
284
+ throw new Error('Expected command to fail')
285
+ }, (error) => {
286
+ console.log('[DEBUG] Short option error handler:', {
287
+ exitCode: error.exitCode,
288
+ stderrLength: error.stderr?.length
289
+ })
290
+
291
+ strictEqual(error.exitCode, 1)
292
+ const hasZError = error.stderr.includes('Error: Unknown option \'-z\'')
293
+ console.log('[DEBUG] Has -z error:', hasZError)
294
+ strictEqual(hasZError, true, 'Should show error message')
295
+
296
+ const hasUsage = error.stderr.includes('Usage: borp [options] [files...]')
297
+ console.log('[DEBUG] Has usage in short option:', hasUsage)
298
+ strictEqual(hasUsage, true, 'Should show help text')
299
+ return true
300
+ })
301
+ console.log('[DEBUG] Short option test completed')
302
+ })
303
+
304
+ test('--help option shows help text and exits successfully', async () => {
305
+ console.log('[DEBUG] Starting --help option test')
306
+ const testCwd = join(import.meta.url, '..', 'fixtures', 'js-esm')
307
+ console.log('[DEBUG] Test CWD:', testCwd)
308
+
309
+ const startTime = Date.now()
310
+ const { stdout, exitCode } = await execa('node', [borp, '--help'], {
311
+ cwd: testCwd,
312
+ timeout: 15000,
313
+ windowsHide: process.platform === 'win32'
314
+ })
315
+ const elapsed = Date.now() - startTime
316
+
317
+ console.log(`[DEBUG] --help completed after ${elapsed}ms:`, {
318
+ exitCode,
319
+ stdoutLength: stdout?.length,
320
+ stdoutPreview: stdout?.substring(0, 100) + '...'
321
+ })
322
+
323
+ strictEqual(exitCode, 0, 'Should exit with code 0')
324
+
325
+ const hasUsage = stdout.includes('Usage: borp [options] [files...]')
326
+ console.log('[DEBUG] --help has usage:', hasUsage)
327
+ strictEqual(hasUsage, true, 'Should show usage line')
328
+
329
+ const hasHelpOption = stdout.includes('--help')
330
+ console.log('[DEBUG] --help has help option:', hasHelpOption)
331
+ strictEqual(hasHelpOption, true, 'Should show help option')
332
+
333
+ const hasCoverage = stdout.includes('--coverage')
334
+ console.log('[DEBUG] --help has coverage:', hasCoverage)
335
+ strictEqual(hasCoverage, true, 'Should show coverage option')
336
+
337
+ const hasExamples = stdout.includes('Examples:')
338
+ console.log('[DEBUG] --help has examples:', hasExamples)
339
+ strictEqual(hasExamples, true, 'Should show examples section')
340
+
341
+ const hasCoverageExample = stdout.includes('borp --coverage')
342
+ console.log('[DEBUG] --help has coverage example:', hasCoverageExample)
343
+ strictEqual(hasCoverageExample, true, 'Should show coverage example')
344
+
345
+ console.log('[DEBUG] --help test completed')
346
+ })
347
+
348
+ test('-h option shows help text and exits successfully', async () => {
349
+ console.log('[DEBUG] Starting -h option test')
350
+ const testCwd = join(import.meta.url, '..', 'fixtures', 'js-esm')
351
+ console.log('[DEBUG] Test CWD:', testCwd)
352
+
353
+ const startTime = Date.now()
354
+ const { stdout, exitCode } = await execa('node', [borp, '-h'], {
355
+ cwd: testCwd,
356
+ timeout: 15000,
357
+ windowsHide: process.platform === 'win32'
358
+ })
359
+ const elapsed = Date.now() - startTime
360
+
361
+ console.log(`[DEBUG] -h completed after ${elapsed}ms:`, {
362
+ exitCode,
363
+ stdoutLength: stdout?.length,
364
+ stdoutPreview: stdout?.substring(0, 100) + '...'
365
+ })
366
+
367
+ strictEqual(exitCode, 0, 'Should exit with code 0')
368
+
369
+ const hasUsage = stdout.includes('Usage: borp [options] [files...]')
370
+ console.log('[DEBUG] -h has usage:', hasUsage)
371
+ strictEqual(hasUsage, true, 'Should show usage line')
372
+
373
+ const hasExamples = stdout.includes('Examples:')
374
+ console.log('[DEBUG] -h has examples:', hasExamples)
375
+ strictEqual(hasExamples, true, 'Should show examples section')
376
+
377
+ console.log('[DEBUG] -h test completed')
378
+ })