Skip to content

Commit 1cc5981

Browse files
authored
feat: protect ProcessPromise from inappropriate instantiation effects (#1097)
* feat: protect `ProcessPromise` from inappropriate instantiation effects * chore: minor imprs
1 parent 0c3313d commit 1cc5981

File tree

3 files changed

+52
-32
lines changed

3 files changed

+52
-32
lines changed

.size-limit.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{
1010
"name": "zx/index",
1111
"path": "build/*.{js,cjs}",
12-
"limit": "809 kB",
12+
"limit": "809.1 kB",
1313
"brotli": false,
1414
"gzip": false
1515
},

src/core.ts

+34-26
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface Shell<
145145
(opts: Partial<Omit<Options, 'sync'>>): Shell<true>
146146
}
147147
}
148+
const bound: [string, string, Options][] = []
148149

149150
export const $: Shell & Options = new Proxy<Shell & Options>(
150151
function (pieces: TemplateStringsArray | Partial<Options>, ...args: any) {
@@ -164,25 +165,14 @@ export const $: Shell & Options = new Proxy<Shell & Options>(
164165
checkShell()
165166
checkQuote()
166167

167-
let resolve: Resolve, reject: Resolve
168-
const process = new ProcessPromise((...args) => ([resolve, reject] = args))
169168
const cmd = buildCmd(
170169
$.quote as typeof quote,
171170
pieces as TemplateStringsArray,
172171
args
173172
) as string
174173
const sync = snapshot[SYNC]
175-
176-
process._bind(
177-
cmd,
178-
from,
179-
resolve!,
180-
(v: ProcessOutput) => {
181-
reject!(v)
182-
if (sync) throw v
183-
},
184-
snapshot
185-
)
174+
bound.push([cmd, from, snapshot])
175+
const process = new ProcessPromise(noop)
186176

187177
if (!process.isHalted() || sync) process.run()
188178

@@ -237,19 +227,26 @@ export class ProcessPromise extends Promise<ProcessOutput> {
237227
private _reject: Resolve = noop
238228
private _resolve: Resolve = noop
239229

240-
_bind(
241-
cmd: string,
242-
from: string,
243-
resolve: Resolve,
244-
reject: Resolve,
245-
options: Options
246-
) {
247-
this._command = cmd
248-
this._from = from
249-
this._resolve = resolve
250-
this._reject = reject
251-
this._snapshot = { ac: new AbortController(), ...options }
252-
if (this._snapshot.halt) this._stage = 'halted'
230+
constructor(executor: (resolve: Resolve, reject: Resolve) => void) {
231+
let resolve: Resolve
232+
let reject: Resolve
233+
super((...args) => {
234+
;[resolve, reject] = args
235+
executor?.(...args)
236+
})
237+
238+
if (bound.length) {
239+
const [cmd, from, snapshot] = bound.pop()!
240+
this._command = cmd
241+
this._from = from
242+
this._resolve = resolve!
243+
this._reject = (v: ProcessOutput) => {
244+
reject!(v)
245+
if (snapshot[SYNC]) throw v
246+
}
247+
this._snapshot = { ac: new AbortController(), ...snapshot }
248+
if (this._snapshot.halt) this._stage = 'halted'
249+
} else ProcessPromise.disarm(this)
253250
}
254251

255252
run(): ProcessPromise {
@@ -653,6 +650,17 @@ export class ProcessPromise extends Promise<ProcessOutput> {
653650
this._stdin.removeListener(event, cb)
654651
return this
655652
}
653+
654+
// prettier-ignore
655+
private static disarm(p: ProcessPromise, toggle = true): void {
656+
Object.getOwnPropertyNames(ProcessPromise.prototype).forEach(k => {
657+
if (k in Promise.prototype) return
658+
if (!toggle) { Reflect.deleteProperty(p, k); return }
659+
Object.defineProperty(p, k, { configurable: true, get() {
660+
throw new Error('Inappropriate usage. Apply $ instead of direct instantiation.')
661+
}})
662+
})
663+
}
656664
}
657665

658666
type ProcessDto = {

test/core.test.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,17 @@ describe('core', () => {
456456

457457
it('all transitions', async () => {
458458
const { promise, resolve, reject } = Promise.withResolvers()
459-
const p = new ProcessPromise(noop, noop)
459+
const p = new ProcessPromise(noop)
460+
ProcessPromise.disarm(p, false)
460461
assert.equal(p.stage, 'initial')
461-
p._bind('echo foo', 'test', resolve, reject, {
462-
...defaults,
463-
halt: true,
464-
})
462+
463+
p._command = 'echo foo'
464+
p._from = 'test'
465+
p._resolve = resolve
466+
p._reject = reject
467+
p._snapshot = { ...defaults }
468+
p._stage = 'halted'
469+
465470
assert.equal(p.stage, 'halted')
466471
p.run()
467472
assert.equal(p.stage, 'running')
@@ -490,6 +495,13 @@ describe('core', () => {
490495
assert.ok(p5 !== p1)
491496
})
492497

498+
test('asserts self instantiation', async () => {
499+
const p = new ProcessPromise(() => {})
500+
501+
assert(typeof p.then === 'function')
502+
assert.throws(() => p.stage, /Inappropriate usage/)
503+
})
504+
493505
test('resolves with ProcessOutput', async () => {
494506
const o = await $`echo foo`
495507
assert.ok(o instanceof ProcessOutput)

0 commit comments

Comments
 (0)