Skip to content

Commit

Permalink
feat: provide sync API (#738)
Browse files Browse the repository at this point in the history
closes #681
  • Loading branch information
antongolub committed Mar 18, 2024
1 parent 0640b80 commit 1f8c8b8
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 11 deletions.
61 changes: 50 additions & 11 deletions src/core.ts
Expand Up @@ -41,16 +41,23 @@ import {
export interface Shell {
(pieces: TemplateStringsArray, ...args: any[]): ProcessPromise
(opts: Partial<Options>): Shell
sync: {
(pieces: TemplateStringsArray, ...args: any[]): ProcessOutput
(opts: Partial<Options>): 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
Expand All @@ -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,
Expand Down Expand Up @@ -127,18 +136,35 @@ export const $: Shell & Options = new Proxy<Shell & Options>(
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)
},
Expand Down Expand Up @@ -169,7 +195,8 @@ export class ProcessPromise extends Promise<ProcessOutput> {
private _resolved = false
private _halted = false
private _piped = false
private zurk: ReturnType<typeof exec> | null = null
private _zurk: ReturnType<typeof exec> | null = null
private _output: ProcessOutput | null = null
_prerun = noop
_postrun = noop

Expand Down Expand Up @@ -203,7 +230,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
verbose: self.isVerbose(),
})

this.zurk = exec({
this._zurk = exec({
input,
cmd: $.prefix + this._command,
cwd: $.cwd ?? $[processCwd],
Expand All @@ -212,7 +239,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
env: $.env,
spawn: $.spawn,
stdio: this._stdio as any,
sync: false,
sync: $[syncExec],
detached: !isWin,
run: (cb) => cb(),
on: {
Expand Down Expand Up @@ -241,9 +268,16 @@ export class ProcessPromise extends Promise<ProcessOutput> {
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,
Expand All @@ -259,6 +293,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
stdall,
message
)
self._output = output
if (status === 0 || (self._nothrow ?? $.nothrow)) {
self._resolve(output)
} else {
Expand All @@ -275,7 +310,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}

get child() {
return this.zurk?.child
return this._zurk?.child
}

get stdin(): Writable {
Expand Down Expand Up @@ -366,7 +401,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
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<void> {
Expand Down Expand Up @@ -419,6 +454,10 @@ export class ProcessPromise extends Promise<ProcessOutput> {
get isHalted(): boolean {
return this._halted
}

get output() {
return this._output
}
}

export class ProcessOutput extends Error {
Expand Down
7 changes: 7 additions & 0 deletions test/core.test.js
Expand Up @@ -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"}'`)
Expand Down

0 comments on commit 1f8c8b8

Please sign in to comment.