diff --git a/package.json b/package.json index 3931cbe..c2c2901 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "scripts": { "build": "concurrently 'npm:build:*'", "build:js": "node ./src/scripts/build.mjs", - "build:dts": "tsc --emitDeclarationOnly --skipLibCheck --outDir target/dts", + "build:dts": "tsc --emitDeclarationOnly --outDir target/dts", "build:docs": "typedoc --options src/main/typedoc", "build:stamp": "npx buildstamp", "test": "concurrently 'npm:test:*'", diff --git a/src/main/ts/mixin/pipe.ts b/src/main/ts/mixin/pipe.ts index d503f67..684ea32 100644 --- a/src/main/ts/mixin/pipe.ts +++ b/src/main/ts/mixin/pipe.ts @@ -13,9 +13,9 @@ export const pipeMixin: TMixin = ($: TShell, re const { fulfilled, stdout} = ctx if (isZurkAny(stream)) { if (fulfilled) { - stream._ctx.input = fulfilled.stdout + stream.ctx.input = fulfilled.stdout } else { - stream._ctx.stdin = stdout as VoidWritable + stream.ctx.stdin = stdout as VoidWritable } return stream diff --git a/src/main/ts/mixin/timeout.ts b/src/main/ts/mixin/timeout.ts index 41d1045..0002c39 100644 --- a/src/main/ts/mixin/timeout.ts +++ b/src/main/ts/mixin/timeout.ts @@ -1,5 +1,5 @@ import process from 'node:process' -import { assign, noop } from '../util.js' +import { assign } from '../util.js' import type { TMixin, TShell, TShellCtx } from '../x.js' import { type TZurk, type TZurkPromise, isZurkPromise } from '../zurk.js' @@ -31,7 +31,7 @@ export const timeoutMixin: TMixin = ($: TShell, }) attachTimeout(ctx, result) - result.finally(() => clearTimeout((ctx as any).timer)).catch(noop) + result.finally(() => clearTimeout((ctx as any).timer)) } return result diff --git a/src/main/ts/spawn.ts b/src/main/ts/spawn.ts index 5e9cf20..41f3541 100644 --- a/src/main/ts/spawn.ts +++ b/src/main/ts/spawn.ts @@ -14,7 +14,7 @@ export type TSpawnResult = { status: number | null signal: NodeJS.Signals | null duration: number - _ctx: TSpawnCtxNormalized + ctx: TSpawnCtxNormalized error?: TSpawnError, child?: TChild } @@ -42,8 +42,6 @@ export interface TSpawnCtxNormalized { spawnSync: typeof cp.spawnSync spawnOpts: Record callback: (err: TSpawnError, result: TSpawnResult) => void - onStdout: (data: string | Buffer) => void - onStderr: (data: string | Buffer) => void stdin: Readable stdout: Writable stderr: Writable @@ -69,8 +67,6 @@ export const normalizeCtx = (...ctxs: TSpawnCtx[]): TSpawnCtxNormalized => assig spawnSync: cp.spawnSync, spawnOpts: {}, callback: noop, - onStdout: noop, - onStderr: noop, stdin: new VoidWritable(), stdout: new VoidWritable(), stderr: new VoidWritable(), @@ -132,14 +128,13 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { stdio, get stdall() { return this.stdout + this.stderr }, duration: Date.now() - now, - _ctx: c + ctx: c }) c.ee.emit('end', c.fulfilled, c) } else { c.run(() => { let error: any = null - // let status: number | null = null const opts = buildSpawnOpts(c) const stderr: string[] = [] const stdout: string[] = [] @@ -148,8 +143,6 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { c.child = child opts.signal.addEventListener('abort', event => { - c.ee.emit('abort', event, c) - if (opts.detached && child.pid) { try { // https://github.com/nodejs/node/issues/51766 @@ -158,6 +151,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { child.kill() } } + c.ee.emit('abort', event, c) }) processInput(child, c.input || c.stdin) @@ -176,7 +170,6 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { error = e c.ee.emit('err', error, c) }) - // .on('exit', (_status) => status = _status) .on('close', (status, signal) => { c.callback(error, c.fulfilled = { error, @@ -187,7 +180,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { stdall: stdall.join(''), stdio: [c.stdin, c.stdout, c.stderr], duration: Date.now() - now, - _ctx: c + ctx: c }) c.ee.emit('end', c.fulfilled, c) }) @@ -205,7 +198,7 @@ export const invoke = (c: TSpawnCtxNormalized): TSpawnCtxNormalized => { stdall: '', stdio, duration: Date.now() - now, - _ctx: c + ctx: c } ) c.ee.emit('err', error, c) diff --git a/src/main/ts/x.ts b/src/main/ts/x.ts index 293d9ed..0bb96ff 100644 --- a/src/main/ts/x.ts +++ b/src/main/ts/x.ts @@ -48,9 +48,10 @@ export type TShellOptions = Omit & { input?: TShellCtx['input'] | TShellResponse | TShellResponseSync | null } & TShellOptionsExtra -export interface TShellResponse extends Omit, 'stdio' | '_ctx'>, Promise>, TShellResponseExtra { +export interface TShellResponse extends Omit, 'stdio' | 'ctx' | 'child'>, Promise>, TShellResponseExtra { + child: TZurk['child'] stdio: [Readable | Writable, Writable, Writable] - _ctx: TShellCtx + ctx: TShellCtx on: (event: string | symbol, listener: TZurkListener) => TShellResponse } @@ -104,24 +105,24 @@ const zurkMixin: TMixin = ($: TShell, target: TShellOptions | TZurk | TZurkPromi return isPromiseLike(result) ? zurkifyPromise( (result as TZurkPromise).then((r: TZurk) => applyMixins($, r, result)) as Promise, - result._ctx) + result.ctx) : result as TZurk } $.mixins = [zurkMixin, killMixin, pipeMixin, timeoutMixin] export const applyMixins = ($: TShell, result: TZurk | TZurkPromise | TShellOptions, parent?: TZurk | TZurkPromise) => { - let ctx: TShellCtx = (parent as TZurkPromise | TZurk)?._ctx + let ctx: TShellCtx = (parent as TZurkPromise | TZurk)?.ctx return $.mixins.reduce((r, m) => { - ctx = ctx || (r as TZurkPromise | TZurk)._ctx + ctx = ctx || (r as TZurkPromise | TZurk).ctx return m($, r as any, ctx) }, result) } export const parseInput = (input: TShellOptions['input']): TShellCtx['input'] => { if (typeof (input as TShellResponseSync)?.stdout === 'string') return (input as TShellResponseSync).stdout - if ((input as TShellResponse)?._ctx) return (input as TShellResponse)._ctx.stdout + if ((input as TShellResponse)?.ctx) return (input as TShellResponse).ctx.stdout return input as TShellCtx['input'] } diff --git a/src/main/ts/zurk.ts b/src/main/ts/zurk.ts index 8231bb3..7e0282c 100644 --- a/src/main/ts/zurk.ts +++ b/src/main/ts/zurk.ts @@ -1,3 +1,4 @@ +import type EventEmitter from 'node:events' import util from 'node:util' import { invoke, @@ -11,26 +12,37 @@ export const ZURK = Symbol('Zurk') export type TZurkListener = (value: any, ctx: TZurkCtx) => void +export type TZurkListeners = { + stdout: (data: Buffer, ctx: TZurkCtx) => void + stderr: (data: Buffer, ctx: TZurkCtx) => void + end: (result: TZurk, ctx: TZurkCtx) => void + err: (error: any, ctx: TZurkCtx) => void + abort: (error: any, ctx: TZurkCtx) => void +} + +// TODO infer export interface TZurkOn { - on(name: 'stdout', listener: (data: Buffer, ctx: TZurkCtx) => void): R - on(name: 'stderr', listener: (data: Buffer, ctx: TZurkCtx) => void): R - on(name: 'end', listener: (result: TZurk, ctx: TZurkCtx) => void): R - on(name: 'err', listener: (error: any, ctx: TZurkCtx) => void): R - on(name: 'abort', listener: (error: any, ctx: TZurkCtx) => void): R + on(name: T, listener: L): R + on(name: T, listener: L): R + on(name: T, listener: L): R + on(name: T, listener: L): R + on(name: T, listener: L): R } export interface TZurk extends TSpawnResult, TZurkOn { - _ctx: TZurkCtx - on(event: string | symbol, listener: TZurkListener): TZurk + ctx: TZurkCtx } export type TZurkCtx = TSpawnCtxNormalized & { nothrow?: boolean, nohandle?: boolean } -export type TZurkOptions = Partial> +export type TZurkOptions = Partial> & { + on?: Partial +} export type TZurkPromise = Promise & Promisified & TZurkOn & { - _ctx: TZurkCtx + ctx: TZurkCtx stdio: TZurkCtx['stdio'] + child: TZurkCtx['child'] } export const zurk = (opts: T): R => @@ -45,6 +57,7 @@ export const zurkAsync = (opts: TZurkOptions): TZurkPromise => { ctx.error && !ctx.nothrow ? reject(ctx.error) : resolve(zurkFactory(ctx)) } }) + attachListeners(ctx.ee, opts.on) invoke(ctx) @@ -61,6 +74,7 @@ export const zurkSync = (opts: TZurkOptions): TZurk => { response = zurkFactory(ctx) } }) + attachListeners(ctx.ee, opts.on) invoke(ctx) @@ -81,7 +95,8 @@ export const zurkifyPromise = (target: Promise | TZurkPromise, ctx: TSpaw if (p === 'catch') return target.catch.bind(target) if (p === 'finally') return target.finally.bind(target) if (p === 'stdio') return ctx.stdio - if (p === '_ctx') return ctx + if (p === 'ctx') return ctx + if (p === 'child') return ctx.child if (p === 'on') return function (name: string, cb: VoidFunction){ ctx.ee.on(name, cb); return proxy } if (p in target) return Reflect.get(target, p, receiver) @@ -93,6 +108,12 @@ export const zurkifyPromise = (target: Promise | TZurkPromise, ctx: TSpaw return proxy } +export const attachListeners = (ee: EventEmitter, on: Partial = {}) => { + for (const [name, listener] of Object.entries(on)) { + ee.on(name, listener as any) + } +} + export const getError = (data: TSpawnResult) => { if (data.error) return data.error if (data.status) return new Error(`Command failed with exit code ${data.status}`) @@ -109,23 +130,24 @@ export const zurkFactory = (ctx: C): TZurk => ne class Zurk implements TZurk { [ZURK] = ZURK - _ctx: TZurkCtx + ctx: TZurkCtx constructor(ctx: TZurkCtx) { - this._ctx = ctx + this.ctx = ctx } - on(name: string, cb: TVoidCallback): this { this._ctx.ee.on(name, cb); return this } - get status() { return this._ctx.fulfilled?.status ?? null } - get signal() { return this._ctx.fulfilled?.signal ?? null } - get error() { return this._ctx.error } - get stderr() { return this._ctx.fulfilled?.stderr || '' } - get stdout() { return this._ctx.fulfilled?.stdout || '' } - get stdall() { return this._ctx.fulfilled?.stdall || '' } + on(name: string, cb: TVoidCallback): this { this.ctx.ee.on(name, cb); return this } + get child() { return this.ctx.child } + get status() { return this.ctx.fulfilled?.status ?? null } + get signal() { return this.ctx.fulfilled?.signal ?? null } + get error() { return this.ctx.error } + get stderr() { return this.ctx.fulfilled?.stderr || '' } + get stdout() { return this.ctx.fulfilled?.stdout || '' } + get stdall() { return this.ctx.fulfilled?.stdall || '' } get stdio(): TSpawnResult['stdio'] { return [ - this._ctx.stdin, - this._ctx.stdout, - this._ctx.stderr + this.ctx.stdin, + this.ctx.stdout, + this.ctx.stderr ]} - get duration() { return this._ctx.fulfilled?.duration ?? 0 } + get duration() { return this.ctx.fulfilled?.duration ?? 0 } toString(){ return this.stdall.trim() } valueOf(){ return this.stdall.trim() } } diff --git a/src/test/ts/x.test.ts b/src/test/ts/x.test.ts index 478a001..2d00260 100644 --- a/src/test/ts/x.test.ts +++ b/src/test/ts/x.test.ts @@ -115,7 +115,7 @@ describe('mixins', () => { const p = $`sleep 10` p.timeoutSignal = 'SIGALRM' p.timeout = 25 - p._ctx.nothrow = true + p.ctx.nothrow = true const { error } = await p assert.equal(error.message, 'Command failed with signal SIGALRM') diff --git a/src/test/ts/zurk.test.ts b/src/test/ts/zurk.test.ts index d001c8c..d156623 100644 --- a/src/test/ts/zurk.test.ts +++ b/src/test/ts/zurk.test.ts @@ -1,6 +1,7 @@ import * as assert from 'node:assert' import { describe, it } from 'node:test' import { zurk, isZurk, isZurkPromise } from '../../main/ts/zurk.js' +import type { TSpawnResult } from '../../main/ts/spawn.js' describe('zurk()', () => { it('sync returns Zurk instance', async () => { @@ -14,6 +15,11 @@ describe('zurk()', () => { it('async returns ZurkPromise', async () => { const result = zurk({ sync: false, cmd: 'echo', args: ['foo']}) + let _result: TSpawnResult + + result.on('end', data => _result = data) + + assert.equal(result.child, result.ctx.child) assert.equal((await result).toString(), 'foo') assert.equal((await result).stdout, 'foo\n') assert.equal(await result.stdout, 'foo\n') @@ -21,5 +27,8 @@ describe('zurk()', () => { assert.equal(await result.signal, null) assert.ok(isZurkPromise(result)) assert.ok(isZurk(await result)) + + // @ts-expect-error should be resolved by now + assert.equal(_result.stdout, 'foo\n') }) })