diff --git a/index.d.ts b/index.d.ts index 0c1b160..25eca12 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -export interface RunPathOptions { +type CommonOptions = { /** Working directory. @@ -7,46 +7,50 @@ export interface RunPathOptions { readonly cwd?: string | URL; /** - PATH to be appended. Default: [`PATH`](https://github.com/sindresorhus/path-key). - - Set it to an empty string to exclude the default PATH. - */ - readonly path?: string; - - /** - Path to the Node.js executable to use in child processes if that is different from the current one. Its directory is pushed to the front of PATH. + The path to the current Node.js executable. This can be either an absolute path or a path relative to the `cwd` option. - @default process.execPath + @default [process.execPath](https://nodejs.org/api/process.html#processexecpath) */ readonly execPath?: string | URL; -} - -export type ProcessEnv = Record; -export interface EnvOptions { /** - The working directory. + Whether to push the current Node.js executable's directory (`execPath` option) to the front of PATH. - @default process.cwd() + @default true */ - readonly cwd?: string | URL; + readonly addExecPath?: boolean; /** - Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. + Whether to push the locally installed binaries' directory to the front of PATH. + + @default true */ - readonly env?: ProcessEnv; + readonly preferLocal?: boolean; +}; +export type RunPathOptions = CommonOptions & { /** - The path to the current Node.js executable. Its directory is pushed to the front of PATH. + PATH to be appended. - This can be either an absolute path or a path relative to the `cwd` option. + Set it to an empty string to exclude the default PATH. - @default process.execPath + @default [`PATH`](https://github.com/sindresorhus/path-key) */ - readonly execPath?: string | URL; -} + readonly path?: string; +}; + +export type ProcessEnv = Record; + +export type EnvOptions = CommonOptions & { + /** + Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. + + @default [process.env](https://nodejs.org/api/process.html#processenv) + */ + readonly env?: ProcessEnv; +}; /** Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries. @@ -68,6 +72,8 @@ console.log(npmRunPath()); export function npmRunPath(options?: RunPathOptions): string; /** +Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries. + @returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. @example diff --git a/index.js b/index.js index ee779b8..9d6c637 100644 --- a/index.js +++ b/index.js @@ -1,39 +1,52 @@ import process from 'node:process'; import path from 'node:path'; -import url from 'node:url'; +import {fileURLToPath} from 'node:url'; import pathKey from 'path-key'; -export function npmRunPath(options = {}) { - const { - cwd = process.cwd(), - path: path_ = process.env[pathKey()], - execPath = process.execPath, - } = options; +export const npmRunPath = ({ + cwd = process.cwd(), + path: pathOption = process.env[pathKey()], + preferLocal = true, + execPath = process.execPath, + addExecPath = true, +} = {}) => { + const cwdString = cwd instanceof URL ? fileURLToPath(cwd) : cwd; + const cwdPath = path.resolve(cwdString); + const result = []; + + if (preferLocal) { + applyPreferLocal(result, cwdPath); + } + + if (addExecPath) { + applyExecPath(result, execPath, cwdPath); + } + + return [...result, pathOption].join(path.delimiter); +}; +const applyPreferLocal = (result, cwdPath) => { let previous; - const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath; - const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd; - let cwdPath = path.resolve(cwdString); - const result = []; while (previous !== cwdPath) { result.push(path.join(cwdPath, 'node_modules/.bin')); previous = cwdPath; cwdPath = path.resolve(cwdPath, '..'); } +}; - // Ensure the running `node` binary is used. - result.push(path.resolve(cwdString, execPathString, '..')); - - return [...result, path_].join(path.delimiter); -} +// Ensure the running `node` binary is used +const applyExecPath = (result, execPath, cwdPath) => { + const execPathString = execPath instanceof URL ? fileURLToPath(execPath) : execPath; + result.push(path.resolve(cwdPath, execPathString, '..')); +}; -export function npmRunPathEnv({env = process.env, ...options} = {}) { +export const npmRunPathEnv = ({env = process.env, ...options} = {}) => { env = {...env}; - const path = pathKey({env}); - options.path = env[path]; - env[path] = npmRunPath(options); + const pathName = pathKey({env}); + options.path = env[pathName]; + env[pathName] = npmRunPath(options); return env; -} +}; diff --git a/index.test-d.ts b/index.test-d.ts index d977bd9..184acdf 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,17 +1,38 @@ import process from 'node:process'; -import {expectType} from 'tsd'; +import {expectType, expectError} from 'tsd'; import {npmRunPath, npmRunPathEnv, ProcessEnv} from './index.js'; +const fileUrl = new URL('file:///foo'); + expectType(npmRunPath()); expectType(npmRunPath({cwd: '/foo'})); -expectType(npmRunPath({cwd: new URL('file:///foo')})); +expectType(npmRunPath({cwd: fileUrl})); +expectError(npmRunPath({cwd: false})); expectType(npmRunPath({path: '/usr/local/bin'})); +expectError(npmRunPath({path: fileUrl})); +expectError(npmRunPath({path: false})); expectType(npmRunPath({execPath: '/usr/local/bin'})); -expectType(npmRunPath({execPath: new URL('file:///usr/local/bin')})); +expectType(npmRunPath({execPath: fileUrl})); +expectError(npmRunPath({execPath: false})); +expectType(npmRunPath({addExecPath: false})); +expectError(npmRunPath({addExecPath: ''})); +expectType(npmRunPath({preferLocal: false})); +expectError(npmRunPath({preferLocal: ''})); expectType(npmRunPathEnv()); expectType(npmRunPathEnv({cwd: '/foo'})); -expectType(npmRunPathEnv({cwd: new URL('file:///foo')})); +expectType(npmRunPathEnv({cwd: fileUrl})); +expectError(npmRunPathEnv({cwd: false})); expectType(npmRunPathEnv({env: process.env})); // eslint-disable-line @typescript-eslint/no-unsafe-assignment +expectType(npmRunPathEnv({env: {foo: 'bar'}})); +expectType(npmRunPathEnv({env: {foo: undefined}})); +expectError(npmRunPath({env: false})); +expectError(npmRunPath({env: {[Symbol('key')]: 'bar'}})); +expectError(npmRunPath({env: {foo: false}})); expectType(npmRunPathEnv({execPath: '/usr/local/bin'})); -expectType(npmRunPathEnv({execPath: new URL('file:///usr/local/bin')})); +expectType(npmRunPathEnv({execPath: fileUrl})); +expectError(npmRunPath({execPath: false})); +expectType(npmRunPathEnv({addExecPath: false})); +expectError(npmRunPathEnv({addExecPath: ''})); +expectType(npmRunPathEnv({preferLocal: false})); +expectError(npmRunPathEnv({preferLocal: ''})); diff --git a/readme.md b/readme.md index acc6faf..bec78f9 100644 --- a/readme.md +++ b/readme.md @@ -32,66 +32,71 @@ childProcess.execFileSync('foo', { ### npmRunPath(options?) +`options`: [`Options`](#options)\ +_Returns_: `string` + Returns the augmented PATH string. -#### options +### npmRunPathEnv(options?) + +`options`: [`Options`](#options)\ +_Returns_: `object` + +Returns the augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. + +### options Type: `object` -##### cwd +#### cwd Type: `string | URL`\ Default: `process.cwd()` The working directory. -##### path - -Type: `string`\ -Default: [`PATH`](https://github.com/sindresorhus/path-key) - -The PATH to be appended. - -Set it to an empty string to exclude the default PATH. - -##### execPath +#### execPath Type: `string | URL`\ -Default: `process.execPath` +Default: [`process.execPath`](https://nodejs.org/api/process.html#processexecpath) -The path to the current Node.js executable. Its directory is pushed to the front of PATH. +The path to the current Node.js executable. This can be either an absolute path or a path relative to the [`cwd` option](#cwd). -### npmRunPathEnv(options?) +#### addExecPath -Returns the augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. +Type: `boolean`\ +Default: `true` -#### options +Whether to push the current Node.js executable's directory ([`execPath`](#execpath) option) to the front of PATH. -Type: `object` +#### preferLocal -##### cwd +Type: `boolean`\ +Default: `true` -Type: `string | URL`\ -Default: `process.cwd()` +Whether to push the locally installed binaries' directory to the front of PATH. -The working directory. +#### path -##### env +Type: `string`\ +Default: [`PATH`](https://github.com/sindresorhus/path-key) -Type: `object` +The PATH to be appended. -Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. +Set it to an empty string to exclude the default PATH. -##### execPath +Only available with [`npmRunPath()`](#npmrunpathoptions), not [`npmRunPathEnv()`](#npmrunpathenvoptions). -Type: `string`\ -Default: `process.execPath` +#### env -The path to the Node.js executable to use in child processes if that is different from the current one. Its directory is pushed to the front of PATH. +Type: `object`\ +Default: [`process.env`](https://nodejs.org/api/process.html#processenv) -This can be either an absolute path or a path relative to the [`cwd` option](#cwd). +Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. + +Only available with [`npmRunPathEnv()`](#npmrunpathenvoptions), not [`npmRunPath()`](#npmrunpathoptions). ## Related diff --git a/test.js b/test.js index 633a7f3..d3a6a0f 100644 --- a/test.js +++ b/test.js @@ -6,17 +6,29 @@ import {npmRunPath, npmRunPathEnv} from './index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -test('main', t => { +const testLocalDir = (t, addExecPath, preferLocal, expectedResult) => { t.is( - npmRunPath({path: ''}).split(path.delimiter)[0], - path.join(__dirname, 'node_modules/.bin'), + npmRunPath({path: '', addExecPath, preferLocal}).split(path.delimiter)[0] === path.join(__dirname, 'node_modules/.bin'), + expectedResult, ); +}; +test('Adds node_modules/.bin - npmRunPath()', testLocalDir, undefined, undefined, true); +test('"addExecPath: false" still adds node_modules/.bin - npmRunPath()', testLocalDir, false, undefined, true); +test('"preferLocal: false" does not add node_modules/.bin - npmRunPath()', testLocalDir, undefined, false, false); +test('"preferLocal: false", "addExecPath: false" does not add node_modules/.bin - npmRunPath()', testLocalDir, false, false, false); + +const testLocalDirEnv = (t, addExecPath, preferLocal, expectedResult) => { t.is( - npmRunPathEnv({env: {PATH: 'foo'}}).PATH.split(path.delimiter)[0], - path.join(__dirname, 'node_modules/.bin'), + npmRunPathEnv({env: {PATH: 'foo'}, addExecPath, preferLocal}).PATH.split(path.delimiter)[0] === path.join(__dirname, 'node_modules/.bin'), + expectedResult, ); -}); +}; + +test('Adds node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, undefined, undefined, true); +test('"addExecPath: false" still adds node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, false, undefined, true); +test('"preferLocal: false" does not add node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, undefined, false, false); +test('"preferLocal: false", "addExecPath: false" does not add node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, false, false, false); test('the `cwd` option changes the current directory', t => { t.is( @@ -37,18 +49,29 @@ test('push `execPath` later in the PATH', t => { t.is(pathEnv[pathEnv.length - 2], path.dirname(process.execPath)); }); -test('can change `execPath` with the `execPath` option', t => { - const pathEnv = npmRunPath({path: '', execPath: 'test/test'}).split( - path.delimiter, - ); - t.is(pathEnv[pathEnv.length - 2], path.resolve(process.cwd(), 'test')); -}); +const testExecPath = (t, preferLocal, addExecPath, expectedResult) => { + const pathEnv = npmRunPath({path: '', execPath: 'test/test', preferLocal, addExecPath}).split(path.delimiter); + t.is(pathEnv[pathEnv.length - 2] === path.resolve('test'), expectedResult); +}; + +test('can change `execPath` with the `execPath` option - npmRunPath()', testExecPath, undefined, undefined, true); +test('"preferLocal: false" still adds execPath - npmRunPath()', testExecPath, false, undefined, true); +test('"addExecPath: false" does not add execPath - npmRunPath()', testExecPath, undefined, false, false); +test('"addExecPath: false", "preferLocal: false" does not add execPath - npmRunPath()', testExecPath, false, false, false); + +const testExecPathEnv = (t, preferLocal, addExecPath, expectedResult) => { + const pathEnv = npmRunPathEnv({env: {PATH: 'foo'}, execPath: 'test/test', preferLocal, addExecPath}).PATH.split(path.delimiter); + t.is(pathEnv[pathEnv.length - 2] === path.resolve('test'), expectedResult); +}; + +test('can change `execPath` with the `execPath` option - npmRunPathEnv()', testExecPathEnv, undefined, undefined, true); +test('"preferLocal: false" still adds execPath - npmRunPathEnv()', testExecPathEnv, false, undefined, true); +test('"addExecPath: false" does not add execPath - npmRunPathEnv()', testExecPathEnv, undefined, false, false); +test('"addExecPath: false", "preferLocal: false" does not add execPath - npmRunPathEnv()', testExecPathEnv, false, false, false); test('the `execPath` option can be a file URL', t => { - const pathEnv = npmRunPath({path: '', execPath: pathToFileURL('test/test')}).split( - path.delimiter, - ); - t.is(pathEnv[pathEnv.length - 2], path.resolve(process.cwd(), 'test')); + const pathEnv = npmRunPath({path: '', execPath: pathToFileURL('test/test')}).split(path.delimiter); + t.is(pathEnv[pathEnv.length - 2], path.resolve('test')); }); test('the `execPath` option is relative to the `cwd` option', t => {