mirror of
https://github.com/nodejs/node.git
synced 2025-05-13 05:38:36 +00:00

PR-URL: https://github.com/nodejs/node/pull/39065 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Ruy Adorno <ruyadorno@github.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
185 lines
5.6 KiB
JavaScript
185 lines
5.6 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 logger = require('proc-log')
|
|
const mkdirp = require('mkdirp-infer-owner')
|
|
const npa = require('npm-package-arg')
|
|
const pacote = require('pacote')
|
|
const readPackageJson = require('read-package-json-fast')
|
|
|
|
const cacheInstallDir = require('./cache-install-dir.js')
|
|
const { fileExists, localFileExists } = require('./file-exists.js')
|
|
const getBinFromManifest = require('./get-bin-from-manifest.js')
|
|
const manifestMissing = require('./manifest-missing.js')
|
|
const noTTY = require('./no-tty.js')
|
|
const runScript = require('./run-script.js')
|
|
const isWindows = require('./is-windows.js')
|
|
|
|
/* 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
|
|
const log = flatOptions.log || logger
|
|
|
|
// dereferences values because we manipulate it later
|
|
const packages = [..._packages]
|
|
const pathArr = [...PATH]
|
|
const _run = () => runScript({
|
|
args,
|
|
call,
|
|
color,
|
|
flatOptions,
|
|
locationMsg,
|
|
log,
|
|
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])
|
|
}
|
|
|
|
// 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 readPackageJson at
|
|
// node_modules/${name}/package.json, 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 === '') {
|
|
// fall through to the pacote.manifest() approach
|
|
try {
|
|
const pj = resolve(path, 'node_modules', spec.name, 'package.json')
|
|
return await readPackageJson(pj)
|
|
} catch (er) {}
|
|
}
|
|
// 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])
|
|
|
|
// figure out whether we need to install stuff, or if local is fine
|
|
const localArb = new Arborist({
|
|
...flatOptions,
|
|
path,
|
|
})
|
|
const tree = await localArb.loadActual()
|
|
|
|
// do we have all the packages in manifest list?
|
|
const needInstall =
|
|
manis.some(manifest => manifestMissing({ tree, manifest }))
|
|
|
|
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()
|
|
|
|
// 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 => manifestMissing({
|
|
tree,
|
|
manifest: {
|
|
...mani,
|
|
_from: `${mani.name}@${mani.version}`,
|
|
},
|
|
}))
|
|
.map(mani => 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? `
|
|
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
|