diff options
Diffstat (limited to 'node_modules/libnpmexec/lib/index.js')
-rw-r--r-- | node_modules/libnpmexec/lib/index.js | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/node_modules/libnpmexec/lib/index.js b/node_modules/libnpmexec/lib/index.js new file mode 100644 index 000000000..906a0b540 --- /dev/null +++ b/node_modules/libnpmexec/lib/index.js @@ -0,0 +1,185 @@ +const { delimiter, resolve } = require('path') +const { promisify } = require('util') +const read = promisify(require('read')) +const stat = promisify(require('fs').stat) + +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 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 fileExists = (file) => stat(file) + .then((stat) => stat.isFile()) + .catch(() => false) + +/* 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 = '', + locationMsg = undefined, + globalBin = '', + output, + packages: _packages = [], + path = '.', + runPath = '.', + scriptShell = undefined, + 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 + if (await fileExists(`${localBin}/${args[0]}`)) { + pathArr.unshift(localBin) + 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 { cache } = flatOptions + const installDir = cacheInstallDir({ cache, 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)) + + // 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 |