Skip to content

Commit

Permalink
Add preferLocal and addExecaPath options (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky authored Feb 22, 2024
1 parent 8462fd2 commit 09419af
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 96 deletions.
54 changes: 30 additions & 24 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface RunPathOptions {
type CommonOptions = {
/**
Working directory.
Expand All @@ -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<string, string | undefined>;

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<string, string | undefined>;

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.
Expand All @@ -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
Expand Down
55 changes: 34 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
31 changes: 26 additions & 5 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<string>(npmRunPath());
expectType<string>(npmRunPath({cwd: '/foo'}));
expectType<string>(npmRunPath({cwd: new URL('file:///foo')}));
expectType<string>(npmRunPath({cwd: fileUrl}));
expectError(npmRunPath({cwd: false}));
expectType<string>(npmRunPath({path: '/usr/local/bin'}));
expectError(npmRunPath({path: fileUrl}));
expectError(npmRunPath({path: false}));
expectType<string>(npmRunPath({execPath: '/usr/local/bin'}));
expectType<string>(npmRunPath({execPath: new URL('file:///usr/local/bin')}));
expectType<string>(npmRunPath({execPath: fileUrl}));
expectError(npmRunPath({execPath: false}));
expectType<string>(npmRunPath({addExecPath: false}));
expectError(npmRunPath({addExecPath: ''}));
expectType<string>(npmRunPath({preferLocal: false}));
expectError(npmRunPath({preferLocal: ''}));

expectType<ProcessEnv>(npmRunPathEnv());
expectType<ProcessEnv>(npmRunPathEnv({cwd: '/foo'}));
expectType<ProcessEnv>(npmRunPathEnv({cwd: new URL('file:///foo')}));
expectType<ProcessEnv>(npmRunPathEnv({cwd: fileUrl}));
expectError(npmRunPathEnv({cwd: false}));
expectType<ProcessEnv>(npmRunPathEnv({env: process.env})); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
expectType<ProcessEnv>(npmRunPathEnv({env: {foo: 'bar'}}));
expectType<ProcessEnv>(npmRunPathEnv({env: {foo: undefined}}));
expectError(npmRunPath({env: false}));
expectError(npmRunPath({env: {[Symbol('key')]: 'bar'}}));
expectError(npmRunPath({env: {foo: false}}));
expectType<ProcessEnv>(npmRunPathEnv({execPath: '/usr/local/bin'}));
expectType<ProcessEnv>(npmRunPathEnv({execPath: new URL('file:///usr/local/bin')}));
expectType<ProcessEnv>(npmRunPathEnv({execPath: fileUrl}));
expectError(npmRunPath({execPath: false}));
expectType<ProcessEnv>(npmRunPathEnv({addExecPath: false}));
expectError(npmRunPathEnv({addExecPath: ''}));
expectType<ProcessEnv>(npmRunPathEnv({preferLocal: false}));
expectError(npmRunPathEnv({preferLocal: ''}));
65 changes: 35 additions & 30 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 39 additions & 16 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 => {
Expand Down

0 comments on commit 09419af

Please sign in to comment.