Skip to content

Commit c992fd6

Browse files
authored
fix: look up local command bins from local tree (#5273)
1 parent 9078e27 commit c992fd6

File tree

2 files changed

+83
-25
lines changed

2 files changed

+83
-25
lines changed

workspaces/libnpmexec/lib/index.js

+29-25
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,37 @@ const binPaths = []
2828
const manifests = new Map()
2929

3030
const getManifest = async (spec, flatOptions) => {
31-
if (!manifests.get(spec.raw)) {
31+
if (!manifests.has(spec.raw)) {
3232
const manifest = await pacote.manifest(spec, { ...flatOptions, preferOnline: true })
3333
manifests.set(spec.raw, manifest)
3434
}
3535
return manifests.get(spec.raw)
3636
}
3737

3838
// Returns the required manifest if the spec is missing from the tree
39+
// Returns the found node if it is in the tree
3940
const missingFromTree = async ({ spec, tree, flatOptions }) => {
4041
if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) {
4142
// registry spec that is not a specific tag.
4243
const nodesBySpec = tree.inventory.query('packageName', spec.name)
4344
for (const node of nodesBySpec) {
4445
if (spec.type === 'tag') {
4546
// package requested by name only
46-
return
47+
return { node }
4748
} else if (spec.type === 'version') {
4849
// package requested by specific version
4950
if (node.pkgid === spec.raw) {
50-
return
51+
return { node }
5152
}
5253
} else {
5354
// package requested by version range, only remaining registry type
5455
if (semver.satisfies(node.package.version, spec.rawSpec)) {
55-
return
56+
return { node }
5657
}
5758
}
5859
}
59-
return await getManifest(spec, flatOptions)
60+
const manifest = await getManifest(spec, flatOptions)
61+
return { manifest }
6062
} else {
6163
// non-registry spec, or a specific tag. Look up manifest and check
6264
// resolved to see if it's in the tree.
@@ -65,10 +67,10 @@ const missingFromTree = async ({ spec, tree, flatOptions }) => {
6567
for (const node of nodesByManifest) {
6668
if (node.package.resolved === manifest._resolved) {
6769
// we have a package by the same name and the same resolved destination, nothing to add.
68-
return
70+
return { node }
6971
}
7072
}
71-
return manifest
73+
return { manifest }
7274
}
7375
}
7476

@@ -132,35 +134,37 @@ const exec = async (opts) => {
132134

133135
// Find anything that isn't installed locally
134136
const needInstall = []
135-
await Promise.all(packages.map(async pkg => {
137+
let commandManifest
138+
await Promise.all(packages.map(async (pkg, i) => {
136139
const spec = npa(pkg, path)
137-
const manifest = await missingFromTree({ spec, tree: localTree, flatOptions })
140+
const { manifest, node } = await missingFromTree({ spec, tree: localTree, flatOptions })
138141
if (manifest) {
139142
// Package does not exist in the local tree
140143
needInstall.push({ spec, manifest })
144+
if (i === 0) {
145+
commandManifest = manifest
146+
}
147+
} else if (i === 0) {
148+
// The node.package has enough to look up the bin
149+
commandManifest = node.package
141150
}
142151
}))
143152

144153
if (needPackageCommandSwap) {
145-
// Either we have a scoped package or the bin of our package we inferred
146-
// from arg[0] might not be identical to the package name
147154
const spec = npa(args[0])
148-
let commandManifest
149-
if (needInstall.length === 0) {
150-
commandManifest = await getManifest(spec, flatOptions)
151-
} else {
152-
commandManifest = needInstall[0].manifest
153-
}
154155

155156
args[0] = getBinFromManifest(commandManifest)
156157

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()
158+
if (needInstall.length > 0) {
159+
// See if the package is installed globally, and run the translated bin
160+
const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true })
161+
const globalTree = await globalArb.loadActual()
162+
const { manifest: globalManifest } =
163+
await missingFromTree({ spec, tree: globalTree, flatOptions })
164+
if (!globalManifest) {
165+
binPaths.push(globalBin)
166+
return await run()
167+
}
164168
}
165169
}
166170

@@ -183,7 +187,7 @@ const exec = async (opts) => {
183187
})
184188
const npxTree = await npxArb.loadActual()
185189
await Promise.all(needInstall.map(async ({ spec }) => {
186-
const manifest = await missingFromTree({ spec, tree: npxTree, flatOptions })
190+
const { manifest } = await missingFromTree({ spec, tree: npxTree, flatOptions })
187191
if (manifest) {
188192
// Manifest is not in npxCache, we need to install it there
189193
if (!spec.registry) {

workspaces/libnpmexec/test/index.js

+54
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,60 @@ require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`,
6666
t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
6767
})
6868

69+
t.test('locally available pkg - by scoped name only', async t => {
70+
const pkg = {
71+
name: '@npmcli/npx-local-test',
72+
version: '2.0.0',
73+
bin: {
74+
'npx-local-test': './index.js',
75+
},
76+
}
77+
const path = t.testdir({
78+
cache: {},
79+
npxCache: {},
80+
node_modules: {
81+
'.bin': {},
82+
'@npmcli': {
83+
'npx-local-test': {
84+
'package.json': JSON.stringify(pkg),
85+
'index.js': `#!/usr/bin/env node
86+
require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`,
87+
},
88+
},
89+
},
90+
'package.json': JSON.stringify({
91+
name: 'pkg',
92+
dependencies: {
93+
'@npmcli/npx-local-test': '^2.0.0',
94+
},
95+
}),
96+
})
97+
const runPath = path
98+
const cache = resolve(path, 'cache')
99+
const npxCache = resolve(path, 'npxCache')
100+
101+
const executable =
102+
resolve(path, 'node_modules/@npmcli/npx-local-test/index.js')
103+
fs.chmodSync(executable, 0o775)
104+
105+
await binLinks({
106+
path: resolve(path, 'node_modules/@npmcli/npx-local-test'),
107+
pkg,
108+
})
109+
110+
await libexec({
111+
...baseOpts,
112+
cache,
113+
npxCache,
114+
args: ['@npmcli/npx-local-test', 'resfile'],
115+
path,
116+
runPath,
117+
})
118+
119+
const res = fs.readFileSync(resolve(path, 'resfile')).toString()
120+
t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
121+
})
122+
69123
t.test('locally available pkg - by name', async t => {
70124
const pkg = {
71125
name: '@ruyadorno/create-index',

0 commit comments

Comments
 (0)