first-base 3.0.0 → 4.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.
@@ -0,0 +1,2 @@
1
+ import type { RunContext } from "./run-context";
2
+ export declare const allInflightRunContexts: Set<RunContext>;
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.allInflightRunContexts = void 0;
4
+ exports.allInflightRunContexts = new Set();
@@ -0,0 +1,14 @@
1
+ export type AwaitableBufferRequest = {
2
+ value: string | RegExp;
3
+ resolve: () => void;
4
+ reject: (error: Error) => void;
5
+ };
6
+ export declare class AwaitableBuffer {
7
+ private _content;
8
+ private _requests;
9
+ request(value: string | RegExp): Promise<void>;
10
+ addContent(data: string): void;
11
+ private _check;
12
+ clearContent(): void;
13
+ cancelRequests(errorMaker: (request: AwaitableBufferRequest) => Error): void;
14
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AwaitableBuffer = void 0;
7
+ const defer_1 = __importDefault(require("@suchipi/defer"));
8
+ const strip_ansi_1 = __importDefault(require("strip-ansi"));
9
+ class AwaitableBuffer {
10
+ _content = "";
11
+ _requests = new Set();
12
+ request(value) {
13
+ const defer = new defer_1.default();
14
+ const request = {
15
+ value,
16
+ resolve: () => {
17
+ this._requests.delete(request);
18
+ defer.resolve();
19
+ },
20
+ reject: (error) => {
21
+ this._requests.delete(request);
22
+ defer.reject(error);
23
+ },
24
+ };
25
+ this._requests.add(request);
26
+ this._check();
27
+ return defer.promise;
28
+ }
29
+ addContent(data) {
30
+ this._content += data;
31
+ this._check();
32
+ }
33
+ _check() {
34
+ for (const request of this._requests) {
35
+ if (typeof request.value === "string") {
36
+ if ((0, strip_ansi_1.default)(this._content).indexOf(request.value) != -1) {
37
+ request.resolve();
38
+ }
39
+ }
40
+ else if (request.value instanceof RegExp) {
41
+ if (request.value.test((0, strip_ansi_1.default)(this._content))) {
42
+ request.resolve();
43
+ }
44
+ }
45
+ }
46
+ }
47
+ clearContent() {
48
+ this._content = "";
49
+ }
50
+ cancelRequests(errorMaker) {
51
+ for (const request of this._requests) {
52
+ request.reject(errorMaker(request));
53
+ }
54
+ }
55
+ }
56
+ exports.AwaitableBuffer = AwaitableBuffer;
package/dist/index.d.ts CHANGED
@@ -1,44 +1,5 @@
1
- import { sanitizers } from "./sanitizers";
2
- export type Options = {
3
- cwd?: string;
4
- env?: {
5
- [varName: string]: string | undefined;
6
- };
7
- argv0?: string;
8
- detached?: boolean;
9
- uid?: number;
10
- gid?: number;
11
- shell?: boolean | string;
12
- windowsVerbatimArguments?: boolean;
13
- windowsHide?: boolean;
14
- pty?: boolean;
15
- debug?: boolean;
16
- };
17
- export type RunContext = {
18
- result: {
19
- stdout: string;
20
- stderr: string;
21
- code: null | number;
22
- error: null | Error;
23
- };
24
- cleanResult(): {
25
- stdout: string;
26
- stderr: string;
27
- code: null | number;
28
- error: null | Error;
29
- };
30
- completion: Promise<void>;
31
- /** @deprecated pass `debug: true` as an option to {@link spawn} instead. */
32
- debug(): RunContext;
33
- outputContains(value: string | RegExp): Promise<void>;
34
- clearOutputContainsBuffer(): void;
35
- write(data: string | Buffer): void;
36
- close(stream: "stdin" | "stdout" | "stderr"): void;
37
- kill(signal?: NodeJS.Signals): void;
38
- };
39
- declare const allInflightRunContexts: Set<RunContext>;
40
- declare function spawn(cmd: string): RunContext;
41
- declare function spawn(cmd: string, args: Array<string>): RunContext;
42
- declare function spawn(cmd: string, options: Options): RunContext;
43
- declare function spawn(cmd: string, args: Array<string>, options: Options): RunContext;
44
- export { spawn, sanitizers, allInflightRunContexts };
1
+ export { spawn } from "./spawn";
2
+ export type { SpawnOptions as Options } from "./spawn-options";
3
+ export { allInflightRunContexts } from "./all-inflight-run-contexts";
4
+ export { sanitizers, type ReplaceRootDir as ReplaceRootDirSanitizer, } from "./sanitizers";
5
+ export type { RunContext, NonPtyRunContext, PtyRunContext, NonPtyRunContextResult, PtyRunContextResult, } from "./run-context";
package/dist/index.js CHANGED
@@ -1,289 +1,9 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.allInflightRunContexts = exports.sanitizers = void 0;
7
- exports.spawn = spawn;
8
- const child_process_1 = require("child_process");
9
- const strip_ansi_1 = __importDefault(require("strip-ansi"));
10
- const sanitizers_1 = require("./sanitizers");
3
+ exports.sanitizers = exports.allInflightRunContexts = exports.spawn = void 0;
4
+ var spawn_1 = require("./spawn");
5
+ Object.defineProperty(exports, "spawn", { enumerable: true, get: function () { return spawn_1.spawn; } });
6
+ var all_inflight_run_contexts_1 = require("./all-inflight-run-contexts");
7
+ Object.defineProperty(exports, "allInflightRunContexts", { enumerable: true, get: function () { return all_inflight_run_contexts_1.allInflightRunContexts; } });
8
+ var sanitizers_1 = require("./sanitizers");
11
9
  Object.defineProperty(exports, "sanitizers", { enumerable: true, get: function () { return sanitizers_1.sanitizers; } });
12
- const allInflightRunContexts = new Set();
13
- exports.allInflightRunContexts = allInflightRunContexts;
14
- function spawn(cmd, argsOrOptions, passedOptions) {
15
- let args;
16
- let options;
17
- if (Array.isArray(argsOrOptions)) {
18
- args = argsOrOptions;
19
- }
20
- else if (typeof argsOrOptions === "object") {
21
- options = argsOrOptions;
22
- }
23
- if (passedOptions && !options) {
24
- options = passedOptions;
25
- }
26
- if (!args) {
27
- args = [];
28
- }
29
- if (!options) {
30
- options = {};
31
- }
32
- let child;
33
- let stdin;
34
- let stdout;
35
- let stderr;
36
- let unreffable = null;
37
- let running;
38
- let debug = options.debug ?? false;
39
- let outputContainsBuffer = "";
40
- let pendingOutputContainsRequests = new Set();
41
- const disposables = [];
42
- const debugLog = (...msg) => {
43
- if (debug) {
44
- console.log(...msg);
45
- }
46
- };
47
- const runContext = {
48
- result: {
49
- // All of the stdout and stderr the process has written so far.
50
- stdout: "",
51
- stderr: "",
52
- // Exit status code, if the process has finished.
53
- code: null,
54
- // if the process errored out, this will be the Error
55
- error: null,
56
- },
57
- // Return a version of result which has had the string sanitizers run on it
58
- cleanResult() {
59
- return Object.assign({}, runContext.result, {
60
- stdout: sanitizers_1.sanitizers.reduce((str, transformFn) => transformFn(str), runContext.result.stdout),
61
- stderr: sanitizers_1.sanitizers.reduce((str, transformFn) => transformFn(str), runContext.result.stderr),
62
- });
63
- },
64
- // Promise that gets resolved when the child process completes.
65
- // Actual value gets filled in below.
66
- completion: Promise.resolve(),
67
- debug() {
68
- debug = true;
69
- return this;
70
- },
71
- // Returns a Promise that resolves once the child process output
72
- // (combined stdout and stderr) contains the passed string or
73
- // matches the passed RegExp. Ignores ansi control characters.
74
- outputContains(value) {
75
- debugLog(`Waiting for output to contain ${JSON.stringify(value)}...`);
76
- return new Promise((resolve, reject) => {
77
- const request = { value, resolve: undefined, reject: undefined };
78
- request.resolve = () => {
79
- pendingOutputContainsRequests.delete(request);
80
- resolve();
81
- };
82
- request.reject = (error) => {
83
- pendingOutputContainsRequests.delete(request);
84
- reject(error);
85
- };
86
- pendingOutputContainsRequests.add(request);
87
- });
88
- },
89
- clearOutputContainsBuffer() {
90
- outputContainsBuffer = "";
91
- },
92
- // Call this function to write into stdin.
93
- write(data) {
94
- stdin.write(data);
95
- },
96
- // Call this function to close stdin, stdout, or stderr.
97
- close(stream) {
98
- switch (String(stream).toLowerCase()) {
99
- case "stdin": {
100
- if ("end" in stdin) {
101
- stdin.end();
102
- }
103
- break;
104
- }
105
- case "stdout": {
106
- if ("destroy" in stdout) {
107
- stdout.destroy();
108
- }
109
- break;
110
- }
111
- case "stderr": {
112
- if (stderr != null && "destroy" in stderr) {
113
- stderr.destroy();
114
- }
115
- break;
116
- }
117
- default: {
118
- throw new Error(`Invalid stream name: '${stream}'. Valid names are 'stdin', 'stdout', or 'stderr'.`);
119
- }
120
- }
121
- },
122
- // Call this function to send a signal to the child process.
123
- // You can pass "SIGTERM", "SIGKILL", etc. Defaults to "SIGINT".
124
- kill(signal = "SIGINT") {
125
- if (running) {
126
- child.kill(signal);
127
- }
128
- if (unreffable != null) {
129
- unreffable.unref();
130
- }
131
- },
132
- };
133
- if (options.pty) {
134
- debugLog("pty option was true; using node-pty");
135
- const ptySpawn = require("@lydell/node-pty").spawn;
136
- const ptyChild = ptySpawn(cmd, args, options);
137
- child = ptyChild;
138
- stdin = ptyChild;
139
- stdout = ptyChild;
140
- stderr = null; // no way to tell between stdout and stderr with pty
141
- // no unreffable equivalent on ptyChild
142
- }
143
- else {
144
- debugLog("pty option was NOT true; using child_process");
145
- const nonPtyChild = (0, child_process_1.spawn)(cmd, args, options);
146
- child = nonPtyChild;
147
- stdin = nonPtyChild.stdin;
148
- stdout = nonPtyChild.stdout;
149
- stderr = nonPtyChild.stderr;
150
- unreffable = nonPtyChild;
151
- }
152
- running = true;
153
- allInflightRunContexts.add(runContext);
154
- if ("on" in child) {
155
- debugLog("using 'on' method to listen for child spawn event");
156
- child.on("spawn", () => {
157
- debugLog("'spawn' event");
158
- });
159
- }
160
- else {
161
- debugLog("child had no 'on' method, so child spawn event listener wasn't set up");
162
- }
163
- const checkForPendingOutputRequestsToResolve = () => {
164
- pendingOutputContainsRequests.forEach((request) => {
165
- if (typeof request.value === "string") {
166
- if ((0, strip_ansi_1.default)(outputContainsBuffer).indexOf(request.value) != -1) {
167
- request.resolve();
168
- }
169
- }
170
- else if (request.value instanceof RegExp) {
171
- if (request.value.test((0, strip_ansi_1.default)(outputContainsBuffer))) {
172
- request.resolve();
173
- }
174
- }
175
- });
176
- };
177
- if ("setEncoding" in stdout) {
178
- debugLog("setting stdout encoding to utf-8");
179
- stdout.setEncoding("utf-8");
180
- }
181
- else {
182
- debugLog("not setting stdout encoding because the setEncoding method was not present");
183
- }
184
- const handleStdoutData = (data) => {
185
- runContext.result.stdout += data;
186
- outputContainsBuffer += data;
187
- debugLog(`STDOUT: ${data.toString()}`);
188
- checkForPendingOutputRequestsToResolve();
189
- };
190
- if ("onData" in stdout) {
191
- debugLog("using 'onData' method to listen for stdout data event");
192
- // the pty instance returned by node-pty
193
- // requires attaching handlers differently
194
- stdout.onData(handleStdoutData);
195
- }
196
- else {
197
- debugLog("using 'on' method to listen for stdout data event");
198
- stdout.on("data", handleStdoutData);
199
- }
200
- if (stderr) {
201
- debugLog("setting stderr encoding to utf-8");
202
- stderr.setEncoding("utf-8");
203
- // this is never a pty instance,
204
- // so we don't need to deal with onData here:
205
- debugLog("using 'on' method to listen for stderr data event");
206
- stderr.on("data", (data) => {
207
- runContext.result.stderr += data;
208
- outputContainsBuffer += data;
209
- debugLog(`STDERR: ${data.toString()}`);
210
- checkForPendingOutputRequestsToResolve();
211
- });
212
- }
213
- else {
214
- debugLog("stderr isn't present (pty mixes stdout and stderr together), so not setting encoding or setting up data event listener for stderr");
215
- }
216
- runContext.completion = new Promise((resolve) => {
217
- let hasFinished = false;
218
- const finish = (reason) => {
219
- debugLog("in finish", runContext.result);
220
- if (hasFinished) {
221
- debugLog("finish called more than once; ignoring");
222
- }
223
- else {
224
- running = false;
225
- allInflightRunContexts.delete(runContext);
226
- resolve();
227
- for (const request of pendingOutputContainsRequests) {
228
- request.reject(new Error(`Child process ${reason} before its output contained the requested content: ${request.value}`));
229
- }
230
- for (const disposable of disposables) {
231
- disposable.dispose();
232
- }
233
- hasFinished = true;
234
- }
235
- };
236
- if ("on" in child) {
237
- debugLog("using 'on' method to listen for child close event");
238
- child.on("close", (code, signal) => {
239
- debugLog("'close' event", { code, signal });
240
- if (code != null) {
241
- runContext.result.code = code;
242
- }
243
- });
244
- }
245
- else {
246
- debugLog("child had no 'on' method, so child close event listener wasn't set up");
247
- }
248
- if ("onExit" in child) {
249
- debugLog("using 'onExit' method to listen for child exit event");
250
- const disposable = child.onExit(({ exitCode, signal }) => {
251
- debugLog("onExit", { exitCode, signal });
252
- if (exitCode != null) {
253
- runContext.result.code = exitCode;
254
- }
255
- finish("exited");
256
- });
257
- disposables.push(disposable);
258
- }
259
- else {
260
- debugLog("using 'on' method to listen for child exit event");
261
- child.on("exit", (code) => {
262
- debugLog("'exit' event", { code });
263
- if (code != null) {
264
- runContext.result.code = code;
265
- }
266
- finish("exited");
267
- });
268
- }
269
- if ("on" in child) {
270
- debugLog("using 'on' method to listen for child error event");
271
- child.on("error", (error) => {
272
- debugLog("'error' event", { error });
273
- if (typeof error === "object" &&
274
- error !== null &&
275
- error.code === "EIO") {
276
- // not real; process is about to exit
277
- debugLog("Ignoring spurious EIO error:", error);
278
- return;
279
- }
280
- runContext.result.error = error;
281
- finish("errored");
282
- });
283
- }
284
- else {
285
- debugLog("child had no 'on' method, so child error event listener wasn't set up");
286
- }
287
- });
288
- return runContext;
289
- }
@@ -0,0 +1,94 @@
1
+ export type PtyRunContextResult = {
2
+ /** All of the output the process has written so far (pty mode combines stdout and stderr). */
3
+ output: string;
4
+ /** Exit status code, if the process has finished. */
5
+ code: null | number;
6
+ /** If the process errored out (ie. failed to spawn, etc), this will be the Error. */
7
+ error: null | Error;
8
+ };
9
+ export type PtyRunContext = {
10
+ /** Outputs of the running or completed process. */
11
+ result: PtyRunContextResult;
12
+ /** Returns a version of {@link PtyRunContext["result"]} with all the {@link sanitizers} run over it. */
13
+ cleanResult(): PtyRunContextResult;
14
+ /**
15
+ * Resolves after the node-pty process's "onExit" callback is called.
16
+ */
17
+ completion: Promise<void>;
18
+ /**
19
+ * Returns a Promise that resolves once the child process output
20
+ * (combined stdout and stderr) contains the passed string or
21
+ * matches the passed RegExp. Ignores ansi control characters.
22
+ */
23
+ outputContains(value: string | RegExp): Promise<void>;
24
+ /**
25
+ * Call this to reset the buffer used by the process's `outputContains`
26
+ * tracking. This is needed if you want to wait for the same output to appear
27
+ * a second time.
28
+ */
29
+ clearOutputContainsBuffer(): void;
30
+ /** Call this to write to the child's stdin. */
31
+ write(data: string | Buffer): void;
32
+ /**
33
+ * Call this function to send a signal to the child process.
34
+ * You can pass "SIGTERM", "SIGKILL", etc. Defaults to "SIGINT".
35
+ */
36
+ kill(signal?: NodeJS.Signals): void;
37
+ /** Indicates that the process was spawned in a pseudo-tty. */
38
+ pty: true;
39
+ };
40
+ export type NonPtyRunContextResult = {
41
+ /** All of the stdout the process has written so far. */
42
+ stdout: string;
43
+ /** All of the stderr the process has written so far. */
44
+ stderr: string;
45
+ /** Exit status code, if the process has finished. */
46
+ code: null | number;
47
+ /** If the process errored out (ie. failed to spawn, etc), this will be the Error. */
48
+ error: null | Error;
49
+ };
50
+ export type NonPtyRunContext = {
51
+ /** Outputs of the running or completed process. */
52
+ result: NonPtyRunContextResult;
53
+ /** Returns a version of {@link NonPtyRunContext["result"]} with all the {@link sanitizers} run over it. */
54
+ cleanResult(): NonPtyRunContextResult;
55
+ /**
56
+ * Resolves after the child process has emitted its "exit" event AND its
57
+ * "close" event.
58
+ */
59
+ completion: Promise<void>;
60
+ /**
61
+ * Resolves after the first time the child process emits the corresponding
62
+ * event.
63
+ */
64
+ eventFired(eventName: "spawn"): Promise<void>;
65
+ eventFired(eventName: "error"): Promise<Error & {
66
+ code?: string;
67
+ }>;
68
+ eventFired(eventName: "exit"): Promise<[code: number | null, signal: NodeJS.Signals | null]>;
69
+ eventFired(eventName: "close"): Promise<[code: number | null, signal: NodeJS.Signals | null]>;
70
+ /**
71
+ * Returns a Promise that resolves once the child process output
72
+ * (combined stdout and stderr) contains the passed string or
73
+ * matches the passed RegExp. Ignores ansi control characters.
74
+ */
75
+ outputContains(value: string | RegExp): Promise<void>;
76
+ /**
77
+ * Call this to reset the buffer used by the process's `outputContains`
78
+ * tracking. This is needed if you want to wait for the same output to appear
79
+ * a second time.
80
+ */
81
+ clearOutputContainsBuffer(): void;
82
+ /** Call this to write to the child's stdin. */
83
+ write(data: string | Buffer): void;
84
+ /** Call this to close one of the child process's stdio streams. */
85
+ close(stream: "stdin" | "stdout" | "stderr"): void;
86
+ /**
87
+ * Call this function to send a signal to the child process.
88
+ * You can pass "SIGTERM", "SIGKILL", etc. Defaults to "SIGINT".
89
+ */
90
+ kill(signal?: NodeJS.Signals): void;
91
+ /** Indicates that the process was NOT spawned in a pseudo-tty. */
92
+ pty: false;
93
+ };
94
+ export type RunContext<IsPty extends boolean = boolean> = IsPty extends true ? PtyRunContext : IsPty extends false ? NonPtyRunContext : PtyRunContext | NonPtyRunContext;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1 +1,19 @@
1
+ export interface ReplaceRootDir {
2
+ (str: string): string;
3
+ /**
4
+ * If you want to bypass the builtin "find root dir" logic used by the
5
+ * replaceRootDir sanitizer, you can run:
6
+ *
7
+ * ```ts
8
+ * const replaceRootDir = sanitizers.find(fn => fn.name === "replaceRootDir");
9
+ * replaceRootDir.cache.set(process.cwd(), "/home/me/my-project");
10
+ * ```
11
+ *
12
+ * If `process.cwd()` changes during the course of a test run, you'll need to
13
+ * add cache entries for the other locations, too. If that's too inconvenient,
14
+ * you're welcome to remove replaceRootDir from the sanitizers array and
15
+ * optionally replace it with your own implementation.
16
+ */
17
+ cache: Map<string, string>;
18
+ }
1
19
  export declare const sanitizers: Array<(str: string) => string>;
@@ -17,6 +17,13 @@ const replaceRootDir = Object.assign(function _replaceRootDir(str) {
17
17
  }, {
18
18
  cache: new Map(),
19
19
  });
20
+ // Minifier protection so the above documented code snippet remains possible
21
+ Object.defineProperty(replaceRootDir, "name", {
22
+ configurable: true,
23
+ enumerable: false,
24
+ writable: false,
25
+ value: "replaceRootDir",
26
+ });
20
27
  exports.sanitizers = [
21
28
  strip_ansi_1.default,
22
29
  replaceRootDir,
@@ -0,0 +1,3 @@
1
+ import type { NonPtyRunContext } from "./run-context";
2
+ import type { SpawnOptions } from "./spawn-options";
3
+ export declare function spawnNonPty(cmd: string, args: Array<string>, options: SpawnOptions, debugLog: (...msg: Array<any>) => void): NonPtyRunContext;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.spawnNonPty = spawnNonPty;
7
+ const child_process_1 = require("child_process");
8
+ const defer_1 = __importDefault(require("@suchipi/defer"));
9
+ const sanitizers_1 = require("./sanitizers");
10
+ const all_inflight_run_contexts_1 = require("./all-inflight-run-contexts");
11
+ const awaitable_buffer_1 = require("./awaitable-buffer");
12
+ function spawnNonPty(cmd, args, options, debugLog) {
13
+ debugLog("in spawnNonPtyRunContext");
14
+ let child;
15
+ let stdin;
16
+ let stdout;
17
+ let stderr;
18
+ let running;
19
+ const outputBuffer = new awaitable_buffer_1.AwaitableBuffer();
20
+ const eventDefers = {
21
+ exit: new defer_1.default(),
22
+ error: new defer_1.default(),
23
+ spawn: new defer_1.default(),
24
+ close: new defer_1.default(),
25
+ };
26
+ const runContext = {
27
+ pty: false,
28
+ result: {
29
+ stdout: "",
30
+ stderr: "",
31
+ code: null,
32
+ error: null,
33
+ },
34
+ cleanResult() {
35
+ return {
36
+ ...runContext.result,
37
+ stdout: sanitizers_1.sanitizers.reduce((str, transformFn) => transformFn(str), runContext.result.stdout),
38
+ stderr: sanitizers_1.sanitizers.reduce((str, transformFn) => transformFn(str), runContext.result.stderr),
39
+ };
40
+ },
41
+ // Placeholder; real value gets filled in below.
42
+ completion: Promise.resolve(),
43
+ outputContains(value) {
44
+ debugLog(`Waiting for output to contain ${typeof value === "string" ? JSON.stringify(value) : String(value)}...`);
45
+ return outputBuffer.request(value);
46
+ },
47
+ eventFired(eventName) {
48
+ const defer = eventDefers[eventName];
49
+ if (defer != null) {
50
+ return defer.promise;
51
+ }
52
+ throw new Error(`Invalid event name: ${JSON.stringify(eventName)}`);
53
+ },
54
+ clearOutputContainsBuffer() {
55
+ outputBuffer.clearContent();
56
+ },
57
+ write(data) {
58
+ stdin.write(data);
59
+ },
60
+ close(stream) {
61
+ switch (String(stream).toLowerCase()) {
62
+ case "stdin": {
63
+ if ("end" in stdin) {
64
+ stdin.end();
65
+ }
66
+ break;
67
+ }
68
+ case "stdout": {
69
+ stdout.destroy();
70
+ break;
71
+ }
72
+ case "stderr": {
73
+ stderr.destroy();
74
+ break;
75
+ }
76
+ default: {
77
+ throw new Error(`Invalid stream name: '${stream}'. Valid names are 'stdin', 'stdout', or 'stderr'.`);
78
+ }
79
+ }
80
+ },
81
+ kill(signal = "SIGINT") {
82
+ if (running) {
83
+ child.kill(signal);
84
+ }
85
+ child.unref();
86
+ },
87
+ };
88
+ child = (0, child_process_1.spawn)(cmd, args, options);
89
+ stdin = child.stdin;
90
+ stdout = child.stdout;
91
+ stderr = child.stderr;
92
+ running = true;
93
+ all_inflight_run_contexts_1.allInflightRunContexts.add(runContext);
94
+ debugLog("setting up spawn event listener");
95
+ child.on("spawn", () => {
96
+ debugLog("'spawn' event");
97
+ eventDefers.spawn?.resolve();
98
+ });
99
+ debugLog("setting stdout encoding to utf-8");
100
+ stdout.setEncoding("utf-8");
101
+ debugLog("setting up stdout data event listener");
102
+ stdout.on("data", (data) => {
103
+ debugLog("STDOUT: ", data);
104
+ runContext.result.stdout += data;
105
+ outputBuffer.addContent(data);
106
+ });
107
+ debugLog("setting stderr encoding to utf-8");
108
+ stderr.setEncoding("utf-8");
109
+ debugLog("setting up stderr data event listener");
110
+ stderr.on("data", (data) => {
111
+ debugLog("STDERR: ", data);
112
+ runContext.result.stderr += data;
113
+ outputBuffer.addContent(data);
114
+ });
115
+ const completionDefer = new defer_1.default();
116
+ let hasClosed = false;
117
+ let hasExited = false;
118
+ const endings = {
119
+ complete: () => {
120
+ running = false;
121
+ all_inflight_run_contexts_1.allInflightRunContexts.delete(runContext);
122
+ completionDefer.resolve();
123
+ outputBuffer.cancelRequests((request) => new Error(`Child process closed and exited before its output contained the requested content: ${request.value}`));
124
+ },
125
+ fail: (error) => {
126
+ running = false;
127
+ runContext.result.error = error;
128
+ all_inflight_run_contexts_1.allInflightRunContexts.delete(runContext);
129
+ completionDefer.reject(error);
130
+ outputBuffer.cancelRequests((request) => new Error(`Child process errored before its output contained the requested content: ${request.value}`));
131
+ },
132
+ };
133
+ debugLog("setting up child close event listener");
134
+ child.on("close", (code, signal) => {
135
+ debugLog("'close' event", { code, signal });
136
+ hasClosed = true;
137
+ eventDefers.close?.resolve([code, signal]);
138
+ if (code != null && runContext.result.code == null) {
139
+ runContext.result.code = code;
140
+ }
141
+ if (hasExited) {
142
+ endings.complete();
143
+ }
144
+ });
145
+ debugLog("setting up child exit event listener");
146
+ child.on("exit", (code, signal) => {
147
+ debugLog("'exit' event", { code });
148
+ hasExited = true;
149
+ eventDefers.exit.resolve([code, signal]);
150
+ if (code != null && runContext.result.code == null) {
151
+ runContext.result.code = code;
152
+ }
153
+ if (hasClosed) {
154
+ endings.complete();
155
+ }
156
+ });
157
+ debugLog("setting up child error event listener");
158
+ child.on("error", (error) => {
159
+ debugLog("'error' event", { error });
160
+ eventDefers.error?.resolve(error);
161
+ if (typeof error === "object" && error !== null && error.code === "EIO") {
162
+ // not real; process is about to exit
163
+ debugLog("Ignoring spurious EIO error:", error);
164
+ return;
165
+ }
166
+ endings.fail(error);
167
+ });
168
+ runContext.completion = completionDefer.promise;
169
+ return runContext;
170
+ }
@@ -0,0 +1,15 @@
1
+ export type SpawnOptions = {
2
+ cwd?: string;
3
+ env?: {
4
+ [varName: string]: string | undefined;
5
+ };
6
+ argv0?: string;
7
+ detached?: boolean;
8
+ uid?: number;
9
+ gid?: number;
10
+ shell?: boolean | string;
11
+ windowsVerbatimArguments?: boolean;
12
+ windowsHide?: boolean;
13
+ pty?: boolean;
14
+ debug?: boolean;
15
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import type { PtyRunContext } from "./run-context";
2
+ import type { SpawnOptions } from "./spawn-options";
3
+ export declare function spawnPty(cmd: string, args: Array<string>, options: SpawnOptions, debugLog: (...msg: Array<any>) => void): PtyRunContext;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.spawnPty = spawnPty;
7
+ const defer_1 = __importDefault(require("@suchipi/defer"));
8
+ const sanitizers_1 = require("./sanitizers");
9
+ const all_inflight_run_contexts_1 = require("./all-inflight-run-contexts");
10
+ const awaitable_buffer_1 = require("./awaitable-buffer");
11
+ function spawnPty(cmd, args, options, debugLog) {
12
+ debugLog("in spawnPtyRunContext");
13
+ let child;
14
+ let running;
15
+ const outputBuffer = new awaitable_buffer_1.AwaitableBuffer();
16
+ const disposables = [];
17
+ const runContext = {
18
+ pty: true,
19
+ result: {
20
+ output: "",
21
+ code: null,
22
+ error: null,
23
+ },
24
+ cleanResult() {
25
+ return {
26
+ ...runContext.result,
27
+ output: sanitizers_1.sanitizers.reduce((str, transformFn) => transformFn(str), runContext.result.output),
28
+ };
29
+ },
30
+ // Placeholder; actual value gets filled in below.
31
+ completion: Promise.resolve(),
32
+ outputContains(value) {
33
+ debugLog(`Waiting for output to contain ${typeof value === "string" ? JSON.stringify(value) : String(value)}...`);
34
+ return outputBuffer.request(value);
35
+ },
36
+ clearOutputContainsBuffer() {
37
+ outputBuffer.clearContent();
38
+ },
39
+ write(data) {
40
+ child.write(data);
41
+ },
42
+ kill(signal = "SIGINT") {
43
+ if (running) {
44
+ child.kill(signal);
45
+ }
46
+ },
47
+ };
48
+ const ptySpawn = require("@lydell/node-pty").spawn;
49
+ const ptyChild = ptySpawn(cmd, args, options);
50
+ child = ptyChild;
51
+ running = true;
52
+ all_inflight_run_contexts_1.allInflightRunContexts.add(runContext);
53
+ debugLog("using 'onData' method to listen for child data event");
54
+ disposables.push(child.onData((data) => {
55
+ debugLog(`OUTPUT: ${data.toString()}`);
56
+ runContext.result.output += data;
57
+ outputBuffer.addContent(data);
58
+ }));
59
+ const completionDefer = new defer_1.default();
60
+ debugLog("using 'onExit' method to listen for child exit event");
61
+ disposables.push(child.onExit(({ exitCode, signal }) => {
62
+ debugLog("onExit", { exitCode, signal });
63
+ if (exitCode != null) {
64
+ runContext.result.code = exitCode;
65
+ }
66
+ running = false;
67
+ all_inflight_run_contexts_1.allInflightRunContexts.delete(runContext);
68
+ for (const disposable of disposables) {
69
+ disposable.dispose();
70
+ }
71
+ completionDefer.resolve();
72
+ outputBuffer.cancelRequests((request) => new Error(`Child process exited before its output contained the requested content: ${request.value}`));
73
+ }));
74
+ runContext.completion = completionDefer.promise;
75
+ return runContext;
76
+ }
@@ -0,0 +1,18 @@
1
+ import type { NonPtyRunContext, PtyRunContext } from "./run-context";
2
+ import type { SpawnOptions } from "./spawn-options";
3
+ /**
4
+ * Start a child process and return a {@link RunContext} object to interact with
5
+ * it. Function signature is the same as child_process spawn, except you can
6
+ * pass `pty: true` in options to run the process in a psuedo-tty, and
7
+ * `debug: true` to enable debug output.
8
+ */
9
+ export declare function spawn(cmd: string): NonPtyRunContext;
10
+ export declare function spawn(cmd: string, args: Array<string>): NonPtyRunContext;
11
+ export declare function spawn(cmd: string, options: SpawnOptions & {
12
+ pty: true;
13
+ }): PtyRunContext;
14
+ export declare function spawn(cmd: string, options: SpawnOptions): NonPtyRunContext;
15
+ export declare function spawn(cmd: string, args: Array<string>, options: SpawnOptions & {
16
+ pty: true;
17
+ }): PtyRunContext;
18
+ export declare function spawn(cmd: string, args: Array<string>, options: SpawnOptions): NonPtyRunContext;
package/dist/spawn.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawn = spawn;
4
+ const spawn_non_pty_1 = require("./spawn-non-pty");
5
+ const spawn_pty_1 = require("./spawn-pty");
6
+ function spawn(cmd, argsOrOptions, passedOptions) {
7
+ let args;
8
+ let options;
9
+ if (Array.isArray(argsOrOptions)) {
10
+ args = argsOrOptions;
11
+ }
12
+ else if (typeof argsOrOptions === "object") {
13
+ options = argsOrOptions;
14
+ }
15
+ if (passedOptions && !options) {
16
+ options = passedOptions;
17
+ }
18
+ if (!args) {
19
+ args = [];
20
+ }
21
+ if (!options) {
22
+ options = {};
23
+ }
24
+ const debug = options.debug ?? false;
25
+ const debugLog = (...msg) => {
26
+ if (debug) {
27
+ console.log(...msg);
28
+ }
29
+ };
30
+ if (options.pty) {
31
+ return (0, spawn_pty_1.spawnPty)(cmd, args, options, debugLog);
32
+ }
33
+ else {
34
+ return (0, spawn_non_pty_1.spawnNonPty)(cmd, args, options, debugLog);
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "first-base",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Integration testing for CLI applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,6 +12,7 @@
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
14
  "@lydell/node-pty": "^1.2.0-beta.3",
15
+ "@suchipi/defer": "^1.0.0",
15
16
  "nice-path": "^3.2.2",
16
17
  "strip-ansi": "^5.0.0"
17
18
  },
@@ -22,7 +23,7 @@
22
23
  "vitest": "^4.1.2"
23
24
  },
24
25
  "scripts": {
25
- "build": "mkdir -p dist; rm -rf dist/*; tsc && rm -f dist/*.test.js dist/*.test.d.ts && cp src/index.js.flow dist/",
26
+ "build": "mkdir -p dist; rm -rf dist/*; tsc && rm -f dist/*.test.js dist/*.test.d.ts",
26
27
  "test": "vitest run"
27
28
  }
28
29
  }
@@ -1,43 +0,0 @@
1
- // @flow
2
- export type Options = {
3
- cwd?: ?string,
4
- env?: ?{ [varName: string]: string | undefined },
5
- argv0?: ?string,
6
- detached?: ?boolean,
7
- uid?: ?number,
8
- gid?: ?number,
9
- shell?: ?boolean | string,
10
- windowsVerbatimArguments?: ?boolean,
11
- windowsHide?: ?boolean,
12
- pty?: ?boolean,
13
- debug?: ?boolean,
14
- };
15
-
16
- export type RunContext = {
17
- result: {
18
- stdout: string,
19
- stderr: string,
20
- code: null | number,
21
- error: null | Error,
22
- },
23
- completion: Promise<void>,
24
- debug(): RunContext,
25
- outputContains(value: string | RegExp): Promise<void>,
26
- clearOutputContainsBuffer(): void,
27
- write(data: string | Buffer): void,
28
- close(stream: "stdin" | "stdout" | "stderr"): void,
29
- kill(signal?: string): void,
30
- };
31
-
32
- interface Exports {
33
- spawn(cmd: string): RunContext;
34
- spawn(cmd: string, args: Array<string>): RunContext;
35
- spawn(cmd: string, options: Options): RunContext;
36
- spawn(cmd: string, args: Array<string>, options: Options): RunContext;
37
-
38
- sanitizers: Array<(str: string) => string>;
39
-
40
- allInflightRunContexts: Set<RunContext>;
41
- }
42
-
43
- declare module.exports: Exports;