Repository: sindresorhus/execa (js)

Issue:
Fix the `.pipeStdout(filePath)` tests
The `.pipeStdout(filePath)` tests (and related `.pipeStderr()`/`.pipeAll()` tests) are currently randomly failing.

https://github.com/sindresorhus/execa/blob/5b1a1c7acd2f54a924c4eb8e55319a49f39eeb46/test/pipe.js#L41-L44

```
  pipe › pipeStderr() can pipe to files

  Rejected promise returned by test. Reason:

  Error {
    code: 'ENOENT',
    errno: -4058,
    path: 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\6afa72f0-ed8a-47ae-8547-6f7c29bc33b4.txt',
    syscall: 'open',
    message: 'ENOENT: no such file or directory, open \'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\6afa72f0-ed8a-47ae-8547-6f7c29bc33b4.txt\'',
  }

  Error: ENOENT: no such file or directory, open 'C:\Users\runneradmin\AppData\Local\Temp\6afa72f0-ed8a-47ae-8547-6f7c29bc33b4.txt'
```

When piping to a stream (including the one created when passing a file path), the Execa promise resolves once all data has been sent to the stream, but not when that stream has finished processing that data.

For example, when `await execa(...).pipeStdout('filePath')` resolves, the file might not exist yet. This is currently making our tests randomly fail.

One way to solve this would be to make the stream part of a `Stream.pipeline()`, as described in #591, #592 and #593.

Pull request:
Fix `.pipeStdout(filePath)` tests
Fixes #599.

This is not a perfect fix, but seems to work.
IMHO The real fix would be to remove the `.pipeStdout()` method and replaces it with the solution proposed in #591, #592, #593.

Patch:
diff --git a/index.d.ts b/index.d.ts
index f37c7ea..0a07c4f 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -852,6 +852,56 @@ type Execa$<StdoutStderrType extends StdoutStderrAll = string> = {
 		templates: TemplateStringsArray,
 		...expressions: TemplateExpression[]
 	): ExecaSyncReturnValue<StdoutStderrType>;
