From 8900e458b3b3fa48f82a553e8553e25d11d488ba Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 23 Nov 2024 12:30:29 +0300 Subject: [PATCH] fix: autorun halted processes on pipe run (#951) relates #949 --- .size-limit.json | 4 ++-- src/core.ts | 32 +++++++++++++++++++++++--------- test/core.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 8f6b1a4be7..7bc403e489 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,7 +2,7 @@ { "name": "zx/core", "path": ["build/core.cjs", "build/util.cjs", "build/vendor-core.cjs"], - "limit": "71.1 kB", + "limit": "72 kB", "brotli": false, "gzip": false }, @@ -30,7 +30,7 @@ { "name": "all", "path": "build/*", - "limit": "833.6 kB", + "limit": "835 kB", "brotli": false, "gzip": false } diff --git a/src/core.ts b/src/core.ts index 634620c088..d82bc5e178 100644 --- a/src/core.ts +++ b/src/core.ts @@ -23,6 +23,7 @@ import { type AsyncHook, AsyncLocalStorage, createHook } from 'node:async_hooks' import { type Readable, type Writable } from 'node:stream' import { inspect } from 'node:util' import { EOL as _EOL } from 'node:os' +import { EventEmitter } from 'node:events' import { exec, buildCmd, @@ -205,6 +206,10 @@ export class ProcessPromise extends Promise { private _resolved = false private _halted?: boolean private _piped = false + private _pipedFrom?: ProcessPromise + private _run = false + private _ee = new EventEmitter() + private _stdin = new VoidStream() private _zurk: ReturnType | null = null private _output: ProcessOutput | null = null private _reject: Resolve = noop @@ -225,7 +230,10 @@ export class ProcessPromise extends Promise { } run(): ProcessPromise { - if (this.child) return this // The _run() can be called from a few places. + if (this._run) return this // The _run() can be called from a few places. + this._halted = false + this._run = true + this._pipedFrom?.run() const $ = this._snapshot const self = this @@ -255,9 +263,11 @@ export class ProcessPromise extends Promise { spawn: $.spawn, spawnSync: $.spawnSync, store: $.store, + stdin: self._stdin, stdio: self._stdio ?? $.stdio, sync: $[SYNC], detached: $.detached, + ee: self._ee, run: (cb) => cb(), on: { start: () => { @@ -326,20 +336,18 @@ export class ProcessPromise extends Promise { ...args: any[] ): (Writable & PromiseLike) | ProcessPromise { if (isStringLiteral(dest, ...args)) - return this.pipe($(dest as TemplateStringsArray, ...args)) + return this.pipe($({ halt: true })(dest as TemplateStringsArray, ...args)) if (isString(dest)) throw new Error('The pipe() method does not take strings. Forgot $?') this._piped = true - const { store, ee, fulfilled } = this._zurk! + const ee = this._ee const from = new VoidStream() const fill = () => { - for (const chunk of store.stdout) { - from.write(chunk) - } + for (const chunk of this._zurk!.store.stdout) from.write(chunk) } - if (fulfilled) { + if (this._resolved) { fill() from.end() } else { @@ -354,8 +362,14 @@ export class ProcessPromise extends Promise { } if (dest instanceof ProcessPromise) { - this.catch((e) => (dest.isNothrow() ? noop : dest._reject(e))) - from.pipe(dest.stdin) + dest._pipedFrom = this + + if (dest.isHalted() && this.isHalted()) { + ee.once('start', () => from.pipe(dest.run()._stdin)) + } else { + this.catch((e) => (dest.isNothrow() ? noop : dest._reject(e))) + from.pipe(dest.run()._stdin) + } return dest } from.once('end', () => dest.emit('end-piped-from')).pipe(dest) diff --git a/test/core.test.js b/test/core.test.js index e78bc39343..29a87f90a6 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -436,6 +436,32 @@ describe('core', () => { assert.equal(o2, 'HELLO WORLD\n') }) + test('$ > $ halted', async () => { + const $h = $({ halt: true }) + const { stdout } = await $`echo "hello"` + .pipe($h`awk '{print $1" world"}'`) + .pipe($h`tr '[a-z]' '[A-Z]'`) + + assert.equal(stdout, 'HELLO WORLD\n') + }) + + test('$ halted > $ halted', async () => { + const $h = $({ halt: true }) + const { stdout } = await $h`echo "hello"` + .pipe($h`awk '{print $1" world"}'`) + .pipe($h`tr '[a-z]' '[A-Z]'`) + .run() + + assert.equal(stdout, 'HELLO WORLD\n') + }) + + test('$ halted > $ literal', async () => { + const { stdout } = await $({ halt: true })`echo "hello"` + .pipe`awk '{print $1" world"}'`.pipe`tr '[a-z]' '[A-Z]'`.run() + + assert.equal(stdout, 'HELLO WORLD\n') + }) + test('$ > stream', async () => { const file = tempfile() const fileStream = fs.createWriteStream(file)