Skip to content

Commit 19a8346

Browse files
authored
fix: properly find and run global scoped packages (#5250)
1 parent 8233fca commit 19a8346

File tree

5 files changed

+84
-22
lines changed

5 files changed

+84
-22
lines changed

lib/commands/exec.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const path = require('path')
12
const libexec = require('libnpmexec')
23
const BaseCommand = require('../base-command.js')
34

@@ -24,7 +25,7 @@ class Exec extends BaseCommand {
2425

2526
async exec (_args, { locationMsg, runPath } = {}) {
2627
// This is where libnpmexec will look for locally installed packages
27-
const path = this.npm.localPrefix
28+
const localPrefix = this.npm.localPrefix
2829

2930
// This is where libnpmexec will actually run the scripts from
3031
if (!runPath) {
@@ -37,6 +38,7 @@ class Exec extends BaseCommand {
3738
flatOptions,
3839
localBin,
3940
globalBin,
41+
globalDir,
4042
} = this.npm
4143
const output = this.npm.output.bind(this.npm)
4244
const scriptShell = this.npm.config.get('script-shell') || undefined
@@ -57,9 +59,10 @@ class Exec extends BaseCommand {
5759
localBin,
5860
locationMsg,
5961
globalBin,
62+
globalPath: path.resolve(globalDir, '..'),
6063
output,
6164
packages,
62-
path,
65+
path: localPrefix,
6366
runPath,
6467
scriptShell,
6568
yes,

test/fixtures/mock-npm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const LoadMockNpm = async (t, {
6363
prefixDir = {},
6464
homeDir = {},
6565
cacheDir = {},
66-
globalPrefixDir = {},
66+
globalPrefixDir = { lib: {} },
6767
config = {},
6868
mocks = {},
6969
globals = null,

test/lib/commands/exec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ t.test('registry package', async t => {
4242
}),
4343
})
4444

45-
await registry.package({ manifest,
45+
await registry.package({
46+
times: 2,
47+
manifest,
4648
tarballs: {
4749
'1.0.0': path.join(npm.prefix, 'npm-exec-test'),
4850
} })

workspaces/libnpmexec/lib/index.js

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@ const binPaths = []
2727
// spec.raw so we don't have to fetch again when we check npxCache
2828
const manifests = new Map()
2929

30+
const getManifest = async (spec, flatOptions) => {
31+
if (!manifests.get(spec.raw)) {
32+
const manifest = await pacote.manifest(spec, { ...flatOptions, preferOnline: true })
33+
manifests.set(spec.raw, manifest)
34+
}
35+
return manifests.get(spec.raw)
36+
}
37+
3038
// Returns the required manifest if the spec is missing from the tree
31-
const missingFromTree = async ({ spec, tree, pacoteOpts }) => {
39+
const missingFromTree = async ({ spec, tree, flatOptions }) => {
3240
if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) {
3341
// registry spec that is not a specific tag.
3442
const nodesBySpec = tree.inventory.query('packageName', spec.name)
@@ -48,17 +56,11 @@ const missingFromTree = async ({ spec, tree, pacoteOpts }) => {
4856
}
4957
}
5058
}
51-
if (!manifests.get(spec.raw)) {
52-
manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts))
53-
}
54-
return manifests.get(spec.raw)
59+
return await getManifest(spec, flatOptions)
5560
} else {
5661
// non-registry spec, or a specific tag. Look up manifest and check
5762
// resolved to see if it's in the tree.
58-
if (!manifests.get(spec.raw)) {
59-
manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts))
60-
}
61-
const manifest = manifests.get(spec.raw)
63+
const manifest = await getManifest(spec, flatOptions)
6264
const nodesByManifest = tree.inventory.query('packageName', manifest.name)
6365
for (const node of nodesByManifest) {
6466
if (node.package.resolved === manifest._resolved) {
@@ -78,6 +80,7 @@ const exec = async (opts) => {
7880
localBin = resolve('./node_modules/.bin'),
7981
locationMsg = undefined,
8082
globalBin = '',
83+
globalPath = '',
8184
output,
8285
// dereference values because we manipulate it later
8386
packages: [...packages] = [],
@@ -106,9 +109,9 @@ const exec = async (opts) => {
106109
return run()
107110
}
108111

109-
const pacoteOpts = { ...flatOptions, perferOnline: true }
110-
111112
const needPackageCommandSwap = (args.length > 0) && (packages.length === 0)
113+
// If they asked for a command w/o specifying a package, see if there is a
114+
// bin that directly matches that name either globally or in the local tree.
112115
if (needPackageCommandSwap) {
113116
const dir = dirname(dirname(localBin))
114117
const localBinPath = await localFileExists(dir, args[0], '/')
@@ -131,25 +134,34 @@ const exec = async (opts) => {
131134
const needInstall = []
132135
await Promise.all(packages.map(async pkg => {
133136
const spec = npa(pkg, path)
134-
const manifest = await missingFromTree({ spec, tree: localTree, pacoteOpts })
137+
const manifest = await missingFromTree({ spec, tree: localTree, flatOptions })
135138
if (manifest) {
139+
// Package does not exist in the local tree
136140
needInstall.push({ spec, manifest })
137141
}
138142
}))
139143

140144
if (needPackageCommandSwap) {
141145
// Either we have a scoped package or the bin of our package we inferred
142-
// from arg[0] is not identical to the package name
146+
// from arg[0] might not be identical to the package name
147+
const spec = npa(args[0])
143148
let commandManifest
144149
if (needInstall.length === 0) {
145-
commandManifest = await pacote.manifest(args[0], {
146-
...flatOptions,
147-
preferOnline: true,
148-
})
150+
commandManifest = await getManifest(spec, flatOptions)
149151
} else {
150152
commandManifest = needInstall[0].manifest
151153
}
154+
152155
args[0] = getBinFromManifest(commandManifest)
156+
157+
// See if the package is installed globally, and run the translated bin
158+
const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
159+
const globalTree = await globalArb.loadActual()
160+
const globalManifest = await missingFromTree({ spec, tree: globalTree, flatOptions })
161+
if (!globalManifest) {
162+
binPaths.push(globalBin)
163+
return await run()
164+
}
153165
}
154166

155167
const add = []
@@ -171,7 +183,7 @@ const exec = async (opts) => {
171183
})
172184
const npxTree = await npxArb.loadActual()
173185
await Promise.all(needInstall.map(async ({ spec }) => {
174-
const manifest = await missingFromTree({ spec, tree: npxTree, pacoteOpts })
186+
const manifest = await missingFromTree({ spec, tree: npxTree, flatOptions })
175187
if (manifest) {
176188
// Manifest is not in npxCache, we need to install it there
177189
if (!spec.registry) {

workspaces/libnpmexec/test/index.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,51 @@ t.test('global space pkg', async t => {
485485
t.equal(res, 'GLOBAL PKG', 'should run local pkg bin script')
486486
})
487487

488+
t.test('global scoped pkg', async t => {
489+
const pkg = {
490+
name: '@ruyadorno/create-test',
491+
bin: {
492+
'create-test': 'index.js',
493+
},
494+
}
495+
const path = t.testdir({
496+
cache: {},
497+
npxCache: {},
498+
global: {
499+
node_modules: {
500+
'.bin': {},
501+
'@ruyadorno': {
502+
'create-test': {
503+
'index.js': `#!/usr/bin/env node
504+
require('fs').writeFileSync(process.argv.slice(2)[0], 'GLOBAL PKG')`,
505+
'package.json': JSON.stringify(pkg),
506+
},
507+
},
508+
},
509+
},
510+
})
511+
const globalBin = resolve(path, 'global/node_modules/.bin')
512+
const globalPath = resolve(path, 'global')
513+
const runPath = path
514+
515+
await binLinks({
516+
path: resolve(path, 'global/node_modules/@ruyadorno/create-test'),
517+
pkg,
518+
})
519+
520+
await libexec({
521+
...baseOpts,
522+
args: ['@ruyadorno/create-test', 'resfile'],
523+
globalBin,
524+
globalPath,
525+
path,
526+
runPath,
527+
})
528+
529+
const res = fs.readFileSync(resolve(path, 'resfile')).toString()
530+
t.equal(res, 'GLOBAL PKG', 'should run global pkg bin script')
531+
})
532+
488533
t.test('run from registry - no local packages', async t => {
489534
const testdir = t.testdir({
490535
cache: {},

0 commit comments

Comments
 (0)