Skip to content

Commit 88ecda1

Browse files
committed
fix(exec): look in workspace and root for bin entries
1 parent 9122fb6 commit 88ecda1

File tree

3 files changed

+47
-24
lines changed

3 files changed

+47
-24
lines changed

lib/commands/exec.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,9 @@ class Exec extends BaseCommand {
4040

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

4847
// This is where libnpmexec will actually run the scripts from
4948
if (!runPath) {
@@ -54,7 +53,7 @@ class Exec extends BaseCommand {
5453
localBin = resolve(this.npm.localDir, name, 'node_modules', '.bin')
5554
// We also need to look for `bin` entries in the workspace package.json
5655
// libnpmexec will NOT look in the project root for the bin entry
57-
path = runPath
56+
pkgPath = runPath
5857
}
5958

6059
const call = this.npm.config.get('call')
@@ -84,16 +83,25 @@ class Exec extends BaseCommand {
8483
// we explicitly set packageLockOnly to false because if it's true
8584
// when we try to install a missing package, we won't actually install it
8685
packageLockOnly: false,
87-
// copy args so they dont get mutated
88-
args: [...args],
86+
// what the user asked to run args[0] is run by default
87+
args: [...args], // copy args so they dont get mutated
88+
// specify a custom command to be run instead of args[0]
8989
call,
9090
chalk,
91+
// where to look for bins globally, if a file matches call or args[0] it is called
9192
globalBin,
93+
// where to look for packages globally, if a package matches call or args[0] it is called
9294
globalPath,
95+
// where to look for bins locally, if a file matches call or args[0] it is called
9396
localBin,
9497
locationMsg,
98+
// packages that need to be installed
9599
packages,
96-
path,
100+
// path where node_modules is
101+
path: this.npm.localPrefix,
102+
// where to look for package.json#bin entries first
103+
pkgPath,
104+
// cwd to run from
97105
runPath,
98106
scriptShell,
99107
yes,

workspaces/libnpmexec/lib/index.js

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const getBinFromManifest = require('./get-bin-from-manifest.js')
1414
const noTTY = require('./no-tty.js')
1515
const runScript = require('./run-script.js')
1616
const isWindows = require('./is-windows.js')
17-
const { dirname, resolve } = require('path')
17+
const { dirname, resolve } = require('node:path')
1818

1919
const binPaths = []
2020

@@ -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

Lines changed: 1 addition & 1 deletion
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)