Skip to content

Commit 09419af

Browse files
authored
Add preferLocal and addExecaPath options (#18)
1 parent 8462fd2 commit 09419af

File tree

5 files changed

+164
-96
lines changed

5 files changed

+164
-96
lines changed

index.d.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface RunPathOptions {
1+
type CommonOptions = {
22
/**
33
Working directory.
44
@@ -7,46 +7,50 @@ export interface RunPathOptions {
77
readonly cwd?: string | URL;
88

99
/**
10-
PATH to be appended. Default: [`PATH`](https://github.com/sindresorhus/path-key).
11-
12-
Set it to an empty string to exclude the default PATH.
13-
*/
14-
readonly path?: string;
15-
16-
/**
17-
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.
10+
The path to the current Node.js executable.
1811
1912
This can be either an absolute path or a path relative to the `cwd` option.
2013
21-
@default process.execPath
14+
@default [process.execPath](https://nodejs.org/api/process.html#processexecpath)
2215
*/
2316
readonly execPath?: string | URL;
24-
}
25-
26-
export type ProcessEnv = Record<string, string | undefined>;
2717

28-
export interface EnvOptions {
2918
/**
30-
The working directory.
19+
Whether to push the current Node.js executable's directory (`execPath` option) to the front of PATH.
3120
32-
@default process.cwd()
21+
@default true
3322
*/
34-
readonly cwd?: string | URL;
23+
readonly addExecPath?: boolean;
3524

3625
/**
37-
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.
26+
Whether to push the locally installed binaries' directory to the front of PATH.
27+
28+
@default true
3829
*/
39-
readonly env?: ProcessEnv;
30+
readonly preferLocal?: boolean;
31+
};
4032

33+
export type RunPathOptions = CommonOptions & {
4134
/**
42-
The path to the current Node.js executable. Its directory is pushed to the front of PATH.
35+
PATH to be appended.
4336
44-
This can be either an absolute path or a path relative to the `cwd` option.
37+
Set it to an empty string to exclude the default PATH.
4538
46-
@default process.execPath
39+
@default [`PATH`](https://github.com/sindresorhus/path-key)
4740
*/
48-
readonly execPath?: string | URL;
49-
}
41+
readonly path?: string;
42+
};
43+
44+
export type ProcessEnv = Record<string, string | undefined>;
45+
46+
export type EnvOptions = CommonOptions & {
47+
/**
48+
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.
49+
50+
@default [process.env](https://nodejs.org/api/process.html#processenv)
51+
*/
52+
readonly env?: ProcessEnv;
53+
};
5054

5155
/**
5256
Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries.
@@ -68,6 +72,8 @@ console.log(npmRunPath());
6872
export function npmRunPath(options?: RunPathOptions): string;
6973

7074
/**
75+
Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries.
76+
7177
@returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object.
7278
7379
@example

index.js

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,52 @@
11
import process from 'node:process';
22
import path from 'node:path';
3-
import url from 'node:url';
3+
import {fileURLToPath} from 'node:url';
44
import pathKey from 'path-key';
55

6-
export function npmRunPath(options = {}) {
7-
const {
8-
cwd = process.cwd(),
9-
path: path_ = process.env[pathKey()],
10-
execPath = process.execPath,
11-
} = options;
6+
export const npmRunPath = ({
7+
cwd = process.cwd(),
8+
path: pathOption = process.env[pathKey()],
9+
preferLocal = true,
10+
execPath = process.execPath,
11+
addExecPath = true,
12+
} = {}) => {
13+
const cwdString = cwd instanceof URL ? fileURLToPath(cwd) : cwd;
14+
const cwdPath = path.resolve(cwdString);
15+
const result = [];
16+
17+
if (preferLocal) {
18+
applyPreferLocal(result, cwdPath);
19+
}
20+
21+
if (addExecPath) {
22+
applyExecPath(result, execPath, cwdPath);
23+
}
24+
25+
return [...result, pathOption].join(path.delimiter);
26+
};
1227

28+
const applyPreferLocal = (result, cwdPath) => {
1329
let previous;
14-
const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath;
15-
const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd;
16-
let cwdPath = path.resolve(cwdString);
17-
const result = [];
1830

1931
while (previous !== cwdPath) {
2032
result.push(path.join(cwdPath, 'node_modules/.bin'));
2133
previous = cwdPath;
2234
cwdPath = path.resolve(cwdPath, '..');
2335
}
36+
};
2437

25-
// Ensure the running `node` binary is used.
26-
result.push(path.resolve(cwdString, execPathString, '..'));
27-
28-
return [...result, path_].join(path.delimiter);
29-
}
38+
// Ensure the running `node` binary is used
39+
const applyExecPath = (result, execPath, cwdPath) => {
40+
const execPathString = execPath instanceof URL ? fileURLToPath(execPath) : execPath;
41+
result.push(path.resolve(cwdPath, execPathString, '..'));
42+
};
3043

31-
export function npmRunPathEnv({env = process.env, ...options} = {}) {
44+
export const npmRunPathEnv = ({env = process.env, ...options} = {}) => {
3245
env = {...env};
3346

34-
const path = pathKey({env});
35-
options.path = env[path];
36-
env[path] = npmRunPath(options);
47+
const pathName = pathKey({env});
48+
options.path = env[pathName];
49+
env[pathName] = npmRunPath(options);
3750

3851
return env;
39-
}
52+
};

index.test-d.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
11
import process from 'node:process';
2-
import {expectType} from 'tsd';
2+
import {expectType, expectError} from 'tsd';
33
import {npmRunPath, npmRunPathEnv, ProcessEnv} from './index.js';
44

5+
const fileUrl = new URL('file:///foo');
6+
57
expectType<string>(npmRunPath());
68
expectType<string>(npmRunPath({cwd: '/foo'}));
7-
expectType<string>(npmRunPath({cwd: new URL('file:///foo')}));
9+
expectType<string>(npmRunPath({cwd: fileUrl}));
10+
expectError(npmRunPath({cwd: false}));
811
expectType<string>(npmRunPath({path: '/usr/local/bin'}));
12+
expectError(npmRunPath({path: fileUrl}));
13+
expectError(npmRunPath({path: false}));
914
expectType<string>(npmRunPath({execPath: '/usr/local/bin'}));
10-
expectType<string>(npmRunPath({execPath: new URL('file:///usr/local/bin')}));
15+
expectType<string>(npmRunPath({execPath: fileUrl}));
16+
expectError(npmRunPath({execPath: false}));
17+
expectType<string>(npmRunPath({addExecPath: false}));
18+
expectError(npmRunPath({addExecPath: ''}));
19+
expectType<string>(npmRunPath({preferLocal: false}));
20+
expectError(npmRunPath({preferLocal: ''}));
1121

1222
expectType<ProcessEnv>(npmRunPathEnv());
1323
expectType<ProcessEnv>(npmRunPathEnv({cwd: '/foo'}));
14-
expectType<ProcessEnv>(npmRunPathEnv({cwd: new URL('file:///foo')}));
24+
expectType<ProcessEnv>(npmRunPathEnv({cwd: fileUrl}));
25+
expectError(npmRunPathEnv({cwd: false}));
1526
expectType<ProcessEnv>(npmRunPathEnv({env: process.env})); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
27+
expectType<ProcessEnv>(npmRunPathEnv({env: {foo: 'bar'}}));
28+
expectType<ProcessEnv>(npmRunPathEnv({env: {foo: undefined}}));
29+
expectError(npmRunPath({env: false}));
30+
expectError(npmRunPath({env: {[Symbol('key')]: 'bar'}}));
31+
expectError(npmRunPath({env: {foo: false}}));
1632
expectType<ProcessEnv>(npmRunPathEnv({execPath: '/usr/local/bin'}));
17-
expectType<ProcessEnv>(npmRunPathEnv({execPath: new URL('file:///usr/local/bin')}));
33+
expectType<ProcessEnv>(npmRunPathEnv({execPath: fileUrl}));
34+
expectError(npmRunPath({execPath: false}));
35+
expectType<ProcessEnv>(npmRunPathEnv({addExecPath: false}));
36+
expectError(npmRunPathEnv({addExecPath: ''}));
37+
expectType<ProcessEnv>(npmRunPathEnv({preferLocal: false}));
38+
expectError(npmRunPathEnv({preferLocal: ''}));

readme.md

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,66 +32,71 @@ childProcess.execFileSync('foo', {
3232

3333
### npmRunPath(options?)
3434

35+
`options`: [`Options`](#options)\
36+
_Returns_: `string`
37+
3538
Returns the augmented PATH string.
3639

37-
#### options
40+
### npmRunPathEnv(options?)
41+
42+
`options`: [`Options`](#options)\
43+
_Returns_: `object`
44+
45+
Returns the augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object.
46+
47+
### options
3848

3949
Type: `object`
4050

41-
##### cwd
51+
#### cwd
4252

4353
Type: `string | URL`\
4454
Default: `process.cwd()`
4555

4656
The working directory.
4757

48-
##### path
49-
50-
Type: `string`\
51-
Default: [`PATH`](https://github.com/sindresorhus/path-key)
52-
53-
The PATH to be appended.
54-
55-
Set it to an empty string to exclude the default PATH.
56-
57-
##### execPath
58+
#### execPath
5859

5960
Type: `string | URL`\
60-
Default: `process.execPath`
61+
Default: [`process.execPath`](https://nodejs.org/api/process.html#processexecpath)
6162

62-
The path to the current Node.js executable. Its directory is pushed to the front of PATH.
63+
The path to the current Node.js executable.
6364

6465
This can be either an absolute path or a path relative to the [`cwd` option](#cwd).
6566

66-
### npmRunPathEnv(options?)
67+
#### addExecPath
6768

68-
Returns the augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object.
69+
Type: `boolean`\
70+
Default: `true`
6971

70-
#### options
72+
Whether to push the current Node.js executable's directory ([`execPath`](#execpath) option) to the front of PATH.
7173

72-
Type: `object`
74+
#### preferLocal
7375

74-
##### cwd
76+
Type: `boolean`\
77+
Default: `true`
7578

76-
Type: `string | URL`\
77-
Default: `process.cwd()`
79+
Whether to push the locally installed binaries' directory to the front of PATH.
7880

79-
The working directory.
81+
#### path
8082

81-
##### env
83+
Type: `string`\
84+
Default: [`PATH`](https://github.com/sindresorhus/path-key)
8285

83-
Type: `object`
86+
The PATH to be appended.
8487

85-
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.
88+
Set it to an empty string to exclude the default PATH.
8689

87-
##### execPath
90+
Only available with [`npmRunPath()`](#npmrunpathoptions), not [`npmRunPathEnv()`](#npmrunpathenvoptions).
8891

89-
Type: `string`\
90-
Default: `process.execPath`
92+
#### env
9193

92-
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.
94+
Type: `object`\
95+
Default: [`process.env`](https://nodejs.org/api/process.html#processenv)
9396

94-
This can be either an absolute path or a path relative to the [`cwd` option](#cwd).
97+
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.
98+
99+
Only available with [`npmRunPathEnv()`](#npmrunpathenvoptions), not [`npmRunPath()`](#npmrunpathoptions).
95100

96101
## Related
97102

test.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,29 @@ import {npmRunPath, npmRunPathEnv} from './index.js';
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88

9-
test('main', t => {
9+
const testLocalDir = (t, addExecPath, preferLocal, expectedResult) => {
1010
t.is(
11-
npmRunPath({path: ''}).split(path.delimiter)[0],
12-
path.join(__dirname, 'node_modules/.bin'),
11+
npmRunPath({path: '', addExecPath, preferLocal}).split(path.delimiter)[0] === path.join(__dirname, 'node_modules/.bin'),
12+
expectedResult,
1313
);
14+
};
1415

16+
test('Adds node_modules/.bin - npmRunPath()', testLocalDir, undefined, undefined, true);
17+
test('"addExecPath: false" still adds node_modules/.bin - npmRunPath()', testLocalDir, false, undefined, true);
18+
test('"preferLocal: false" does not add node_modules/.bin - npmRunPath()', testLocalDir, undefined, false, false);
19+
test('"preferLocal: false", "addExecPath: false" does not add node_modules/.bin - npmRunPath()', testLocalDir, false, false, false);
20+
21+
const testLocalDirEnv = (t, addExecPath, preferLocal, expectedResult) => {
1522
t.is(
16-
npmRunPathEnv({env: {PATH: 'foo'}}).PATH.split(path.delimiter)[0],
17-
path.join(__dirname, 'node_modules/.bin'),
23+
npmRunPathEnv({env: {PATH: 'foo'}, addExecPath, preferLocal}).PATH.split(path.delimiter)[0] === path.join(__dirname, 'node_modules/.bin'),
24+
expectedResult,
1825
);
19-
});
26+
};
27+
28+
test('Adds node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, undefined, undefined, true);
29+
test('"addExecPath: false" still adds node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, false, undefined, true);
30+
test('"preferLocal: false" does not add node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, undefined, false, false);
31+
test('"preferLocal: false", "addExecPath: false" does not add node_modules/.bin - npmRunPathEnv()', testLocalDirEnv, false, false, false);
2032

2133
test('the `cwd` option changes the current directory', t => {
2234
t.is(
@@ -37,18 +49,29 @@ test('push `execPath` later in the PATH', t => {
3749
t.is(pathEnv[pathEnv.length - 2], path.dirname(process.execPath));
3850
});
3951

40-
test('can change `execPath` with the `execPath` option', t => {
41-
const pathEnv = npmRunPath({path: '', execPath: 'test/test'}).split(
42-
path.delimiter,
43-
);
44-
t.is(pathEnv[pathEnv.length - 2], path.resolve(process.cwd(), 'test'));
45-
});
52+
const testExecPath = (t, preferLocal, addExecPath, expectedResult) => {
53+
const pathEnv = npmRunPath({path: '', execPath: 'test/test', preferLocal, addExecPath}).split(path.delimiter);
54+
t.is(pathEnv[pathEnv.length - 2] === path.resolve('test'), expectedResult);
55+
};
56+
57+
test('can change `execPath` with the `execPath` option - npmRunPath()', testExecPath, undefined, undefined, true);
58+
test('"preferLocal: false" still adds execPath - npmRunPath()', testExecPath, false, undefined, true);
59+
test('"addExecPath: false" does not add execPath - npmRunPath()', testExecPath, undefined, false, false);
60+
test('"addExecPath: false", "preferLocal: false" does not add execPath - npmRunPath()', testExecPath, false, false, false);
61+
62+
const testExecPathEnv = (t, preferLocal, addExecPath, expectedResult) => {
63+
const pathEnv = npmRunPathEnv({env: {PATH: 'foo'}, execPath: 'test/test', preferLocal, addExecPath}).PATH.split(path.delimiter);
64+
t.is(pathEnv[pathEnv.length - 2] === path.resolve('test'), expectedResult);
65+
};
66+
67+
test('can change `execPath` with the `execPath` option - npmRunPathEnv()', testExecPathEnv, undefined, undefined, true);
68+
test('"preferLocal: false" still adds execPath - npmRunPathEnv()', testExecPathEnv, false, undefined, true);
69+
test('"addExecPath: false" does not add execPath - npmRunPathEnv()', testExecPathEnv, undefined, false, false);
70+
test('"addExecPath: false", "preferLocal: false" does not add execPath - npmRunPathEnv()', testExecPathEnv, false, false, false);
4671

4772
test('the `execPath` option can be a file URL', t => {
48-
const pathEnv = npmRunPath({path: '', execPath: pathToFileURL('test/test')}).split(
49-
path.delimiter,
50-
);
51-
t.is(pathEnv[pathEnv.length - 2], path.resolve(process.cwd(), 'test'));
73+
const pathEnv = npmRunPath({path: '', execPath: pathToFileURL('test/test')}).split(path.delimiter);
74+
t.is(pathEnv[pathEnv.length - 2], path.resolve('test'));
5275
});
5376

5477
test('the `execPath` option is relative to the `cwd` option', t => {

0 commit comments

Comments
 (0)