From c94aabdd3ef2ccda6dde9a812c403c7e9ef149ab Mon Sep 17 00:00:00 2001 From: Anton Golub <antongolub@antongolub.com> Date: Mon, 10 Feb 2025 23:38:11 +0300 Subject: [PATCH 1/2] feat: protect `ProcessPromise` from inappropriate instantiation effects --- .size-limit.json | 2 +- src/core.ts | 60 +++++++++++++++++++++++++++-------------------- test/core.test.js | 22 +++++++++++++---- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 57a663a0ff..681f0b81ac 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -9,7 +9,7 @@ { "name": "zx/index", "path": "build/*.{js,cjs}", - "limit": "809 kB", + "limit": "809.1 kB", "brotli": false, "gzip": false }, diff --git a/src/core.ts b/src/core.ts index a14d7a1ab8..4843fea60e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -145,6 +145,7 @@ export interface Shell< (opts: Partial<Omit<Options, 'sync'>>): Shell<true> } } +const bound: [boolean, string, string, Options][] = [] export const $: Shell & Options = new Proxy<Shell & Options>( function (pieces: TemplateStringsArray | Partial<Options>, ...args: any) { @@ -164,25 +165,14 @@ export const $: Shell & Options = new Proxy<Shell & Options>( checkShell() checkQuote() - let resolve: Resolve, reject: Resolve - const process = new ProcessPromise((...args) => ([resolve, reject] = args)) const cmd = buildCmd( $.quote as typeof quote, pieces as TemplateStringsArray, args ) as string const sync = snapshot[SYNC] - - process._bind( - cmd, - from, - resolve!, - (v: ProcessOutput) => { - reject!(v) - if (sync) throw v - }, - snapshot - ) + bound.push([sync, cmd, from, snapshot]) + const process = new ProcessPromise(noop) if (!process.isHalted() || sync) process.run() @@ -237,19 +227,26 @@ export class ProcessPromise extends Promise<ProcessOutput> { private _reject: Resolve = noop private _resolve: Resolve = noop - _bind( - cmd: string, - from: string, - resolve: Resolve, - reject: Resolve, - options: Options - ) { - this._command = cmd - this._from = from - this._resolve = resolve - this._reject = reject - this._snapshot = { ac: new AbortController(), ...options } - if (this._snapshot.halt) this._stage = 'halted' + constructor(executor: (resolve: Resolve, reject: Resolve) => void) { + let resolve: Resolve + let reject: Resolve + super((...args) => { + ;[resolve, reject] = args + executor?.(...args) + }) + + if (executor === noop) { + const [sync, cmd, from, snapshot] = bound.pop()! + this._command = cmd + this._from = from + this._resolve = resolve! + this._reject = (v: ProcessOutput) => { + reject!(v) + if (sync) throw v + } + this._snapshot = { ac: new AbortController(), ...snapshot } + if (this._snapshot.halt) this._stage = 'halted' + } else ProcessPromise.disarm(this) } run(): ProcessPromise { @@ -653,6 +650,17 @@ export class ProcessPromise extends Promise<ProcessOutput> { this._stdin.removeListener(event, cb) return this } + + // prettier-ignore + private static disarm(p: ProcessPromise, toggle = true): void { + Object.getOwnPropertyNames(ProcessPromise.prototype).forEach(k => { + if (k in Promise.prototype) return + if (!toggle) { Reflect.deleteProperty(p, k); return } + Object.defineProperty(p, k, { configurable: true, get() { + throw new Error('Inappropriate usage. Apply $ instead of direct instantiation.') + }}) + }) + } } type ProcessDto = { diff --git a/test/core.test.js b/test/core.test.js index 93fd6cc1b4..6942463510 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -456,12 +456,17 @@ describe('core', () => { it('all transitions', async () => { const { promise, resolve, reject } = Promise.withResolvers() - const p = new ProcessPromise(noop, noop) + const p = new ProcessPromise(() => {}) + ProcessPromise.disarm(p, false) assert.equal(p.stage, 'initial') - p._bind('echo foo', 'test', resolve, reject, { - ...defaults, - halt: true, - }) + + p._command = 'echo foo' + p._from = 'test' + p._resolve = resolve + p._reject = reject + p._snapshot = { ...defaults } + p._stage = 'halted' + assert.equal(p.stage, 'halted') p.run() assert.equal(p.stage, 'running') @@ -490,6 +495,13 @@ describe('core', () => { assert.ok(p5 !== p1) }) + test('asserts self instantiation', async () => { + const p = new ProcessPromise(() => {}) + + assert(typeof p.then === 'function') + assert.throws(() => p.stage, /Inappropriate usage/) + }) + test('resolves with ProcessOutput', async () => { const o = await $`echo foo` assert.ok(o instanceof ProcessOutput) From 0dc5973d5966abeca662c255b348cdd775631f0d Mon Sep 17 00:00:00 2001 From: Anton Golub <antongolub@antongolub.com> Date: Tue, 11 Feb 2025 00:29:22 +0300 Subject: [PATCH 2/2] chore: minor imprs --- src/core.ts | 10 +++++----- test/core.test.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core.ts b/src/core.ts index 4843fea60e..58b7818fd4 100644 --- a/src/core.ts +++ b/src/core.ts @@ -145,7 +145,7 @@ export interface Shell< (opts: Partial<Omit<Options, 'sync'>>): Shell<true> } } -const bound: [boolean, string, string, Options][] = [] +const bound: [string, string, Options][] = [] export const $: Shell & Options = new Proxy<Shell & Options>( function (pieces: TemplateStringsArray | Partial<Options>, ...args: any) { @@ -171,7 +171,7 @@ export const $: Shell & Options = new Proxy<Shell & Options>( args ) as string const sync = snapshot[SYNC] - bound.push([sync, cmd, from, snapshot]) + bound.push([cmd, from, snapshot]) const process = new ProcessPromise(noop) if (!process.isHalted() || sync) process.run() @@ -235,14 +235,14 @@ export class ProcessPromise extends Promise<ProcessOutput> { executor?.(...args) }) - if (executor === noop) { - const [sync, cmd, from, snapshot] = bound.pop()! + if (bound.length) { + const [cmd, from, snapshot] = bound.pop()! this._command = cmd this._from = from this._resolve = resolve! this._reject = (v: ProcessOutput) => { reject!(v) - if (sync) throw v + if (snapshot[SYNC]) throw v } this._snapshot = { ac: new AbortController(), ...snapshot } if (this._snapshot.halt) this._stage = 'halted' diff --git a/test/core.test.js b/test/core.test.js index 6942463510..5ae10927a0 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -456,7 +456,7 @@ describe('core', () => { it('all transitions', async () => { const { promise, resolve, reject } = Promise.withResolvers() - const p = new ProcessPromise(() => {}) + const p = new ProcessPromise(noop) ProcessPromise.disarm(p, false) assert.equal(p.stage, 'initial')