Skip to content

Commit daaf461

Browse files
authored
fix: ignore global prefix if --prefix is used (#5291)
When `--prefix` is used, both the local and global prefix values are set to be identical. This is functionally broken because their directory structures are inherently different (for instance, in posix the tree is in `lib/node_modules` in the global prefix). This commit makes npm exec ignore the global folders if it detects both local and global prefix are identical.
1 parent a4808fb commit daaf461

File tree

4 files changed

+63
-11
lines changed

4 files changed

+63
-11
lines changed

lib/commands/exec.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class Exec extends BaseCommand {
3434

3535
const args = [..._args]
3636
const call = this.npm.config.get('call')
37+
let globalPath
3738
const {
3839
flatOptions,
3940
localBin,
@@ -44,6 +45,12 @@ class Exec extends BaseCommand {
4445
const scriptShell = this.npm.config.get('script-shell') || undefined
4546
const packages = this.npm.config.get('package')
4647
const yes = this.npm.config.get('yes')
48+
// --prefix sets both of these to the same thing, meaning the global prefix
49+
// is invalid (i.e. no lib/node_modules). This is not a trivial thing to
50+
// untangle and fix so we work around it here.
51+
if (this.npm.localPrefix !== this.npm.globalPrefix) {
52+
globalPath = path.resolve(globalDir, '..')
53+
}
4754

4855
if (call && _args.length) {
4956
throw this.usageError()
@@ -59,7 +66,7 @@ class Exec extends BaseCommand {
5966
localBin,
6067
locationMsg,
6168
globalBin,
62-
globalPath: path.resolve(globalDir, '..'),
69+
globalPath,
6370
output,
6471
packages,
6572
path: localPrefix,

test/lib/commands/exec.js

+42-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ t.test('registry package', async t => {
2828

2929
const { npm } = await loadMockNpm(t, {
3030
config: {
31+
audit: false,
3132
yes: true,
3233
},
3334
prefixDir: {
@@ -43,7 +44,46 @@ t.test('registry package', async t => {
4344
})
4445

4546
await registry.package({
46-
times: 2,
47+
manifest,
48+
tarballs: {
49+
'1.0.0': path.join(npm.prefix, 'npm-exec-test'),
50+
} })
51+
52+
await npm.exec('exec', ['@npmcli/npx-test'])
53+
const exists = await fs.stat(path.join(npm.prefix, 'npm-exec-test-success'))
54+
t.ok(exists.isFile(), 'bin ran, creating file')
55+
})
56+
57+
t.test('--prefix', async t => {
58+
const registry = new MockRegistry({
59+
tap: t,
60+
registry: 'https://registry.npmjs.org/',
61+
})
62+
63+
const manifest = registry.manifest({ name: '@npmcli/npx-test' })
64+
manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' }
65+
66+
const { npm } = await loadMockNpm(t, {
67+
config: {
68+
audit: false,
69+
yes: true,
70+
},
71+
prefixDir: {
72+
'npm-exec-test': {
73+
'package.json': JSON.stringify(manifest),
74+
'index.js': `#!/usr/bin/env node
75+
require('fs').writeFileSync('npm-exec-test-success', '')`,
76+
},
77+
},
78+
globals: ({ prefix }) => ({
79+
'process.cwd': () => prefix,
80+
}),
81+
})
82+
83+
// This is what `--prefix` does
84+
npm.globalPrefix = npm.localPrefix
85+
86+
await registry.package({
4787
manifest,
4888
tarballs: {
4989
'1.0.0': path.join(npm.prefix, 'npm-exec-test'),
@@ -65,6 +105,7 @@ t.test('workspaces', async t => {
65105

66106
const { npm } = await loadMockNpm(t, {
67107
config: {
108+
audit: false,
68109
yes: true,
69110
workspace: ['workspace-a'],
70111
},

workspaces/libnpmexec/lib/index.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const exec = async (opts) => {
8282
localBin = resolve('./node_modules/.bin'),
8383
locationMsg = undefined,
8484
globalBin = '',
85-
globalPath = '',
85+
globalPath,
8686
output,
8787
// dereference values because we manipulate it later
8888
packages: [...packages] = [],
@@ -120,7 +120,7 @@ const exec = async (opts) => {
120120
if (localBinPath) {
121121
binPaths.push(localBinPath)
122122
return await run()
123-
} else if (await fileExists(`${globalBin}/${args[0]}`)) {
123+
} else if (globalPath && await fileExists(`${globalBin}/${args[0]}`)) {
124124
binPaths.push(globalBin)
125125
return await run()
126126
}
@@ -155,7 +155,7 @@ const exec = async (opts) => {
155155

156156
args[0] = getBinFromManifest(commandManifest)
157157

158-
if (needInstall.length > 0) {
158+
if (needInstall.length > 0 && globalPath) {
159159
// See if the package is installed globally, and run the translated bin
160160
const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
161161
const globalTree = await globalArb.loadActual()

workspaces/libnpmexec/test/index.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,7 @@ t.test('global space pkg', async t => {
517517
},
518518
})
519519
const globalBin = resolve(path, 'global/node_modules/.bin')
520+
const globalPath = resolve(path, 'global')
520521
const runPath = path
521522

522523
const executable = resolve(path, 'global/node_modules/a')
@@ -531,6 +532,7 @@ t.test('global space pkg', async t => {
531532
...baseOpts,
532533
args: ['a', 'resfile'],
533534
globalBin,
535+
globalPath,
534536
path,
535537
runPath,
536538
})
@@ -588,12 +590,13 @@ t.test('run from registry - no local packages', async t => {
588590
const testdir = t.testdir({
589591
cache: {},
590592
npxCache: {},
593+
global: {
594+
lib: {},
595+
bin: {},
596+
},
591597
work: {},
592598
})
593599
const path = resolve(testdir, 'work')
594-
const runPath = path
595-
const cache = resolve(testdir, 'cache')
596-
const npxCache = resolve(testdir, 'npxCache')
597600

598601
t.throws(
599602
() => fs.statSync(resolve(path, 'index.js')),
@@ -604,10 +607,11 @@ t.test('run from registry - no local packages', async t => {
604607
await libexec({
605608
...baseOpts,
606609
args: ['@ruyadorno/create-index'],
607-
cache,
608-
npxCache,
610+
cache: resolve(testdir, 'cache'),
611+
globalPath: resolve(testdir, 'global'),
612+
npxCache: resolve(testdir, 'npxCache'),
609613
path,
610-
runPath,
614+
runPath: path,
611615
})
612616

613617
t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg')

0 commit comments

Comments
 (0)