+
+	/**
+	Same as $\`command\` but synchronous.
+
+	@returns A `childProcessResult` object
+	@throws A `childProcessResult` error
+
+	@example <caption>Basic</caption>
+	```
+	import {$} from 'execa';
+
+	const branch = $.s`git branch --show-current`;
+	$.s`dep deploy --branch=${branch}`;
+	```
+
+	@example <caption>Multiple arguments</caption>
+	```
+	import {$} from 'execa';
+
+	const args = ['unicorns', '&', 'rainbows!'];
+	const {stdout} = $.s`echo ${args}`;
+	console.log(stdout);
+	//=> 'unicorns & rainbows!'
+	```
+
+	@example <caption>With options</caption>
+	```
+	import {$} from 'execa';
+
+	$.s({stdio: 'inherit'})`echo unicorns`;
+	//=> 'unicorns'
+	```
+
+	@example <caption>Shared options</caption>
+	```
+	import {$} from 'execa';
+
+	const $$ = $({stdio: 'inherit'});
+
+	$$.s`echo unicorns`;
+	//=> 'unicorns'
+
+	$$.s`echo rainbows`;
+	//=> 'rainbows'
+	```
+	*/
+	s(
+		templates: TemplateStringsArray,
+		...expressions: TemplateExpression[]
+	): ExecaSyncReturnValue<StdoutStderrType>;
 };
 
 /**
diff --git a/index.js b/index.js
index a319ac5..8d830bb 100644
--- a/index.js
+++ b/index.js
@@ -263,6 +263,8 @@ function create$(options) {
 		return execaSync(file, args, normalizeScriptOptions(options));
 	};
 
+	$.s = $.sync;
+
 	return $;
 }
 
diff --git a/lib/verbose.js b/lib/verbose.js
index 5f5490e..03682f4 100644
--- a/lib/verbose.js
+++ b/lib/verbose.js
@@ -1,3 +1,4 @@
+import {writeFileSync} from 'node:fs';
 import {debuglog} from 'node:util';
 import process from 'node:process';
 
@@ -15,5 +16,7 @@ export const logCommand = (escapedCommand, {verbose}) => {
 		return;
 	}
 
-	process.stderr.write(`[${getTimestamp()}] ${escapedCommand}\n`);
+	// Write synchronously to ensure it is written before spawning the child process.
+	// This guarantees this line is written to `stderr` before the child process prints anything.
+	writeFileSync(process.stderr.fd, `[${getTimestamp()}] ${escapedCommand}\n`);
 };
diff --git a/readme.md b/readme.md
index 004c7a3..18dab0c 100644
--- a/readme.md
+++ b/readme.md
@@ -292,6 +292,7 @@ Same as [`execa()`](#execacommandcommand-options) but synchronous.
 Returns or throws a [`childProcessResult`](#childProcessResult).
 
 ### $.sync\`command\`
+### $.s\`command\`
 
 Same as [$\`command\`](#command) but synchronous.
 


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 1e1f764..7d01cb9 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -292,6 +292,7 @@ expectType<ExecaChildProcess<Buffer>>(
 expectType<ExecaChildProcess>($`unicorns`);
 expectType<ExecaReturnValue>(await $`unicorns`);
 expectType<ExecaSyncReturnValue>($.sync`unicorns`);
+expectType<ExecaSyncReturnValue>($.s`unicorns`);
 
 expectType<ExecaChildProcess>($({encoding: 'utf8'})`unicorns`);
 expectType<ExecaReturnValue>(await $({encoding: 'utf8'})`unicorns`);
diff --git a/test/command.js b/test/command.js
index 552cb6c..ec7eb19 100644
--- a/test/command.js
+++ b/test/command.js
@@ -237,6 +237,11 @@ test('$.sync', t => {
 	t.is(stdout, 'foo\nbar');
 });
 
+test('$.sync can be called $.s', t => {
+	const {stdout} = $.s`echo.js foo bar`;
+	t.is(stdout, 'foo\nbar');
+});
+
 test('$.sync accepts options', t => {
 	const {stdout} = $({stripFinalNewline: true}).sync`noop.js foo`;
 	t.is(stdout, 'foo');
diff --git a/test/kill.js b/test/kill.js
index 42bd53f..66ff48b 100644
--- a/test/kill.js
+++ b/test/kill.js
@@ -167,7 +167,9 @@ const spawnAndKill = async (t, [signal, cleanup, detached, isKilled]) => {
 const exitIfWindows = process.platform === 'win32';
 test('spawnAndKill SIGTERM', spawnAndKill, ['SIGTERM', false, false, exitIfWindows]);
 test('spawnAndKill SIGKILL', spawnAndKill, ['SIGKILL', false, false, exitIfWindows]);
-test('spawnAndKill cleanup SIGTERM', spawnAndKill, ['SIGTERM', true, false, true]);
+// The `cleanup` option can introduce a race condition in this test
+// This is especially true when run concurrently, so we use `test.serial()`
+test.serial('spawnAndKill cleanup SIGTERM', spawnAndKill, ['SIGTERM', true, false, true]);
 test('spawnAndKill cleanup SIGKILL', spawnAndKill, ['SIGKILL', true, false, exitIfWindows]);
 test('spawnAndKill detached SIGTERM', spawnAndKill, ['SIGTERM', false, true, false]);
 test('spawnAndKill detached SIGKILL', spawnAndKill, ['SIGKILL', false, true, false]);
diff --git a/test/pipe.js b/test/pipe.js
index 264f41f..39651de 100644
--- a/test/pipe.js
+++ b/test/pipe.js
@@ -38,10 +38,11 @@ const pipeToFile = async (t, fixtureName, funcName, streamName) => {
 	t.is(await readFile(file, 'utf8'), 'test\n');
 };
 
-test('pipeStdout() can pipe to files', pipeToFile, 'noop.js', 'pipeStdout', 'stdout');
-test('pipeStderr() can pipe to files', pipeToFile, 'noop-err.js', 'pipeStderr', 'stderr');
-test('pipeAll() can pipe stdout to files', pipeToFile, 'noop.js', 'pipeAll', 'stdout');
-test('pipeAll() can pipe stderr to files', pipeToFile, 'noop-err.js', 'pipeAll', 'stderr');
+// `test.serial()` is due to a race condition: `execa(...).pipe*(file)` might resolve before the file stream has resolved
+test.serial('pipeStdout() can pipe to files', pipeToFile, 'noop.js', 'pipeStdout', 'stdout');
+test.serial('pipeStderr() can pipe to files', pipeToFile, 'noop-err.js', 'pipeStderr', 'stderr');
+test.serial('pipeAll() can pipe stdout to files', pipeToFile, 'noop.js', 'pipeAll', 'stdout');
+test.serial('pipeAll() can pipe stderr to files', pipeToFile, 'noop-err.js', 'pipeAll', 'stderr');
 
 const invalidTarget = (t, funcName, getTarget) => {
 	t.throws(() => execa('noop.js', {all: true})[funcName](getTarget()), {