diff --git a/src/env.ts b/src/env.ts index b267be2..7b1c1da 100644 --- a/src/env.ts +++ b/src/env.ts @@ -36,17 +36,20 @@ export function getPathFromEnv(env: EnvLike): EnvPathInfo { function addNodeBinToPath(cwd: string, path: EnvPathInfo): EnvPathInfo { const parts = path.value.split(pathDelimiter); + const nodeModulesBinPaths: string[] = []; let currentPath = cwd; let lastPath: string; do { - parts.push(resolvePath(currentPath, 'node_modules', '.bin')); + nodeModulesBinPaths.push(resolvePath(currentPath, 'node_modules', '.bin')); lastPath = currentPath; currentPath = dirname(currentPath); } while (currentPath !== lastPath); - return {key: path.key, value: parts.join(pathDelimiter)}; + const newPath = nodeModulesBinPaths.concat(parts).join(pathDelimiter); + + return {key: path.key, value: newPath}; } export function computeEnv(cwd: string, env?: EnvLike): EnvLike { diff --git a/src/test/env_test.ts b/src/test/env_test.ts index 80f66d1..87f019f 100644 --- a/src/test/env_test.ts +++ b/src/test/env_test.ts @@ -1,7 +1,7 @@ import {computeEnv, getPathFromEnv} from '../env.js'; import {expect, test, describe} from 'vitest'; import process from 'node:process'; -import {sep as pathSep} from 'node:path'; +import path, {sep as pathSep, delimiter as pathDelimiter} from 'node:path'; const pathKey = getPathFromEnv(process.env).key; @@ -69,4 +69,36 @@ describe('computeEnv', async () => { process.env[pathKey] = originalPath; } }); + + test('prepends local node_modules/.bin to PATH', () => { + /** The original variable is just `PATH=/usr/local/bin` */ + const originalPath = path.join(pathSep, 'usr', 'local', 'bin'); + const cwd = path.resolve(pathSep, 'one', 'two', 'three'); + + const env = computeEnv(cwd, { + PATH: originalPath + }); + + /** + * After computing, the PATH is now prefixed with all the `node_modules/.bin` + * directories starting from the CWD=/one/two/three. This means local binaries + * are preferred from the closest directory to the CWD, and are preferred to + * the global ones from the existing path. Essentially, if `eslint` is installed + * via `npm` to a local directory, it is preferred to a globally-installed `eslint`. + * + * This should match the behavior of `npm` path resolution algorithm used for + * running scripts. + * + * @link https://github.com/npm/run-script/blob/08ad35e66f0d09ed7a6b85b9a457e54859b70acd/lib/set-path.js#L37 + */ + const expected = [ + path.resolve(pathSep, 'one', 'two', 'three', 'node_modules', '.bin'), + path.resolve(pathSep, 'one', 'two', 'node_modules', '.bin'), + path.resolve(pathSep, 'one', 'node_modules', '.bin'), + path.resolve(pathSep, 'node_modules', '.bin'), + originalPath + ].join(pathDelimiter); + + expect(env[pathKey]).toBe(expected); + }); });