Skip to content

Commit 7d0943f

Browse files
authored
Improve verbose option (#883)
1 parent de32969 commit 7d0943f

21 files changed

+292
-86
lines changed

docs/scripts.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@ Or:
415415
NODE_DEBUG=execa node file.js
416416
```
417417

418+
Which prints:
419+
420+
```
421+
[19:49:00.360] $ echo example
422+
```
423+
418424
### Piping stdout to another command
419425

420426
```sh

index.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,8 @@ execa('unicorns', {windowsHide: false});
14421442
execaSync('unicorns', {windowsHide: false});
14431443
execa('unicorns', {verbose: false});
14441444
execaSync('unicorns', {verbose: false});
1445+
expectError(execa('unicorns', {verbose: 'other'}));
1446+
expectError(execaSync('unicorns', {verbose: 'other'}));
14451447
/* eslint-enable @typescript-eslint/no-floating-promises */
14461448
expectType<boolean>(execa('unicorns').kill());
14471449
execa('unicorns').kill('SIGKILL');

lib/arguments/options.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {npmRunPathEnv} from 'npm-run-path';
55
import isPlainObject from 'is-plain-obj';
66
import {normalizeForceKillAfterDelay} from '../exit/kill.js';
77
import {validateTimeout} from '../exit/timeout.js';
8+
import {logCommand} from '../verbose/start.js';
89
import {handleNodeOption} from './node.js';
9-
import {logCommand, verboseDefault} from './verbose.js';
1010
import {joinCommand} from './escape.js';
1111
import {normalizeCwd, safeNormalizeFileUrl, normalizeFileUrl} from './cwd.js';
1212

@@ -37,9 +37,13 @@ export const normalizeArguments = (rawFile, rawArgs = [], rawOptions = {}) => {
3737
return [filePath, normalizedArgs, options];
3838
};
3939

40-
export const handleArguments = (filePath, rawArgs, rawOptions) => {
40+
export const handleCommand = (filePath, rawArgs, rawOptions) => {
4141
const {command, escapedCommand} = joinCommand(filePath, rawArgs);
42+
logCommand(escapedCommand, rawOptions);
43+
return {command, escapedCommand};
44+
};
4245

46+
export const handleArguments = (filePath, rawArgs, rawOptions) => {
4347
rawOptions.cwd = normalizeCwd(rawOptions.cwd);
4448
const [processedFile, processedArgs, processedOptions] = handleNodeOption(filePath, rawArgs, rawOptions);
4549

@@ -56,9 +60,7 @@ export const handleArguments = (filePath, rawArgs, rawOptions) => {
5660
args.unshift('/q');
5761
}
5862

59-
logCommand(escapedCommand, options);
60-
61-
return {file, args, command, escapedCommand, options};
63+
return {file, args, options};
6264
};
6365

6466
const addDefaultOptions = ({
@@ -74,7 +76,6 @@ const addDefaultOptions = ({
7476
cleanup = true,
7577
all = false,
7678
windowsHide = true,
77-
verbose = verboseDefault,
7879
killSignal = 'SIGTERM',
7980
forceKillAfterDelay = true,
8081
lines = false,
@@ -94,7 +95,6 @@ const addDefaultOptions = ({
9495
cleanup,
9596
all,
9697
windowsHide,
97-
verbose,
9898
killSignal,
9999
forceKillAfterDelay,
100100
lines,

lib/arguments/verbose.js

Lines changed: 0 additions & 22 deletions
This file was deleted.

lib/async.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {setMaxListeners} from 'node:events';
22
import {spawn} from 'node:child_process';
3-
import {normalizeArguments, handleArguments} from './arguments/options.js';
3+
import {normalizeArguments, handleCommand, handleArguments} from './arguments/options.js';
44
import {makeError, makeSuccessResult} from './return/error.js';
55
import {handleOutput, handleResult} from './return/output.js';
66
import {handleEarlyError} from './return/early-error.js';
@@ -14,9 +14,8 @@ import {getSpawnedResult} from './stream/resolve.js';
1414
import {mergePromise} from './promise.js';
1515

1616
export const execa = (rawFile, rawArgs, rawOptions) => {
17-
const {file, args, command, escapedCommand, options} = handleAsyncArguments(rawFile, rawArgs, rawOptions);
18-
const stdioStreamsGroups = handleInputAsync(options);
19-
const {spawned, promise} = runExeca({file, args, options, command, escapedCommand, stdioStreamsGroups});
17+
const {file, args, command, escapedCommand, options, stdioStreamsGroups} = handleAsyncArguments(rawFile, rawArgs, rawOptions);
18+
const {spawned, promise} = spawnProcessAsync({file, args, options, command, escapedCommand, stdioStreamsGroups});
2019
spawned.pipe = pipeToProcess.bind(undefined, {source: spawned, sourcePromise: promise, stdioStreamsGroups, destinationOptions: {}});
2120
mergePromise(spawned, promise);
2221
PROCESS_OPTIONS.set(spawned, options);
@@ -25,15 +24,17 @@ export const execa = (rawFile, rawArgs, rawOptions) => {
2524

2625
const handleAsyncArguments = (rawFile, rawArgs, rawOptions) => {
2726
[rawFile, rawArgs, rawOptions] = normalizeArguments(rawFile, rawArgs, rawOptions);
28-
const {file, args, command, escapedCommand, options: normalizedOptions} = handleArguments(rawFile, rawArgs, rawOptions);
27+
const {command, escapedCommand} = handleCommand(rawFile, rawArgs, rawOptions);
28+
const {file, args, options: normalizedOptions} = handleArguments(rawFile, rawArgs, rawOptions);
2929
const options = handleAsyncOptions(normalizedOptions);
30-
return {file, args, command, escapedCommand, options};
30+
const stdioStreamsGroups = handleInputAsync(options);
31+
return {file, args, command, escapedCommand, options, stdioStreamsGroups};
3132
};
3233

3334
// Prevent passing the `timeout` option directly to `child_process.spawn()`
3435
const handleAsyncOptions = ({timeout, ...options}) => ({...options, timeoutDuration: timeout});
3536

36-
const runExeca = ({file, args, options, command, escapedCommand, stdioStreamsGroups}) => {
37+
const spawnProcessAsync = ({file, args, options, command, escapedCommand, stdioStreamsGroups}) => {
3738
let spawned;
3839
try {
3940
spawned = spawn(file, args, options);

lib/pipe/validate.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const getDestinationStream = (destinationOptions, ...args) => {
4141

4242
const getDestination = (destinationOptions, firstArgument, ...args) => {
4343
if (Array.isArray(firstArgument)) {
44-
const destination = create$({...destinationOptions, stdin: 'pipe'})(firstArgument, ...args);
44+
const destination = create$({...destinationOptions, ...PIPED_PROCESS_OPTIONS})(firstArgument, ...args);
4545
return {destination, pipeOptions: destinationOptions};
4646
}
4747

@@ -51,7 +51,7 @@ const getDestination = (destinationOptions, firstArgument, ...args) => {
5151
}
5252

5353
const [rawFile, rawArgs, rawOptions] = normalizeArguments(firstArgument, ...args);
54-
const destination = execa(rawFile, rawArgs, {...rawOptions, stdin: 'pipe'});
54+
const destination = execa(rawFile, rawArgs, {...rawOptions, ...PIPED_PROCESS_OPTIONS});
5555
return {destination, pipeOptions: rawOptions};
5656
}
5757

@@ -66,6 +66,8 @@ const getDestination = (destinationOptions, firstArgument, ...args) => {
6666
throw new TypeError(`The first argument must be a template string, an options object, or an Execa child process: ${firstArgument}`);
6767
};
6868

69+
const PIPED_PROCESS_OPTIONS = {stdin: 'pipe', piped: true};
70+
6971
export const PROCESS_OPTIONS = new WeakMap();
7072

7173
const getSourceStream = (source, stdioStreamsGroups, from, sourceOptions) => {

lib/sync.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
import {spawnSync} from 'node:child_process';
2-
import {normalizeArguments, handleArguments} from './arguments/options.js';
2+
import {normalizeArguments, handleCommand, handleArguments} from './arguments/options.js';
33
import {makeError, makeEarlyError, makeSuccessResult} from './return/error.js';
44
import {handleOutput, handleResult} from './return/output.js';
55
import {handleInputSync, pipeOutputSync} from './stdio/sync.js';
66
import {isFailedExit} from './exit/code.js';
77

88
export const execaSync = (rawFile, rawArgs, rawOptions) => {
9-
const {file, args, command, escapedCommand, options} = handleSyncArguments(rawFile, rawArgs, rawOptions);
10-
const stdioStreamsGroups = handleInputSync(options);
11-
const result = runExecaSync({file, args, options, command, escapedCommand, stdioStreamsGroups});
9+
const {file, args, command, escapedCommand, options, stdioStreamsGroups} = handleSyncArguments(rawFile, rawArgs, rawOptions);
10+
const result = spawnProcessSync({file, args, options, command, escapedCommand, stdioStreamsGroups});
1211
return handleResult(result, options);
1312
};
1413

1514
const handleSyncArguments = (rawFile, rawArgs, rawOptions) => {
1615
[rawFile, rawArgs, rawOptions] = normalizeArguments(rawFile, rawArgs, rawOptions);
16+
const {command, escapedCommand} = handleCommand(rawFile, rawArgs, rawOptions);
1717
const syncOptions = normalizeSyncOptions(rawOptions);
18-
const {file, args, command, escapedCommand, options} = handleArguments(rawFile, rawArgs, syncOptions);
18+
const {file, args, options} = handleArguments(rawFile, rawArgs, syncOptions);
1919
validateSyncOptions(options);
20-
return {file, args, command, escapedCommand, options};
20+
const stdioStreamsGroups = handleInputSync(options);
21+
return {file, args, command, escapedCommand, options, stdioStreamsGroups};
2122
};
2223

2324
const normalizeSyncOptions = options => options.node && !options.ipc ? {...options, ipc: false} : options;
@@ -28,7 +29,7 @@ const validateSyncOptions = ({ipc}) => {
2829
}
2930
};
3031

31-
const runExecaSync = ({file, args, options, command, escapedCommand, stdioStreamsGroups}) => {
32+
const spawnProcessSync = ({file, args, options, command, escapedCommand, stdioStreamsGroups}) => {
3233
let syncResult;
3334
try {
3435
syncResult = spawnSync(file, args, options);

lib/verbose/log.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {writeFileSync} from 'node:fs';
2+
import process from 'node:process';
3+
4+
// Write synchronously to ensure lines are properly ordered and not interleaved with `stdout`
5+
export const verboseLog = (string, icon) => {
6+
writeFileSync(process.stderr.fd, `[${getTimestamp()}] ${ICONS[icon]} ${string}\n`);
7+
};
8+
9+
// Prepending the timestamp allows debugging the slow paths of a process
10+
const getTimestamp = () => {
11+
const date = new Date();
12+
return `${padField(date.getHours(), 2)}:${padField(date.getMinutes(), 2)}:${padField(date.getSeconds(), 2)}.${padField(date.getMilliseconds(), 3)}`;
13+
};
14+
15+
const padField = (field, padding) => String(field).padStart(padding, '0');
16+
17+
const ICONS = {
18+
command: '$',
19+
pipedCommand: '|',
20+
};

lib/verbose/start.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {debuglog} from 'node:util';
2+
import {verboseLog} from './log.js';
3+
4+
// When `verbose` is `short|full`, print each command
5+
export const logCommand = (escapedCommand, {verbose = verboseDefault, piped = false}) => {
6+
if (!verbose) {
7+
return;
8+
}
9+
10+
const icon = piped ? 'pipedCommand' : 'command';
11+
verboseLog(escapedCommand, icon);
12+
};
13+
14+
const verboseDefault = debuglog('execa').enabled;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
"tempfile": "^5.0.0",
6767
"tsd": "^0.29.0",
6868
"which": "^4.0.0",
69-
"xo": "^0.56.0"
69+
"xo": "^0.56.0",
70+
"yoctocolors": "^2.0.0"
7071
},
7172
"c8": {
7273
"reporter": [

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ unicorns
142142
rainbows
143143

144144
> NODE_DEBUG=execa node file.js
145-
[16:50:03.305] echo unicorns
145+
[19:49:00.360] $ echo unicorns
146146
unicorns
147-
[16:50:03.308] echo rainbows
147+
[19:49:00.383] $ echo rainbows
148148
rainbows
149149
```
150150

test/arguments/verbose.js

Lines changed: 0 additions & 37 deletions
This file was deleted.

test/fixtures/nested-pipe-file.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process';
3+
import {execa} from '../../index.js';
4+
5+
const [sourceOptions, sourceFile, sourceArg, destinationOptions, destinationFile, destinationArg] = process.argv.slice(2);
6+
await execa(sourceFile, [sourceArg], JSON.parse(sourceOptions))
7+
.pipe(destinationFile, destinationArg === undefined ? [] : [destinationArg], JSON.parse(destinationOptions));

test/fixtures/nested-pipe-process.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process';
3+
import {execa} from '../../index.js';
4+
5+
const [sourceOptions, sourceFile, sourceArg, destinationOptions, destinationFile, destinationArg] = process.argv.slice(2);
6+
await execa(sourceFile, [sourceArg], JSON.parse(sourceOptions))
7+
.pipe(execa(destinationFile, destinationArg === undefined ? [] : [destinationArg], JSON.parse(destinationOptions)));

test/fixtures/nested-pipe-script.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process';
3+
import {$} from '../../index.js';
4+
5+
const [sourceOptions, sourceFile, sourceArg, destinationOptions, destinationFile, destinationArg] = process.argv.slice(2);
6+
await $(JSON.parse(sourceOptions))`${sourceFile} ${sourceArg}`
7+
.pipe(JSON.parse(destinationOptions))`${destinationFile} ${destinationArg === undefined ? [] : [destinationArg]}`;

test/fixtures/nested-sync.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env node
2+
import process from 'node:process';
3+
import {execaSync} from '../../index.js';
4+
5+
const [options, file, ...args] = process.argv.slice(2);
6+
execaSync(file, args, JSON.parse(options));

test/fixtures/verbose-script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ import {$} from '../../index.js';
33

44
const $$ = $({stdio: 'inherit'});
55
await $$`node -e console.error(1)`;
6-
await $$`node -e console.error(2)`;
6+
await $$({reject: false})`node -e process.exit(2)`;

test/helpers/verbose.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {platform} from 'node:process';
2+
import {stripVTControlCharacters} from 'node:util';
3+
import {execa} from '../../index.js';
4+
import {foobarString} from './input.js';
5+
6+
const isWindows = platform === 'win32';
7+
export const QUOTE = isWindows ? '"' : '\'';
8+
9+
// eslint-disable-next-line max-params
10+
const nestedExeca = (fixtureName, file, args, options, parentOptions) => {
11+
[args, options = {}, parentOptions = {}] = Array.isArray(args) ? [args, options, parentOptions] : [[], args, options];
12+
return execa(fixtureName, [JSON.stringify(options), file, ...args], parentOptions);
13+
};
14+
15+
export const nestedExecaAsync = nestedExeca.bind(undefined, 'nested.js');
16+
export const nestedExecaSync = nestedExeca.bind(undefined, 'nested-sync.js');
17+
18+
export const runErrorProcess = async (t, verbose, execaMethod) => {
19+
const {stderr} = await t.throwsAsync(execaMethod('noop-fail.js', ['1', foobarString], {verbose}));
20+
t.true(stderr.includes('exit code 2'));
21+
return stderr;
22+
};
23+
24+
export const runEarlyErrorProcess = async (t, execaMethod) => {
25+
const {stderr} = await t.throwsAsync(execaMethod('noop.js', [foobarString], {verbose: true, cwd: true}));
26+
t.true(stderr.includes('The "cwd" option must'));
27+
return stderr;
28+
};
29+
30+
export const getCommandLine = stderr => getCommandLines(stderr)[0];
31+
export const getCommandLines = stderr => getNormalizedLines(stderr).filter(line => isCommandLine(line));
32+
const isCommandLine = line => line.includes(' $ ') || line.includes(' | ');
33+
export const getNormalizedLines = stderr => splitLines(normalizeStderr(stderr));
34+
const splitLines = stderr => stderr.split('\n');
35+
36+
const normalizeStderr = stderr => normalizeTimestamp(stripVTControlCharacters(stderr));
37+
export const testTimestamp = '[00:00:00.000]';
38+
const normalizeTimestamp = stderr => stderr.replaceAll(/^\[\d{2}:\d{2}:\d{2}.\d{3}]/gm, testTimestamp);

0 commit comments

Comments
 (0)