Commit ae55549

Anton Golub <antongolub@antongolub.com>
2024-05-21 15:07:37
feat: introduce `$.preferLocal` option (#798)
* feat: introduce `$.preferLocal` option * fix: handle $PATH on win32 * refactor: move `preferNmBin` to utils
1 parent 01af3d5
src/core.ts
@@ -40,6 +40,7 @@ import {
   quotePowerShell,
   noquote,
   ensureEol,
+  preferNmBin,
 } from './util.js'
 
 export interface Shell {
@@ -74,6 +75,7 @@ export interface Options {
   quote: typeof quote
   quiet: boolean
   detached: boolean
+  preferLocal: boolean
   spawn: typeof spawn
   spawnSync: typeof spawnSync
   log: typeof log
@@ -108,6 +110,7 @@ export const defaults: Options = {
   postfix: '',
   quote: noquote,
   detached: false,
+  preferLocal: false,
   spawn,
   spawnSync,
   log,
@@ -251,6 +254,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
 
     if (input) this.stdio('pipe')
     if ($.timeout) this.timeout($.timeout, $.timeoutSignal)
+    if ($.preferLocal) $.env = preferNmBin($.env, $.cwd, $[processCwd])
 
     $.log({
       kind: 'cmd',
src/util.ts
@@ -46,6 +46,28 @@ export function isString(obj: any) {
 
 const pad = (v: string) => (v === ' ' ? ' ' : '')
 
+export function preferNmBin(
+  env: NodeJS.ProcessEnv,
+  ...dirs: (string | undefined)[]
+) {
+  const pathKey =
+    process.platform === 'win32'
+      ? Object.keys(env)
+          .reverse()
+          .find((key) => key.toUpperCase() === 'PATH') || 'Path'
+      : 'PATH'
+  const pathValue = dirs
+    .map((c) => c && path.resolve(c as string, 'node_modules', '.bin'))
+    .concat(env[pathKey])
+    .filter(Boolean)
+    .join(path.delimiter)
+
+  return {
+    ...env,
+    [pathKey]: pathValue,
+  }
+}
+
 export function normalizeMultilinePieces(
   pieces: TemplateStringsArray
 ): TemplateStringsArray {
test/core.test.js
@@ -43,9 +43,10 @@ describe('core', () => {
       assert.equal(foo.stdout, 'foo\n')
     })
 
-    test('env vars is safe to pass', async () => {
+    test('env vars are safe to pass', async () => {
       process.env.ZX_TEST_BAR = 'hi; exit 1'
-      await $`echo $ZX_TEST_BAR`
+      const bar = await $`echo $ZX_TEST_BAR`
+      assert.equal(bar.stdout, 'hi; exit 1\n')
     })
 
     test('arguments are quoted', async () => {
@@ -205,6 +206,20 @@ describe('core', () => {
         assert.equal(exitCode, null)
         assert.equal(signal, 'SIGKILL')
       })
+
+      test('`env` option', async () => {
+        const baz = await $({
+          env: { ZX_TEST_BAZ: 'baz' },
+        })`echo $ZX_TEST_BAZ`
+        assert.equal(baz.stdout, 'baz\n')
+      })
+
+      test('`preferLocal` preserves env', async () => {
+        const path = await $({
+          preferLocal: true,
+        })`echo $PATH`
+        assert(path.stdout.startsWith(`${process.cwd()}/node_modules/.bin:`))
+      })
     })
 
     test('accepts `stdio`', async () => {
test/util.test.js
@@ -30,6 +30,7 @@ import {
   tempdir,
   tempfile,
   ensureEol,
+  preferNmBin,
 } from '../build/util.js'
 
 describe('util', () => {
@@ -183,3 +184,11 @@ test('ensureEol() should ensure buffer ends with a newline character', () => {
   const result3 = ensureEol(buffer3).toString()
   assert.strictEqual(result3, '\n')
 })
+
+test('preferNmBin()', () => {
+  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}`)
+})