From 1f8c8b85d301607faedf0ba820a742a53c6e41b2 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Mon, 18 Mar 2024 19:28:34 +0300 Subject: [PATCH] feat: provide sync API (#738) closes #681 --- src/core.ts | 61 ++++++++++++++++++++++++++++++++++++++--------- test/core.test.js | 7 ++++++ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/core.ts b/src/core.ts index 8747379a86..3c42bce230 100644 --- a/src/core.ts +++ b/src/core.ts @@ -41,16 +41,23 @@ import { export interface Shell { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise (opts: Partial): Shell + sync: { + (pieces: TemplateStringsArray, ...args: any[]): ProcessOutput + (opts: Partial): Shell + } } const processCwd = Symbol('processCwd') +const syncExec = Symbol('syncExec') export interface Options { [processCwd]: string + [syncExec]: boolean cwd?: string ac?: AbortController input?: string | Buffer | Readable | ProcessOutput | ProcessPromise verbose: boolean + sync: boolean env: NodeJS.ProcessEnv shell: string | boolean nothrow: boolean @@ -73,8 +80,10 @@ hook.enable() export const defaults: Options = { [processCwd]: process.cwd(), + [syncExec]: false, verbose: true, env: process.env, + sync: false, shell: true, nothrow: false, quiet: false, @@ -127,18 +136,35 @@ export const $: Shell & Options = new Proxy( args ) as string - promise._bind(cmd, from, resolve!, reject!, getStore()) + const snapshot = getStore() + const sync = snapshot[syncExec] + const callback = () => promise.isHalted || promise.run() + + promise._bind( + cmd, + from, + resolve!, + (v: ProcessOutput) => { + reject!(v) + if (sync) throw v + }, + snapshot + ) // Postpone run to allow promise configuration. - setImmediate(() => promise.isHalted || promise.run()) - return promise + sync ? callback() : setImmediate(callback) + + return sync ? promise.output : promise } as Shell & Options, { set(_, key, value) { const target = key in Function.prototype ? _ : getStore() - Reflect.set(target, key, value) + Reflect.set(target, key === 'sync' ? syncExec : key, value) + return true }, get(_, key) { + if (key === 'sync') return $({ sync: true }) + const target = key in Function.prototype ? _ : getStore() return Reflect.get(target, key) }, @@ -169,7 +195,8 @@ export class ProcessPromise extends Promise { private _resolved = false private _halted = false private _piped = false - private zurk: ReturnType | null = null + private _zurk: ReturnType | null = null + private _output: ProcessOutput | null = null _prerun = noop _postrun = noop @@ -203,7 +230,7 @@ export class ProcessPromise extends Promise { verbose: self.isVerbose(), }) - this.zurk = exec({ + this._zurk = exec({ input, cmd: $.prefix + this._command, cwd: $.cwd ?? $[processCwd], @@ -212,7 +239,7 @@ export class ProcessPromise extends Promise { env: $.env, spawn: $.spawn, stdio: this._stdio as any, - sync: false, + sync: $[syncExec], detached: !isWin, run: (cb) => cb(), on: { @@ -241,9 +268,16 @@ export class ProcessPromise extends Promise { const message = ProcessOutput.getErrorMessage(error, self._from) // Should we enable this? // (nothrow ? self._resolve : self._reject)( - self._reject( - new ProcessOutput(null, null, stdout, stderr, stdall, message) + const output = new ProcessOutput( + null, + null, + stdout, + stderr, + stdall, + message ) + self._output = output + self._reject(output) } else { const message = ProcessOutput.getExitMessage( status, @@ -259,6 +293,7 @@ export class ProcessPromise extends Promise { stdall, message ) + self._output = output if (status === 0 || (self._nothrow ?? $.nothrow)) { self._resolve(output) } else { @@ -275,7 +310,7 @@ export class ProcessPromise extends Promise { } get child() { - return this.zurk?.child + return this._zurk?.child } get stdin(): Writable { @@ -366,7 +401,7 @@ export class ProcessPromise extends Promise { if (!this.child) throw new Error('Trying to abort a process without creating one.') - this.zurk?.ac.abort(reason) + this._zurk?.ac.abort(reason) } async kill(signal = 'SIGTERM'): Promise { @@ -419,6 +454,10 @@ export class ProcessPromise extends Promise { get isHalted(): boolean { return this._halted } + + get output() { + return this._output + } } export class ProcessOutput extends Error { diff --git a/test/core.test.js b/test/core.test.js index 8c7b756080..5df3a2eb7c 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -120,6 +120,13 @@ describe('core', () => { assert.equal((await p5).stdout, 'baz') }) + test('`$.sync()` provides synchronous API', () => { + const o1 = $.sync`echo foo` + const o2 = $({ sync: true })`echo foo` + assert.equal(o1.stdout, 'foo\n') + assert.equal(o2.stdout, 'foo\n') + }) + test('pipes are working', async () => { let { stdout } = await $`echo "hello"` .pipe($`awk '{print $1" world"}'`)