Commit fbf2ddf

Anton Golub <antongolub@antongolub.com>
2024-09-08 19:13:53
feat: let `$.preferLocal` accept dir (#886)
* feat: provide `$.killSignal` option * feat: let `$.preferLocal` accept directories
1 parent 76463de
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<Options>()
@@ -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<Shell & Options>(
     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<ProcessOutput> {
   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<ProcessOutput> {
 
     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<ProcessOutput> {
     return this._snapshot.signal || this._snapshot.ac?.signal
   }
 
-  async kill(signal = 'SIGTERM'): Promise<void> {
+  async kill(signal = $.killSignal): 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.')
@@ -530,7 +537,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     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 {
src/goods.ts
@@ -182,13 +182,12 @@ export async function spinner<T>(
   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
   })
 }
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)
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 () => {
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}`
+  )
 })