node/deps/npm/node_modules/libnpmexec/lib/index.js
npm CLI robot 47ea59fad1
deps: upgrade npm to 8.12.1
PR-URL: https://github.com/nodejs/node/pull/43301
Reviewed-By: Ruy Adorno <ruyadorno@github.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
Reviewed-By: Mestery <mestery@protonmail.com>
2022-06-14 07:04:21 +01:00

211 lines
6.3 KiB
JavaScript

const { delimiter, dirname, resolve } = require('path')
const { promisify } = require('util')
const read = promisify(require('read'))
const Arborist = require('@npmcli/arborist')
const ciDetect = require('@npmcli/ci-detect')
const log = require('proc-log')
const npmlog = require('npmlog')
const mkdirp = require('mkdirp-infer-owner')
const npa = require('npm-package-arg')
const pacote = require('pacote')
const cacheInstallDir = require('./cache-install-dir.js')
const { fileExists, localFileExists } = require('./file-exists.js')
const getBinFromManifest = require('./get-bin-from-manifest.js')
const noTTY = require('./no-tty.js')
const runScript = require('./run-script.js')
const isWindows = require('./is-windows.js')
const _localManifest = Symbol('localManifest')
/* istanbul ignore next */
const PATH = (
process.env.PATH || process.env.Path || process.env.path
).split(delimiter)
const exec = async (opts) => {
const {
args = [],
call = '',
color = false,
localBin = resolve('./node_modules/.bin'),
locationMsg = undefined,
globalBin = '',
output,
packages: _packages = [],
path = '.',
runPath = '.',
scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh',
yes = undefined,
...flatOptions
} = opts
// dereferences values because we manipulate it later
const packages = [..._packages]
const pathArr = [...PATH]
const _run = () => runScript({
args,
call,
color,
flatOptions,
locationMsg,
output,
path,
pathArr,
runPath,
scriptShell,
})
// nothing to maybe install, skip the arborist dance
if (!call && !args.length && !packages.length) {
return await _run()
}
const needPackageCommandSwap = args.length && !packages.length
// if there's an argument and no package has been explicitly asked for
// check the local and global bin paths for a binary named the same as
// the argument and run it if it exists, otherwise fall through to
// the behavior of treating the single argument as a package name
if (needPackageCommandSwap) {
let binExists = false
const dir = dirname(dirname(localBin))
const localBinPath = await localFileExists(dir, args[0])
if (localBinPath) {
pathArr.unshift(localBinPath)
binExists = true
} else if (await fileExists(`${globalBin}/${args[0]}`)) {
pathArr.unshift(globalBin)
binExists = true
}
if (binExists) {
return await _run()
}
packages.push(args[0])
}
// figure out whether we need to install stuff, or if local is fine
const localArb = new Arborist({
...flatOptions,
path,
})
const localTree = await localArb.loadActual()
const getLocalManifest = ({ tree, name }) => {
// look up the package name in the current tree inventory,
// if it's found then return that normalized pkg data
const [node] = tree.inventory.query('packageName', name)
if (node) {
return {
_id: node.pkgid,
...node.package,
[_localManifest]: true,
}
}
}
// If we do `npm exec foo`, and have a `foo` locally, then we'll
// always use that, so we don't really need to fetch the manifest.
// So: run npa on each packages entry, and if it is a name with a
// rawSpec==='', then try to find that node name in the tree inventory
// and only pacote fetch if that fails.
const manis = await Promise.all(packages.map(async p => {
const spec = npa(p, path)
if (spec.type === 'tag' && spec.rawSpec === '') {
const localManifest = getLocalManifest({
tree: localTree,
name: spec.name,
})
if (localManifest) {
return localManifest
}
}
// Force preferOnline to true so we are making sure to pull in the latest
// This is especially useful if the user didn't give us a version, and
// they expect to be running @latest
return await pacote.manifest(p, {
...flatOptions,
preferOnline: true,
})
}))
if (needPackageCommandSwap) {
args[0] = getBinFromManifest(manis[0])
}
// are all packages from the manifest list installed?
const needInstall =
manis.some(manifest => !manifest[_localManifest])
if (needInstall) {
const { npxCache } = flatOptions
const installDir = cacheInstallDir({ npxCache, packages })
await mkdirp(installDir)
const arb = new Arborist({
...flatOptions,
path: installDir,
})
const tree = await arb.loadActual()
// inspect the npx-space installed tree to check if the package is already
// there, if that's the case also check that it's version matches the same
// version expected by the user requested pkg returned by pacote.manifest
const filterMissingPackagesFromInstallDir = (mani) => {
const localManifest = getLocalManifest({ tree, name: mani.name })
if (localManifest) {
return localManifest.version !== mani.version
}
return true
}
// at this point, we have to ensure that we get the exact same
// version, because it's something that has only ever been installed
// by npm exec in the cache install directory
const add = manis
.filter(mani => !mani[_localManifest])
.filter(filterMissingPackagesFromInstallDir)
.map(mani => mani._id || mani._from)
.sort((a, b) => a.localeCompare(b, 'en'))
// no need to install if already present
if (add.length) {
if (!yes) {
// set -n to always say no
if (yes === false) {
throw new Error('canceled')
}
if (noTTY() || ciDetect()) {
log.warn('exec', `The following package${
add.length === 1 ? ' was' : 's were'
} not found and will be installed: ${
add.map((pkg) => pkg.replace(/@$/, '')).join(', ')
}`)
} else {
const addList = add.map(a => ` ${a.replace(/@$/, '')}`)
.join('\n') + '\n'
const prompt = `Need to install the following packages:\n${
addList
}Ok to proceed? `
npmlog.clearProgress()
const confirm = await read({ prompt, default: 'y' })
if (confirm.trim().toLowerCase().charAt(0) !== 'y') {
throw new Error('canceled')
}
}
}
await arb.reify({
...flatOptions,
add,
})
}
pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
}
return await _run()
}
module.exports = exec