diff --git a/src/core.ts b/src/core.ts index cde45d4558..d0120adb4c 100644 --- a/src/core.ts +++ b/src/core.ts @@ -44,7 +44,7 @@ import { isString, noop, parseDuration, - preferNmBin, + preferLocalBin, quote, quotePowerShell, } from './util.js' @@ -70,7 +70,7 @@ export interface Options { signal?: AbortSignal input?: string | Buffer | Readable | ProcessOutput | ProcessPromise timeout?: Duration - timeoutSignal?: string + timeoutSignal?: NodeJS.Signals stdio: StdioOptions verbose: boolean sync: boolean @@ -82,12 +82,13 @@ export interface Options { quote?: typeof quote quiet: boolean detached: boolean - preferLocal: boolean + preferLocal: boolean | string | string[] spawn: typeof spawn spawnSync: typeof spawnSync store?: TSpawnStore log: typeof log kill: typeof kill + killSignal?: NodeJS.Signals } const storage = new AsyncLocalStorage() @@ -125,6 +126,8 @@ export const defaults: Options = { spawnSync, log, kill, + killSignal: 'SIGTERM', + timeoutSignal: 'SIGTERM', } export function usePowerShell() { @@ -168,9 +171,9 @@ export const $: Shell & Options = new Proxy( if (!Array.isArray(pieces)) { return function (this: any, ...args: any) { const self = this - return within(() => { - return Object.assign($, snapshot, pieces).apply(self, args) - }) + return within(() => + Object.assign($, snapshot, pieces).apply(self, args) + ) } } const from = getCallerLocation() @@ -237,7 +240,7 @@ export class ProcessPromise extends Promise { private _quiet?: boolean private _verbose?: boolean private _timeout?: number - private _timeoutSignal = 'SIGTERM' + private _timeoutSignal = $.timeoutSignal private _resolved = false private _halted = false private _piped = false @@ -270,7 +273,11 @@ export class ProcessPromise extends Promise { if (input) this.stdio('pipe') if ($.timeout) this.timeout($.timeout, $.timeoutSignal) - if ($.preferLocal) $.env = preferNmBin($.env, $.cwd, $[processCwd]) + if ($.preferLocal) { + const dirs = + $.preferLocal === true ? [$.cwd, $[processCwd]] : [$.preferLocal].flat() + $.env = preferLocalBin($.env, ...dirs) + } $.log({ kind: 'cmd', @@ -486,7 +493,7 @@ export class ProcessPromise extends Promise { return this._snapshot.signal || this._snapshot.ac?.signal } - async kill(signal = 'SIGTERM'): Promise { + async kill(signal = $.killSignal): Promise { 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.') @@ -530,7 +537,7 @@ export class ProcessPromise extends Promise { return this._nothrow ?? this._snapshot.nothrow } - timeout(d: Duration, signal = 'SIGTERM'): ProcessPromise { + timeout(d: Duration, signal = $.timeoutSignal): ProcessPromise { this._timeout = parseDuration(d) this._timeoutSignal = signal return this @@ -686,7 +693,7 @@ export function cd(dir: string | ProcessOutput) { $[processCwd] = process.cwd() } -export async function kill(pid: number, signal?: string) { +export async function kill(pid: number, signal = $.killSignal) { let children = await ps.tree({ pid, recursive: true }) for (const p of children) { try { diff --git a/src/goods.ts b/src/goods.ts index 783a7bd341..d1afb0aa17 100644 --- a/src/goods.ts +++ b/src/goods.ts @@ -182,13 +182,12 @@ export async function spinner( return within(async () => { $.verbose = false const id = setInterval(spin, 100) - let result: T + try { - result = await callback!() + return await callback!() } finally { clearInterval(id as NodeJS.Timeout) process.stderr.write(' '.repeat((process.stdout.columns || 1) - 1) + '\r') } - return result }) } diff --git a/src/util.ts b/src/util.ts index 458c670f90..d4908ef9d1 100644 --- a/src/util.ts +++ b/src/util.ts @@ -47,7 +47,7 @@ export function isString(obj: any) { const pad = (v: string) => (v === ' ' ? ' ' : '') -export function preferNmBin( +export function preferLocalBin( env: NodeJS.ProcessEnv, ...dirs: (string | undefined)[] ) { @@ -58,7 +58,14 @@ export function preferNmBin( .find((key) => key.toUpperCase() === 'PATH') || 'Path' : 'PATH' const pathValue = dirs - .map((c) => c && path.resolve(c as string, 'node_modules', '.bin')) + .map( + (c) => + c && [ + path.resolve(c as string, 'node_modules', '.bin'), + path.resolve(c as string), + ] + ) + .flat() .concat(env[pathKey]) .filter(Boolean) .join(path.delimiter) diff --git a/test/core.test.js b/test/core.test.js index 12b38641ff..8feb7e3d31 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -220,10 +220,22 @@ describe('core', () => { }) test('`preferLocal` preserves env', async () => { - const path = await $({ - preferLocal: true, - })`echo $PATH` - assert(path.stdout.startsWith(`${process.cwd()}/node_modules/.bin:`)) + const cases = [ + [true, `${process.cwd()}/node_modules/.bin:${process.cwd()}:`], + ['/foo', `/foo/node_modules/.bin:/foo:`], + [ + ['/bar', '/baz'], + `/bar/node_modules/.bin:/bar:/baz/node_modules/.bin:/baz`, + ], + ] + + for (const [preferLocal, expected] of cases) { + const path = await $({ + preferLocal, + env: { PATH: process.env.PATH }, + })`echo $PATH` + assert(path.stdout.startsWith(expected)) + } }) test('supports custom intermediate store', async () => { diff --git a/test/util.test.js b/test/util.test.js index 86017a9c02..3770b972ea 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -29,7 +29,7 @@ import { getCallerLocationFromString, tempdir, tempfile, - preferNmBin, + preferLocalBin, } from '../build/util.js' describe('util', () => { @@ -169,10 +169,13 @@ test('tempfile() creates temporary files', () => { assert.equal(fs.readFileSync(tf, 'utf-8'), 'bar') }) -test('preferNmBin()', () => { +test('preferLocalBin()', () => { const env = { PATH: '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/local/sbin', } - const _env = preferNmBin(env, process.cwd()) - assert.equal(_env.PATH, `${process.cwd()}/node_modules/.bin:${env.PATH}`) + const _env = preferLocalBin(env, process.cwd()) + assert.equal( + _env.PATH, + `${process.cwd()}/node_modules/.bin:${process.cwd()}:${env.PATH}` + ) })