Skip to content

Commit 6b55646

Browse files
authored
fix(exec): look in workspace and root for bin entries (#7569)
Closes: #7379
1 parent 4a36d78 commit 6b55646

File tree

3 files changed

+46
-25
lines changed

3 files changed

+46
-25
lines changed

lib/commands/exec.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,8 @@ class Exec extends BaseCommand {
3939
}
4040

4141
async callExec (args, { name, locationMsg, runPath } = {}) {
42-
// This is where libnpmexec will look for locally installed packages at the project level
43-
const localPrefix = this.npm.localPrefix
44-
// This is where libnpmexec will look for locally installed packages at the workspace level
4542
let localBin = this.npm.localBin
46-
let path = localPrefix
43+
let pkgPath = this.npm.localPrefix
4744

4845
// This is where libnpmexec will actually run the scripts from
4946
if (!runPath) {
@@ -54,7 +51,7 @@ class Exec extends BaseCommand {
5451
localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin')
5552
// We also need to look for `bin` entries in the workspace package.json
5653
// libnpmexec will NOT look in the project root for the bin entry
57-
path = runPath
54+
pkgPath = runPath
5855
}
5956

6057
const call = this.npm.config.get('call')
@@ -84,16 +81,25 @@ class Exec extends BaseCommand {
8481
// we explicitly set packageLockOnly to false because if it's true
8582
// when we try to install a missing package, we won't actually install it
8683
packageLockOnly: false,
87-
// copy args so they dont get mutated
88-
args: [...args],
84+
// what the user asked to run args[0] is run by default
85+
args: [...args], // copy args so they dont get mutated
86+
// specify a custom command to be run instead of args[0]
8987
call,
9088
chalk,
89+
// where to look for bins globally, if a file matches call or args[0] it is called
9190
globalBin,
91+
// where to look for packages globally, if a package matches call or args[0] it is called
9292
globalPath,
93+
// where to look for bins locally, if a file matches call or args[0] it is called
9394
localBin,
9495
locationMsg,
96+
// packages that need to be installed
9597
packages,
96-
path,
98+
// path where node_modules is
99+
path: this.npm.localPrefix,
100+
// where to look for package.json#bin entries first
101+
pkgPath,
102+
// cwd to run from
97103
runPath,
98104
scriptShell,
99105
yes,

workspaces/libnpmexec/lib/index.js

+31-16
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ const missingFromTree = async ({ spec, tree, flatOptions, isNpxTree }) => {
7373
}
7474
}
7575

76+
// see if the package.json at `path` has an entry that matches `cmd`
77+
const hasPkgBin = (path, cmd, flatOptions) =>
78+
pacote.manifest(path, flatOptions)
79+
.then(manifest => manifest?.bin?.[cmd]).catch(() => null)
80+
7681
const exec = async (opts) => {
7782
const {
7883
args = [],
@@ -89,6 +94,13 @@ const exec = async (opts) => {
8994
...flatOptions
9095
} = opts
9196

97+
let pkgPaths = opts.pkgPath
98+
if (typeof pkgPaths === 'string') {
99+
pkgPaths = [pkgPaths]
100+
}
101+
if (!pkgPaths) {
102+
pkgPaths = ['.']
103+
}
92104
let yes = opts.yes
93105
const run = () => runScript({
94106
args,
@@ -106,28 +118,31 @@ const exec = async (opts) => {
106118
return run()
107119
}
108120

121+
// Look in the local tree too
122+
pkgPaths.push(path)
123+
109124
let needPackageCommandSwap = (args.length > 0) && (packages.length === 0)
110125
// If they asked for a command w/o specifying a package, see if there is a
111126
// bin that directly matches that name:
112-
// - in the local package itself
113-
// - in the local tree
127+
// - in any local packages (pkgPaths can have workspaces in them or just the root)
128+
// - in the local tree (path)
114129
// - globally
115130
if (needPackageCommandSwap) {
116-
let localManifest
117-
try {
118-
localManifest = await pacote.manifest(path, flatOptions)
119-
} catch {
120-
// no local package.json? no problem, move one.
131+
// Local packages and local tree
132+
for (const p of pkgPaths) {
133+
if (await hasPkgBin(p, args[0], flatOptions)) {
134+
// we have to install the local package into the npx cache so that its
135+
// bin links get set up
136+
flatOptions.installLinks = false
137+
// args[0] will exist when the package is installed
138+
packages.push(p)
139+
yes = true
140+
needPackageCommandSwap = false
141+
break
142+
}
121143
}
122-
if (localManifest?.bin?.[args[0]]) {
123-
// we have to install the local package into the npx cache so that its
124-
// bin links get set up
125-
flatOptions.installLinks = false
126-
// args[0] will exist when the package is installed
127-
packages.push(path)
128-
yes = true
129-
needPackageCommandSwap = false
130-
} else {
144+
if (needPackageCommandSwap) {
145+
// no bin entry in local packages or in tree, now we look for binPaths
131146
const dir = dirname(dirname(localBin))
132147
const localBinPath = await localFileExists(dir, args[0], '/')
133148
if (localBinPath) {

workspaces/libnpmexec/test/local.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ t.test('bin in local pkg', async t => {
8484
await binLinks(existingPkg.pkg)
8585

8686
t.match(await fs.readdir(resolve(path, 'node_modules', '.bin')), ['conflicting-bin'])
87-
await exec({ localBin, args: ['conflicting-bin'] })
87+
await exec({ pkgPath: path, localBin, args: ['conflicting-bin'] })
8888
// local bin was called for conflicting-bin
8989
t.match(await readOutput('conflicting-bin'), {
9090
value: 'LOCAL PKG',

0 commit comments

Comments
 (0)