Commit b08df4d
Changed files (4)
src/core.ts
@@ -104,7 +104,8 @@ export const $ = new Proxy<Shell & Options>(
cmd += s + pieces[++i]
}
promise._bind(cmd, from, resolve!, reject!, getStore())
- setImmediate(() => promise._run()) // Postpone run to allow promise configuration.
+ // Postpone run to allow promise configuration.
+ setImmediate(() => promise.isHalted || promise.run())
return promise
} as Shell & Options,
{
@@ -143,6 +144,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
private _timeout?: number
private _timeoutSignal?: string
private _resolved = false
+ private _halted = false
private _piped = false
_prerun = noop
_postrun = noop
@@ -161,9 +163,9 @@ export class ProcessPromise extends Promise<ProcessOutput> {
this._snapshot = { ...options }
}
- _run() {
+ run(): ProcessPromise {
const $ = this._snapshot
- if (this.child) return // The _run() can be called from a few places.
+ if (this.child) return this // The _run() can be called from a few places.
this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
$.log({
kind: 'cmd',
@@ -234,11 +236,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout)
this.finally(() => clearTimeout(t)).catch(noop)
}
+ return this
}
get stdin(): Writable {
this.stdio('pipe')
- this._run()
+ this.run()
assert(this.child)
if (this.child.stdin == null)
throw new Error('The stdin of subprocess is null.')
@@ -246,7 +249,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}
get stdout(): Readable {
- this._run()
+ this.run()
assert(this.child)
if (this.child.stdout == null)
throw new Error('The stdout of subprocess is null.')
@@ -254,21 +257,46 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}
get stderr(): Readable {
- this._run()
+ this.run()
assert(this.child)
if (this.child.stderr == null)
throw new Error('The stderr of subprocess is null.')
return this.child.stderr
}
- get exitCode() {
+ get exitCode(): Promise<number | null> {
return this.then(
(p) => p.exitCode,
(p) => p.exitCode
)
}
- pipe(dest: Writable | ProcessPromise) {
+ then<R = ProcessOutput, E = ProcessOutput>(
+ onfulfilled?:
+ | ((value: ProcessOutput) => PromiseLike<R> | R)
+ | undefined
+ | null,
+ onrejected?:
+ | ((reason: ProcessOutput) => PromiseLike<E> | E)
+ | undefined
+ | null
+ ): Promise<R | E> {
+ if (this.isHalted && !this.child) {
+ throw new Error('The process is halted!')
+ }
+ return super.then(onfulfilled, onrejected)
+ }
+
+ catch<T = ProcessOutput>(
+ onrejected?:
+ | ((reason: ProcessOutput) => PromiseLike<T> | T)
+ | undefined
+ | null
+ ): Promise<ProcessOutput | T> {
+ return super.catch(onrejected)
+ }
+
+ pipe(dest: Writable | ProcessPromise): ProcessPromise {
if (typeof dest == 'string')
throw new Error('The pipe() method does not take strings. Forgot $?')
if (this._resolved) {
@@ -280,7 +308,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
this._piped = true
if (dest instanceof ProcessPromise) {
dest.stdio('pipe')
- dest._prerun = this._run.bind(this)
+ dest._prerun = this.run.bind(this)
dest._postrun = () => {
if (!dest.child)
throw new Error(
@@ -295,7 +323,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}
}
- async kill(signal = 'SIGTERM') {
+ async kill(signal = 'SIGTERM'): Promise<void> {
if (!this.child)
throw new Error('Trying to kill a process without creating one.')
if (!this.child.pid) throw new Error('The process pid is undefined.')
@@ -310,26 +338,35 @@ export class ProcessPromise extends Promise<ProcessOutput> {
} catch (e) {}
}
- stdio(stdin: IO, stdout: IO = 'pipe', stderr: IO = 'pipe') {
+ stdio(stdin: IO, stdout: IO = 'pipe', stderr: IO = 'pipe'): ProcessPromise {
this._stdio = [stdin, stdout, stderr]
return this
}
- nothrow() {
+ nothrow(): ProcessPromise {
this._nothrow = true
return this
}
- quiet() {
+ quiet(): ProcessPromise {
this._quiet = true
return this
}
- timeout(d: Duration, signal = 'SIGTERM') {
+ timeout(d: Duration, signal = 'SIGTERM'): ProcessPromise {
this._timeout = parseDuration(d)
this._timeoutSignal = signal
return this
}
+
+ halt(): ProcessPromise {
+ this._halted = true
+ return this
+ }
+
+ get isHalted(): boolean {
+ return this._halted
+ }
}
export class ProcessOutput extends Error {
test/core.test.js
@@ -433,4 +433,28 @@ test('$ is a regular function', async () => {
assert.ok(typeof $.apply === 'function')
})
+test('halt() works', async () => {
+ let filepath = `/tmp/${Math.random().toString()}`
+ let p = $`touch ${filepath}`.halt()
+ await sleep(1)
+ assert.not.ok(
+ fs.existsSync(filepath),
+ 'The cmd called, but it should not have been called'
+ )
+ await p.run()
+ assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
+})
+
+test('await on halted throws', async () => {
+ let p = $`sleep 1`.halt()
+ let ok = true
+ try {
+ await p
+ ok = false
+ } catch (err) {
+ assert.is(err.message, 'The process is halted!')
+ }
+ assert.ok(ok, 'Expected failure!')
+})
+
test.run()
test-d/core.test-d.ts
@@ -30,7 +30,8 @@ expectType<ProcessPromise>(p.stdio('pipe'))
expectType<ProcessPromise>(p.timeout('1s'))
expectType<Promise<void>>(p.kill())
expectType<Promise<ProcessOutput>>(p.then((p) => p))
-expectType<Promise<any>>(p.catch((p) => p))
+expectType<Promise<ProcessOutput>>(p.catch((p) => p))
+expectType<Promise<any>>(p.then((p) => p).catch((p) => p))
let o = await p
assert(o instanceof ProcessOutput)
test-d/experimental.test-d.ts
@@ -0,0 +1,19 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { expectType } from 'tsd'
+import { spinner } from '../src/experimental.js'
+
+expectType<string>(await spinner(() => 'foo'))
+expectType<string>(await spinner('title', () => 'bar'))