@scalvert/bin-tester 2.1.0 → 3.0.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/README.md CHANGED
@@ -4,176 +4,116 @@
4
4
  [![npm version](https://badge.fury.io/js/%40scalvert%2Fbin-tester.svg)](https://badge.fury.io/js/%40scalvert%2Fbin-tester)
5
5
  [![License](https://img.shields.io/npm/l/@scalvert/bin-tester.svg)](https://github.com/scalvert/bin-tester/blob/master/package.json)
6
6
  ![Dependabot](https://badgen.net/badge/icon/dependabot?icon=dependabot&label)
7
- ![Volta Managed](https://img.shields.io/static/v1?label=volta&message=managed&color=yellow&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAeQC6AMEpK7AhAAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH5AMGFS07qAYEaAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAFmSURBVDjLY2CgB/g/j0H5/2wGW2xyTAQ1r2DQYOBgm8nwh+EY6TYvZtD7f9rn5e81fAGka17GYPL/esObP+dyj5Cs+edqZsv/V8o//H+z7P+XHarW+NSyoAv8WsFszyKTtoVBM5Tn7/Xys+zf7v76vYrJlPEvAwPjH0YGxp//3jGl/L8LU8+IrPnPUkY3ZomoDQwOpZwMv14zMHy8yMDwh4mB4Q8jA8OTgwz/L299wMDyx4Mp9f9NDAP+bWVwY3jGsJpB3JaDQVCEgYHlLwPDfwYWRqVQJgZmHoZ/+3PPfWP+68Mb/Pw5sqUoLni9ipuRnekrAwMjA8Ofb6K8/PKBF5nU7RX+Hize8Y2DOZTP7+kXogPy1zrH+f/vT/j/Z5nUvGcr5VhJioUf88UC/59L+/97gUgDyVH4YzqXxL8dOs/+zuFLJivd/53HseLPPHZPsjT/nsHi93cqozHZue7rLDYhUvUAADjCgneouzo/AAAAAElFTkSuQmCC&link=https://volta.sh)
8
7
  [![Code Style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](#badge)
9
8
 
10
- > Provides a test harness for node CLIs that allow you to run tests against a real project.
9
+ A test harness for Node.js CLI tools.
11
10
 
12
- ## Install
13
-
14
- ```shell
15
- npm add @scalvert/bin-tester --save-dev
16
-
17
- # or
18
-
19
- yarn add @scalvert/bin-tester --dev
20
- ```
11
+ Testing a CLI isn't like testing a library—you can't just import functions and call them. You need to spawn your CLI as a subprocess, give it real files to work with, and capture its output. bin-tester simplifies this:
21
12
 
22
- ## Usage
23
-
24
- `@scalvert/bin-tester` uses two libraries to provide the test harness:
25
-
26
- - [`fixturify-project`](https://github.com/stefanpenner/node-fixturify-project): Allows you to dynamically create test fixtures using real directories and files in a tmp directory
27
- - [`execa`](https://github.com/sindresorhus/execa): A better replacement for `child_process.exec`
28
-
29
- It combines the above and provides an API for running a binary with a set of arguments against a real project structure, thus mimicking testing a real environment.
30
-
31
- ```js
13
+ ```ts snippet=basic-example.ts
32
14
  import { createBinTester } from '@scalvert/bin-tester';
33
15
 
34
- describe('Some tests', () => {
35
- let project;
36
- let { setupProject, teardownProject, runBin } = createBinTester({
37
- binPath: 'node_modules/.bin/someBin',
38
- staticArgs: ['--some-arg'], // pass some args to the bin that will be used for each invocation
16
+ describe('my-cli', () => {
17
+ const { setupProject, teardownProject, runBin } = createBinTester({
18
+ binPath: './bin/my-cli.js',
39
19
  });
40
20
 
41
- beforeEach(() => {
21
+ let project;
22
+
23
+ beforeEach(async () => {
42
24
  project = await setupProject();
43
25
  });
44
26
 
45
27
  afterEach(() => {
46
- await teardownProject();
47
- });
48
-
49
- // Run the bin and do something with the result
50
- test('a test', async () => {
51
- const result = await runBin();
52
-
53
- expect(result.stdout).toBe('Did some stuff');
28
+ teardownProject();
54
29
  });
55
30
 
56
- test('another test', async () => {
57
- // Write a file with contents to the tmp directory
58
- await project.writeDirJSON({
59
- 'some/file.txt': 'some content',
60
- });
31
+ test('processes files', async () => {
32
+ project.files = { 'input.txt': 'hello' };
33
+ await project.write();
61
34
 
62
- // pass some args to the bin that will be used for only this invocation
63
- const result = await runBin('--path', 'some/file.txt');
35
+ const result = await runBin('input.txt');
64
36
 
65
- expect(result.stdout).toBe('Read "some/file.txt"');
37
+ expect(result.exitCode).toBe(0);
38
+ expect(result.stdout).toContain('processed');
66
39
  });
67
40
  });
68
41
  ```
69
42
 
70
- ## API
71
-
72
- <!--DOCS_START-->
73
- ## Classes
74
-
75
- <dl>
76
- <dt><a href="#BinTesterProject">BinTesterProject</a></dt>
77
- <dd></dd>
78
- </dl>
79
-
80
- ## Functions
81
-
82
- <dl>
83
- <dt><a href="#createBinTester">createBinTester(options)</a> ⇒ <code>CreateBinTesterResult.&lt;TProject&gt;</code></dt>
84
- <dd><p>Creates the bin tester API functions to use within tests.</p></dd>
85
- </dl>
86
-
87
- <a name="BinTesterProject"></a>
88
-
89
- ## BinTesterProject
90
- **Kind**: global class
91
-
92
- * [BinTesterProject](#BinTesterProject)
93
- * [new BinTesterProject(name, version, cb)](#new_BinTesterProject_new)
94
- * [.gitInit()](#BinTesterProject+gitInit) ⇒ <code>\*</code>
95
- * [.chdir()](#BinTesterProject+chdir)
96
- * [.dispose()](#BinTesterProject+dispose) ⇒ <code>void</code>
97
-
98
- <a name="new_BinTesterProject_new"></a>
99
-
100
- ### new BinTesterProject(name, version, cb)
101
- <p>Constructs an instance of a BinTesterProject.</p>
102
-
103
-
104
- | Param | Type | Default | Description |
105
- | --- | --- | --- | --- |
106
- | name | <code>string</code> | <code>&quot;fake-project&quot;</code> | <p>The name of the project. Used within the package.json as the name property.</p> |
107
- | version | <code>string</code> | | <p>The version of the project. Used within the package.json as the version property.</p> |
108
- | cb | <code>function</code> | | <p>An optional callback for additional setup steps after the project is constructed.</p> |
109
-
110
- <a name="BinTesterProject+gitInit"></a>
111
-
112
- ### binTesterProject.gitInit() ⇒ <code>\*</code>
113
- <p>Runs <code>git init</code> inside a project.</p>
114
-
115
- **Kind**: instance method of [<code>BinTesterProject</code>](#BinTesterProject)
116
- **Returns**: <code>\*</code> - <p>{execa.ExecaChildProcess<string>}</p>
117
- <a name="BinTesterProject+chdir"></a>
118
-
119
- ### binTesterProject.chdir()
120
- <p>Changes a directory from inside the project.</p>
43
+ ## Install
121
44
 
122
- **Kind**: instance method of [<code>BinTesterProject</code>](#BinTesterProject)
123
- <a name="BinTesterProject+dispose"></a>
45
+ ```bash
46
+ npm add @scalvert/bin-tester --save-dev
47
+ ```
124
48
 
125
- ### binTesterProject.dispose() ⇒ <code>void</code>
126
- <p>Correctly disposes of the project, observing when the directory has been changed.</p>
49
+ ## Usage
127
50
 
128
- **Kind**: instance method of [<code>BinTesterProject</code>](#BinTesterProject)
129
- <a name="createBinTester"></a>
51
+ `createBinTester` returns helpers for setting up projects, running your CLI, and cleaning up:
130
52
 
131
- ## createBinTester(options) ⇒ <code>CreateBinTesterResult.&lt;TProject&gt;</code>
132
- <p>Creates the bin tester API functions to use within tests.</p>
53
+ ```ts snippet=create-bin-tester.ts
54
+ const { setupProject, teardownProject, runBin } = createBinTester({
55
+ binPath: './bin/my-cli.js',
56
+ staticArgs: ['--verbose'], // args passed to every invocation
57
+ });
58
+ ```
133
59
 
134
- **Kind**: global function
135
- **Returns**: <code>CreateBinTesterResult.&lt;TProject&gt;</code> - <ul>
136
- <li>A project instance.</li>
137
- </ul>
60
+ **Setup and teardown:**
138
61
 
139
- | Param | Type | Description |
140
- | --- | --- | --- |
141
- | options | <code>BinTesterOptions.&lt;TProject&gt;</code> | <p>An object of bin tester options</p> |
62
+ ```ts snippet=setup-teardown.ts
63
+ const project = await setupProject(); // creates temp directory
64
+ // ... run tests ...
65
+ teardownProject(); // removes temp directory
66
+ ```
142
67
 
68
+ **Writing fixture files:**
143
69
 
144
- * [createBinTester(options)](#createBinTester) ⇒ <code>CreateBinTesterResult.&lt;TProject&gt;</code>
145
- * [~runBin(...args)](#createBinTester..runBin) ⇒ <code>execa.ExecaChildProcess.&lt;string&gt;</code>
146
- * [~setupProject()](#createBinTester..setupProject)
147
- * [~setupTmpDir()](#createBinTester..setupTmpDir)
148
- * [~teardownProject()](#createBinTester..teardownProject)
70
+ ```ts snippet=writing-fixtures.ts
71
+ project.files = {
72
+ 'src/index.js': 'export default 42;',
73
+ 'package.json': JSON.stringify({ name: 'test' }),
74
+ };
75
+ await project.write();
76
+ ```
149
77
 
150
- <a name="createBinTester..runBin"></a>
78
+ **Running your CLI:**
151
79
 
152
- ### createBinTester~runBin(...args) ⇒ <code>execa.ExecaChildProcess.&lt;string&gt;</code>
153
- **Kind**: inner method of [<code>createBinTester</code>](#createBinTester)
154
- **Returns**: <code>execa.ExecaChildProcess.&lt;string&gt;</code> - <p>An instance of execa's child process.</p>
80
+ ```ts snippet=running-cli.ts
81
+ const result = await runBin('--flag', 'arg');
155
82
 
156
- | Param | Type | Description |
157
- | --- | --- | --- |
158
- | ...args | <code>RunBinArgs</code> | <p>Arguments or execa options.</p> |
83
+ result.exitCode; // number
84
+ result.stdout; // string
85
+ result.stderr; // string
86
+ ```
159
87
 
160
- <a name="createBinTester..setupProject"></a>
88
+ ## Debugging
161
89
 
162
- ### createBinTester~setupProject()
163
- <p>Sets up the specified project for use within tests.</p>
90
+ Set `BIN_TESTER_DEBUG` to enable the Node inspector and preserve fixtures for inspection:
164
91
 
165
- **Kind**: inner method of [<code>createBinTester</code>](#createBinTester)
166
- <a name="createBinTester..setupTmpDir"></a>
92
+ ```bash
93
+ BIN_TESTER_DEBUG=attach npm test # attach debugger
94
+ BIN_TESTER_DEBUG=break npm test # break on first line
95
+ ```
167
96
 
168
- ### createBinTester~setupTmpDir()
169
- <p>Sets up a tmp directory for use within tests.</p>
97
+ Or use `runBinDebug()` programmatically:
170
98
 
171
- **Kind**: inner method of [<code>createBinTester</code>](#createBinTester)
172
- <a name="createBinTester..teardownProject"></a>
99
+ ```ts snippet=run-bin-debug.ts
100
+ await runBinDebug('--flag'); // runs with --inspect
101
+ ```
173
102
 
174
- ### createBinTester~teardownProject()
175
- <p>Tears the project down, ensuring the tmp directory is removed. Shoud be paired with setupProject.</p>
103
+ For VS Code, add to `.vscode/launch.json`:
104
+
105
+ ```jsonc
106
+ {
107
+ "name": "Debug Tests",
108
+ "type": "node",
109
+ "request": "launch",
110
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vitest",
111
+ "runtimeArgs": ["run"],
112
+ "autoAttachChildProcesses": true,
113
+ "console": "integratedTerminal",
114
+ }
115
+ ```
176
116
 
177
- **Kind**: inner method of [<code>createBinTester</code>](#createBinTester)
117
+ ## API
178
118
 
179
- <!--DOCS_END-->
119
+ See the [full API documentation](https://scalvert.github.io/bin-tester/).
package/dist/index.cjs CHANGED
@@ -1,23 +1,8 @@
1
- var __create = Object.create;
1
+ "use strict";
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
9
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
- var __spreadValues = (a, b) => {
11
- for (var prop in b || (b = {}))
12
- if (__hasOwnProp.call(b, prop))
13
- __defNormalProp(a, prop, b[prop]);
14
- if (__getOwnPropSymbols)
15
- for (var prop of __getOwnPropSymbols(b)) {
16
- if (__propIsEnum.call(b, prop))
17
- __defNormalProp(a, prop, b[prop]);
18
- }
19
- return a;
20
- };
21
6
  var __export = (target, all) => {
22
7
  for (var name in all)
23
8
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,25 +15,30 @@ var __copyProps = (to, from, except, desc) => {
30
15
  }
31
16
  return to;
32
17
  };
33
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
34
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
35
19
 
36
20
  // src/index.ts
37
- var src_exports = {};
38
- __export(src_exports, {
21
+ var index_exports = {};
22
+ __export(index_exports, {
39
23
  BinTesterProject: () => BinTesterProject,
40
24
  createBinTester: () => createBinTester
41
25
  });
42
- module.exports = __toCommonJS(src_exports);
26
+ module.exports = __toCommonJS(index_exports);
43
27
 
44
28
  // src/create-bin-tester.ts
45
- var import_execa2 = __toESM(require("execa"), 1);
29
+ var import_execa2 = require("execa");
46
30
 
47
31
  // src/project.ts
48
- var import_execa = __toESM(require("execa"), 1);
32
+ var import_execa = require("execa");
49
33
  var import_fixturify_project = require("fixturify-project");
50
34
  var ROOT = process.cwd();
51
35
  var BinTesterProject = class extends import_fixturify_project.Project {
36
+ /**
37
+ * Constructs an instance of a BinTesterProject.
38
+ * @param {string} name - The name of the project. Used within the package.json as the name property.
39
+ * @param {string} version - The version of the project. Used within the package.json as the version property.
40
+ * @param {Function} cb - An optional callback for additional setup steps after the project is constructed.
41
+ */
52
42
  constructor(name = "fake-project", version, cb) {
53
43
  super(name, version, cb);
54
44
  this._dirChanged = false;
@@ -58,14 +48,31 @@ var BinTesterProject = class extends import_fixturify_project.Project {
58
48
  repository: "http://fakerepo.com"
59
49
  });
60
50
  }
51
+ /**
52
+ * Runs `git init` inside a project.
53
+ * @returns {*} {ResultPromise}
54
+ */
61
55
  gitInit() {
62
- return (0, import_execa.default)(`git init -q ${this.baseDir}`);
56
+ return (0, import_execa.execa)("git", ["init", "-q", this.baseDir]);
57
+ }
58
+ /**
59
+ * Writes the project files to disk.
60
+ */
61
+ async write() {
62
+ return super.write();
63
63
  }
64
+ /**
65
+ * Changes a directory from inside the project.
66
+ */
64
67
  async chdir() {
65
68
  this._dirChanged = true;
66
69
  await this.write();
67
70
  process.chdir(this.baseDir);
68
71
  }
72
+ /**
73
+ * Correctly disposes of the project, observing when the directory has been changed.
74
+ * @returns {void}
75
+ */
69
76
  dispose() {
70
77
  if (this._dirChanged) {
71
78
  process.chdir(ROOT);
@@ -80,28 +87,59 @@ var DEFAULT_BIN_TESTER_OPTIONS = {
80
87
  };
81
88
  function parseArgs(args) {
82
89
  if (args.length > 0 && typeof args[args.length - 1] === "object") {
83
- const execaOptions = args.pop();
90
+ const argsCopy = [...args];
91
+ const execaOptions = argsCopy.pop();
84
92
  return {
85
- args,
93
+ args: argsCopy,
86
94
  execaOptions
87
95
  };
88
96
  } else {
89
97
  return {
90
- args,
98
+ args: [...args],
91
99
  execaOptions: {}
92
100
  };
93
101
  }
94
102
  }
95
103
  function createBinTester(options) {
96
104
  let project;
97
- const mergedOptions = __spreadValues(__spreadValues({}, DEFAULT_BIN_TESTER_OPTIONS), options);
105
+ const mergedOptions = {
106
+ ...DEFAULT_BIN_TESTER_OPTIONS,
107
+ ...options
108
+ };
98
109
  function runBin(...args) {
99
110
  const mergedRunOptions = parseArgs(args);
100
111
  const binPath = typeof mergedOptions.binPath === "function" ? mergedOptions.binPath(project) : mergedOptions.binPath;
101
- return (0, import_execa2.default)(process.execPath, [binPath, ...mergedOptions.staticArgs, ...mergedRunOptions.args], __spreadValues({
112
+ const optionsEnv = mergedRunOptions.execaOptions.env;
113
+ const debugEnv = optionsEnv?.BIN_TESTER_DEBUG ?? process.env.BIN_TESTER_DEBUG;
114
+ const nodeOptions = [];
115
+ if (debugEnv && debugEnv !== "0" && debugEnv.toLowerCase() !== "false") {
116
+ if (debugEnv.toLowerCase() === "break") {
117
+ nodeOptions.push("--inspect-brk=0");
118
+ } else {
119
+ nodeOptions.push("--inspect=0");
120
+ }
121
+ console.log(`[bin-tester] Debugging enabled. Fixture: ${project.baseDir}`);
122
+ }
123
+ const resolvedCwd = mergedRunOptions.execaOptions.cwd ?? project.baseDir;
124
+ return (0, import_execa2.execaNode)(binPath, [...mergedOptions.staticArgs, ...mergedRunOptions.args], {
102
125
  reject: false,
103
- cwd: project.baseDir
104
- }, mergedRunOptions.execaOptions));
126
+ cwd: resolvedCwd,
127
+ nodeOptions,
128
+ ...mergedRunOptions.execaOptions
129
+ });
130
+ }
131
+ function runBinDebug(...args) {
132
+ const parsedArgs = parseArgs(args);
133
+ const debugEnv = process.env.BIN_TESTER_DEBUG || "attach";
134
+ parsedArgs.execaOptions = {
135
+ ...parsedArgs.execaOptions,
136
+ env: {
137
+ ...parsedArgs.execaOptions.env,
138
+ BIN_TESTER_DEBUG: debugEnv
139
+ }
140
+ };
141
+ const reconstructedArgs = [...parsedArgs.args, parsedArgs.execaOptions];
142
+ return runBin(...reconstructedArgs);
105
143
  }
106
144
  async function setupProject() {
107
145
  project = "createProject" in mergedOptions ? await mergedOptions.createProject() : new BinTesterProject();
@@ -115,10 +153,16 @@ function createBinTester(options) {
115
153
  return project.baseDir;
116
154
  }
117
155
  function teardownProject() {
156
+ const debugEnv = process.env.BIN_TESTER_DEBUG;
157
+ if (debugEnv && debugEnv !== "0" && debugEnv.toLowerCase() !== "false") {
158
+ console.log(`[bin-tester] Fixture preserved: ${project.baseDir}`);
159
+ return;
160
+ }
118
161
  project.dispose();
119
162
  }
120
163
  return {
121
164
  runBin,
165
+ runBinDebug,
122
166
  setupProject,
123
167
  teardownProject,
124
168
  setupTmpDir
@@ -0,0 +1,112 @@
1
+ import { ResultPromise, Options } from 'execa';
2
+ import { Project } from 'fixturify-project';
3
+
4
+ declare class BinTesterProject extends Project {
5
+ private _dirChanged;
6
+ /**
7
+ * Constructs an instance of a BinTesterProject.
8
+ * @param {string} name - The name of the project. Used within the package.json as the name property.
9
+ * @param {string} version - The version of the project. Used within the package.json as the version property.
10
+ * @param {Function} cb - An optional callback for additional setup steps after the project is constructed.
11
+ */
12
+ constructor(name?: string, version?: string, cb?: (project: Project) => void);
13
+ /**
14
+ * Runs `git init` inside a project.
15
+ * @returns {*} {ResultPromise}
16
+ */
17
+ gitInit(): ResultPromise;
18
+ /**
19
+ * Writes the project files to disk.
20
+ */
21
+ write(): Promise<void>;
22
+ /**
23
+ * Changes a directory from inside the project.
24
+ */
25
+ chdir(): Promise<void>;
26
+ /**
27
+ * Correctly disposes of the project, observing when the directory has been changed.
28
+ * @returns {void}
29
+ */
30
+ dispose(): void;
31
+ }
32
+
33
+ /**
34
+ * Options for configuring the bin tester.
35
+ */
36
+ interface BinTesterOptions<TProject> {
37
+ /**
38
+ * The absolute path to the bin to invoke
39
+ */
40
+ binPath: string | (<TProject extends BinTesterProject>(project: TProject) => string);
41
+ /**
42
+ * An array of static arguments that will be used every time when running the bin
43
+ */
44
+ staticArgs?: string[];
45
+ /**
46
+ * An optional function to use to create the project. Use this if you want to provide a custom implementation of a BinTesterProject.
47
+ */
48
+ createProject?: () => Promise<TProject>;
49
+ }
50
+ /**
51
+ * Function signature for running the configured CLI binary.
52
+ */
53
+ interface RunBin {
54
+ /**
55
+ * A runBin implementation that takes no parameters.
56
+ * @returns {*} {ResultPromise}
57
+ */
58
+ (): ResultPromise;
59
+ /**
60
+ * A runBin implementation that takes string varargs.
61
+ * @param {...RunBinArgs} args
62
+ * @returns {*} {ResultPromise}
63
+ */
64
+ (...args: [...binArgs: string[]]): ResultPromise;
65
+ /**
66
+ * A runBin implementation that takes an Options object.
67
+ * @param {...RunBinArgs} args
68
+ * @returns {*} {ResultPromise}
69
+ */
70
+ (...args: [execaOptions: Options]): ResultPromise;
71
+ /**
72
+ * A runBin implementation that takes string or an Options object varargs.
73
+ * @param {...RunBinArgs} args
74
+ * @returns {*} {ResultPromise}
75
+ */
76
+ (...args: [...binArgs: string[], execaOptions: Options]): ResultPromise;
77
+ }
78
+ /**
79
+ * The result returned by createBinTester.
80
+ */
81
+ interface CreateBinTesterResult<TProject extends BinTesterProject> {
82
+ /**
83
+ * Runs the configured bin function via execa.
84
+ */
85
+ runBin: RunBin;
86
+ /**
87
+ * Sets up the specified project for use within tests.
88
+ */
89
+ setupProject: () => Promise<TProject>;
90
+ /**
91
+ * Sets up a tmp directory for use within tests.
92
+ */
93
+ setupTmpDir: () => Promise<string>;
94
+ /**
95
+ * Tears the project down, ensuring the tmp directory is removed.
96
+ * When BIN_TESTER_DEBUG is set, fixtures are preserved for inspection.
97
+ */
98
+ teardownProject: () => void;
99
+ /**
100
+ * Runs the configured bin with Node inspector enabled in attach mode (--inspect).
101
+ * Set BIN_TESTER_DEBUG=break to break on first line instead.
102
+ */
103
+ runBinDebug: RunBin;
104
+ }
105
+ /**
106
+ * Creates the bin tester API functions to use within tests.
107
+ * @param {BinTesterOptions<TProject>} options - An object of bin tester options
108
+ * @returns {CreateBinTesterResult<TProject>} - A project instance.
109
+ */
110
+ declare function createBinTester<TProject extends BinTesterProject>(options: BinTesterOptions<TProject>): CreateBinTesterResult<TProject>;
111
+
112
+ export { type BinTesterOptions, BinTesterProject, type CreateBinTesterResult, type RunBin, createBinTester };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import execa from 'execa';
1
+ import { ResultPromise, Options } from 'execa';
2
2
  import { Project } from 'fixturify-project';
3
3
 
4
4
  declare class BinTesterProject extends Project {
5
5
  private _dirChanged;
6
6
  /**
7
7
  * Constructs an instance of a BinTesterProject.
8
- *
9
8
  * @param {string} name - The name of the project. Used within the package.json as the name property.
10
9
  * @param {string} version - The version of the project. Used within the package.json as the version property.
11
10
  * @param {Function} cb - An optional callback for additional setup steps after the project is constructed.
@@ -13,22 +12,27 @@ declare class BinTesterProject extends Project {
13
12
  constructor(name?: string, version?: string, cb?: (project: Project) => void);
14
13
  /**
15
14
  * Runs `git init` inside a project.
16
- *
17
- * @returns {*} {execa.ExecaChildProcess<string>}
15
+ * @returns {*} {ResultPromise}
18
16
  */
19
- gitInit(): execa.ExecaChildProcess<string>;
17
+ gitInit(): ResultPromise;
18
+ /**
19
+ * Writes the project files to disk.
20
+ */
21
+ write(): Promise<void>;
20
22
  /**
21
23
  * Changes a directory from inside the project.
22
24
  */
23
25
  chdir(): Promise<void>;
24
26
  /**
25
27
  * Correctly disposes of the project, observing when the directory has been changed.
26
- *
27
28
  * @returns {void}
28
29
  */
29
30
  dispose(): void;
30
31
  }
31
32
 
33
+ /**
34
+ * Options for configuring the bin tester.
35
+ */
32
36
  interface BinTesterOptions<TProject> {
33
37
  /**
34
38
  * The absolute path to the bin to invoke
@@ -43,23 +47,37 @@ interface BinTesterOptions<TProject> {
43
47
  */
44
48
  createProject?: () => Promise<TProject>;
45
49
  }
50
+ /**
51
+ * Function signature for running the configured CLI binary.
52
+ */
46
53
  interface RunBin {
47
54
  /**
48
55
  * A runBin implementation that takes no parameters.
49
- *
50
- * @returns {*} {execa.ExecaChildProcess<string>}
51
- * @memberof RunBin
56
+ * @returns {*} {ResultPromise}
57
+ */
58
+ (): ResultPromise;
59
+ /**
60
+ * A runBin implementation that takes string varargs.
61
+ * @param {...RunBinArgs} args
62
+ * @returns {*} {ResultPromise}
63
+ */
64
+ (...args: [...binArgs: string[]]): ResultPromise;
65
+ /**
66
+ * A runBin implementation that takes an Options object.
67
+ * @param {...RunBinArgs} args
68
+ * @returns {*} {ResultPromise}
52
69
  */
53
- (): execa.ExecaChildProcess<string>;
70
+ (...args: [execaOptions: Options]): ResultPromise;
54
71
  /**
55
- * A runBin implementation that takes varargs.
56
- *
72
+ * A runBin implementation that takes string or an Options object varargs.
57
73
  * @param {...RunBinArgs} args
58
- * @returns {*} {execa.ExecaChildProcess<string>}
59
- * @memberof RunBin
74
+ * @returns {*} {ResultPromise}
60
75
  */
61
- (...args: RunBinArgs): execa.ExecaChildProcess<string>;
76
+ (...args: [...binArgs: string[], execaOptions: Options]): ResultPromise;
62
77
  }
78
+ /**
79
+ * The result returned by createBinTester.
80
+ */
63
81
  interface CreateBinTesterResult<TProject extends BinTesterProject> {
64
82
  /**
65
83
  * Runs the configured bin function via execa.
@@ -74,17 +92,21 @@ interface CreateBinTesterResult<TProject extends BinTesterProject> {
74
92
  */
75
93
  setupTmpDir: () => Promise<string>;
76
94
  /**
77
- * Tears the project down, ensuring the tmp directory is removed. Shoud be paired with setupProject.
95
+ * Tears the project down, ensuring the tmp directory is removed.
96
+ * When BIN_TESTER_DEBUG is set, fixtures are preserved for inspection.
78
97
  */
79
98
  teardownProject: () => void;
99
+ /**
100
+ * Runs the configured bin with Node inspector enabled in attach mode (--inspect).
101
+ * Set BIN_TESTER_DEBUG=break to break on first line instead.
102
+ */
103
+ runBinDebug: RunBin;
80
104
  }
81
- declare type RunBinArgs = [...binArgs: string[], execaOptions: execa.Options<string>];
82
105
  /**
83
106
  * Creates the bin tester API functions to use within tests.
84
- *
85
107
  * @param {BinTesterOptions<TProject>} options - An object of bin tester options
86
108
  * @returns {CreateBinTesterResult<TProject>} - A project instance.
87
109
  */
88
110
  declare function createBinTester<TProject extends BinTesterProject>(options: BinTesterOptions<TProject>): CreateBinTesterResult<TProject>;
89
111
 
90
- export { BinTesterProject, createBinTester };
112
+ export { type BinTesterOptions, BinTesterProject, type CreateBinTesterResult, type RunBin, createBinTester };
package/dist/index.js CHANGED
@@ -1,28 +1,17 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
- var __hasOwnProp = Object.prototype.hasOwnProperty;
4
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
- var __spreadValues = (a, b) => {
7
- for (var prop in b || (b = {}))
8
- if (__hasOwnProp.call(b, prop))
9
- __defNormalProp(a, prop, b[prop]);
10
- if (__getOwnPropSymbols)
11
- for (var prop of __getOwnPropSymbols(b)) {
12
- if (__propIsEnum.call(b, prop))
13
- __defNormalProp(a, prop, b[prop]);
14
- }
15
- return a;
16
- };
17
-
18
1
  // src/create-bin-tester.ts
19
- import execa2 from "execa";
2
+ import { execaNode } from "execa";
20
3
 
21
4
  // src/project.ts
22
- import execa from "execa";
5
+ import { execa } from "execa";
23
6
  import { Project } from "fixturify-project";
24
7
  var ROOT = process.cwd();
25
8
  var BinTesterProject = class extends Project {
9
+ /**
10
+ * Constructs an instance of a BinTesterProject.
11
+ * @param {string} name - The name of the project. Used within the package.json as the name property.
12
+ * @param {string} version - The version of the project. Used within the package.json as the version property.
13
+ * @param {Function} cb - An optional callback for additional setup steps after the project is constructed.
14
+ */
26
15
  constructor(name = "fake-project", version, cb) {
27
16
  super(name, version, cb);
28
17
  this._dirChanged = false;
@@ -32,14 +21,31 @@ var BinTesterProject = class extends Project {
32
21
  repository: "http://fakerepo.com"
33
22
  });
34
23
  }
24
+ /**
25
+ * Runs `git init` inside a project.
26
+ * @returns {*} {ResultPromise}
27
+ */
35
28
  gitInit() {
36
- return execa(`git init -q ${this.baseDir}`);
29
+ return execa("git", ["init", "-q", this.baseDir]);
37
30
  }
31
+ /**
32
+ * Writes the project files to disk.
33
+ */
34
+ async write() {
35
+ return super.write();
36
+ }
37
+ /**
38
+ * Changes a directory from inside the project.
39
+ */
38
40
  async chdir() {
39
41
  this._dirChanged = true;
40
42
  await this.write();
41
43
  process.chdir(this.baseDir);
42
44
  }
45
+ /**
46
+ * Correctly disposes of the project, observing when the directory has been changed.
47
+ * @returns {void}
48
+ */
43
49
  dispose() {
44
50
  if (this._dirChanged) {
45
51
  process.chdir(ROOT);
@@ -54,28 +60,59 @@ var DEFAULT_BIN_TESTER_OPTIONS = {
54
60
  };
55
61
  function parseArgs(args) {
56
62
  if (args.length > 0 && typeof args[args.length - 1] === "object") {
57
- const execaOptions = args.pop();
63
+ const argsCopy = [...args];
64
+ const execaOptions = argsCopy.pop();
58
65
  return {
59
- args,
66
+ args: argsCopy,
60
67
  execaOptions
61
68
  };
62
69
  } else {
63
70
  return {
64
- args,
71
+ args: [...args],
65
72
  execaOptions: {}
66
73
  };
67
74
  }
68
75
  }
69
76
  function createBinTester(options) {
70
77
  let project;
71
- const mergedOptions = __spreadValues(__spreadValues({}, DEFAULT_BIN_TESTER_OPTIONS), options);
78
+ const mergedOptions = {
79
+ ...DEFAULT_BIN_TESTER_OPTIONS,
80
+ ...options
81
+ };
72
82
  function runBin(...args) {
73
83
  const mergedRunOptions = parseArgs(args);
74
84
  const binPath = typeof mergedOptions.binPath === "function" ? mergedOptions.binPath(project) : mergedOptions.binPath;
75
- return execa2(process.execPath, [binPath, ...mergedOptions.staticArgs, ...mergedRunOptions.args], __spreadValues({
85
+ const optionsEnv = mergedRunOptions.execaOptions.env;
86
+ const debugEnv = optionsEnv?.BIN_TESTER_DEBUG ?? process.env.BIN_TESTER_DEBUG;
87
+ const nodeOptions = [];
88
+ if (debugEnv && debugEnv !== "0" && debugEnv.toLowerCase() !== "false") {
89
+ if (debugEnv.toLowerCase() === "break") {
90
+ nodeOptions.push("--inspect-brk=0");
91
+ } else {
92
+ nodeOptions.push("--inspect=0");
93
+ }
94
+ console.log(`[bin-tester] Debugging enabled. Fixture: ${project.baseDir}`);
95
+ }
96
+ const resolvedCwd = mergedRunOptions.execaOptions.cwd ?? project.baseDir;
97
+ return execaNode(binPath, [...mergedOptions.staticArgs, ...mergedRunOptions.args], {
76
98
  reject: false,
77
- cwd: project.baseDir
78
- }, mergedRunOptions.execaOptions));
99
+ cwd: resolvedCwd,
100
+ nodeOptions,
101
+ ...mergedRunOptions.execaOptions
102
+ });
103
+ }
104
+ function runBinDebug(...args) {
105
+ const parsedArgs = parseArgs(args);
106
+ const debugEnv = process.env.BIN_TESTER_DEBUG || "attach";
107
+ parsedArgs.execaOptions = {
108
+ ...parsedArgs.execaOptions,
109
+ env: {
110
+ ...parsedArgs.execaOptions.env,
111
+ BIN_TESTER_DEBUG: debugEnv
112
+ }
113
+ };
114
+ const reconstructedArgs = [...parsedArgs.args, parsedArgs.execaOptions];
115
+ return runBin(...reconstructedArgs);
79
116
  }
80
117
  async function setupProject() {
81
118
  project = "createProject" in mergedOptions ? await mergedOptions.createProject() : new BinTesterProject();
@@ -89,10 +126,16 @@ function createBinTester(options) {
89
126
  return project.baseDir;
90
127
  }
91
128
  function teardownProject() {
129
+ const debugEnv = process.env.BIN_TESTER_DEBUG;
130
+ if (debugEnv && debugEnv !== "0" && debugEnv.toLowerCase() !== "false") {
131
+ console.log(`[bin-tester] Fixture preserved: ${project.baseDir}`);
132
+ return;
133
+ }
92
134
  project.dispose();
93
135
  }
94
136
  return {
95
137
  runBin,
138
+ runBinDebug,
96
139
  setupProject,
97
140
  teardownProject,
98
141
  setupTmpDir
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalvert/bin-tester",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "A test harness to invoke a CLI in a tmp directory",
5
5
  "keywords": [
6
6
  "cli",
@@ -18,9 +18,9 @@
18
18
  "type": "module",
19
19
  "exports": {
20
20
  ".": {
21
- "require": "./dist/index.cjs",
21
+ "types": "./dist/index.d.ts",
22
22
  "import": "./dist/index.js",
23
- "types": "./dist/index.d.ts"
23
+ "require": "./dist/index.cjs"
24
24
  }
25
25
  },
26
26
  "main": "./dist/index.cjs",
@@ -28,8 +28,12 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "scripts": {
30
30
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
31
- "docs:generate": "readme-api-generator ./src --ts",
32
- "lint": "eslint .",
31
+ "docs": "typedoc",
32
+ "docs:check": "markdown-code check",
33
+ "docs:sync": "markdown-code sync",
34
+ "format": "prettier --write .",
35
+ "format:check": "prettier --check .",
36
+ "lint": "eslint . && npm run format:check && npm run docs:check",
33
37
  "prepublishOnly": "npm run build",
34
38
  "test": "npm run lint && npm run test:vitest",
35
39
  "test:vitest": "vitest run",
@@ -40,36 +44,37 @@
40
44
  "dist"
41
45
  ],
42
46
  "dependencies": {
43
- "execa": "^5.1.1",
44
- "fixturify-project": "^5.0.2"
47
+ "execa": "^9.6.1",
48
+ "fixturify-project": "^7.1.3"
45
49
  },
46
50
  "devDependencies": {
47
- "@babel/plugin-proposal-class-properties": "^7.16.7",
48
- "@babel/plugin-proposal-object-rest-spread": "^7.17.3",
49
- "@babel/plugin-transform-typescript": "^7.16.8",
50
- "@babel/preset-env": "^7.16.11",
51
- "@babel/preset-typescript": "^7.16.7",
52
- "@scalvert/readme-api-generator": "^0.2.4",
53
- "@typescript-eslint/eslint-plugin": "^5.14.0",
54
- "@typescript-eslint/parser": "^5.14.0",
55
- "eslint": "^8.10.0",
56
- "eslint-config-prettier": "^8.5.0",
57
- "eslint-plugin-jsdoc": "^38.0.4",
51
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
52
+ "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
53
+ "@babel/plugin-transform-typescript": "^7.27.0",
54
+ "@babel/preset-env": "^7.26.9",
55
+ "@babel/preset-typescript": "^7.27.0",
56
+ "@release-it-plugins/lerna-changelog": "^8.0.1",
57
+ "@typescript-eslint/eslint-plugin": "^8.29.1",
58
+ "@typescript-eslint/parser": "^8.29.1",
59
+ "eslint": "^9.24.0",
60
+ "eslint-config-prettier": "^10.1.2",
61
+ "eslint-plugin-jsdoc": "^61.5.0",
58
62
  "eslint-plugin-node": "^11.1.0",
59
- "eslint-plugin-prettier": "^4.0.0",
60
- "eslint-plugin-unicorn": "^41.0.0",
61
- "fixturify": "^2.1.1",
62
- "prettier": "^2.5.1",
63
- "release-it": "^14.2.1",
64
- "release-it-lerna-changelog": "^3.1.0",
65
- "tsup": "^5.12.0",
66
- "type-fest": "^2.12.0",
67
- "typescript": "^4.6.2",
68
- "vite": "^2.8.6",
69
- "vitest": "^0.9.3"
63
+ "eslint-plugin-prettier": "^5.2.6",
64
+ "eslint-plugin-unicorn": "^62.0.0",
65
+ "fixturify": "^3.0.0",
66
+ "markdown-code": "^0.6.1",
67
+ "prettier": "^3.5.3",
68
+ "release-it": "^19.2.2",
69
+ "tsup": "^8.4.0",
70
+ "type-fest": "^5.3.1",
71
+ "typedoc": "^0.28.15",
72
+ "typescript": "^5.8.3",
73
+ "vite": "^7.3.0",
74
+ "vitest": "^4.0.16"
70
75
  },
71
76
  "engines": {
72
- "node": "^12.22.0 || >=14"
77
+ "node": ">=22"
73
78
  },
74
79
  "publishConfig": {
75
80
  "registry": "https://registry.npmjs.org",
@@ -77,7 +82,7 @@
77
82
  },
78
83
  "release-it": {
79
84
  "plugins": {
80
- "release-it-lerna-changelog": {
85
+ "@release-it-plugins/lerna-changelog": {
81
86
  "infile": "CHANGELOG.md",
82
87
  "launchEditor": true
83
88
  }
@@ -89,9 +94,5 @@
89
94
  "release": true,
90
95
  "tokenRef": "GITHUB_AUTH"
91
96
  }
92
- },
93
- "volta": {
94
- "node": "14.19.0",
95
- "npm": "8.5.3"
96
97
  }
97
98
  }