libnpmexec
Advanced tools
Comparing version 4.0.8 to 4.0.9
const { resolve } = require('path') | ||
const { promisify } = require('util') | ||
const stat = promisify(require('fs').stat) | ||
const fs = require('@npmcli/fs') | ||
const walkUp = require('walk-up-path') | ||
const fileExists = (file) => stat(file) | ||
.then((res) => res.isFile()) | ||
.catch(() => false) | ||
const fileExists = async (file) => { | ||
try { | ||
const res = await fs.stat(file) | ||
return res.isFile() | ||
} catch { | ||
return false | ||
} | ||
} | ||
const localFileExists = async (dir, binName, root = '/') => { | ||
root = resolve(root).toLowerCase() | ||
for (const path of walkUp(resolve(dir))) { | ||
const localFileExists = async (dir, binName, root) => { | ||
for (const path of walkUp(dir)) { | ||
const binDir = resolve(path, 'node_modules', '.bin') | ||
@@ -20,3 +22,3 @@ | ||
if (path.toLowerCase() === root) { | ||
if (path.toLowerCase() === resolve(root).toLowerCase()) { | ||
return false | ||
@@ -23,0 +25,0 @@ } |
234
lib/index.js
@@ -1,14 +0,16 @@ | ||
const { delimiter, dirname, resolve } = require('path') | ||
'use strict' | ||
const { promisify } = require('util') | ||
const read = promisify(require('read')) | ||
const Arborist = require('@npmcli/arborist') | ||
const ciDetect = require('@npmcli/ci-detect') | ||
const crypto = require('crypto') | ||
const log = require('proc-log') | ||
const npmlog = require('npmlog') | ||
const mkdirp = require('mkdirp-infer-owner') | ||
const npa = require('npm-package-arg') | ||
const npmlog = require('npmlog') | ||
const pacote = require('pacote') | ||
const read = promisify(require('read')) | ||
const semver = require('semver') | ||
const cacheInstallDir = require('./cache-install-dir.js') | ||
const { fileExists, localFileExists } = require('./file-exists.js') | ||
@@ -19,9 +21,56 @@ const getBinFromManifest = require('./get-bin-from-manifest.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 { dirname, resolve } = require('path') | ||
const binPaths = [] | ||
// when checking the local tree we look up manifests, cache those results by | ||
// spec.raw so we don't have to fetch again when we check npxCache | ||
const manifests = new Map() | ||
const getManifest = async (spec, flatOptions) => { | ||
if (!manifests.get(spec.raw)) { | ||
const manifest = await pacote.manifest(spec, { ...flatOptions, preferOnline: true }) | ||
manifests.set(spec.raw, manifest) | ||
} | ||
return manifests.get(spec.raw) | ||
} | ||
// Returns the required manifest if the spec is missing from the tree | ||
const missingFromTree = async ({ spec, tree, flatOptions }) => { | ||
if (spec.registry && (spec.rawSpec === '' || spec.type !== 'tag')) { | ||
// registry spec that is not a specific tag. | ||
const nodesBySpec = tree.inventory.query('packageName', spec.name) | ||
for (const node of nodesBySpec) { | ||
if (spec.type === 'tag') { | ||
// package requested by name only | ||
return | ||
} else if (spec.type === 'version') { | ||
// package requested by specific version | ||
if (node.pkgid === spec.raw) { | ||
return | ||
} | ||
} else { | ||
// package requested by version range, only remaining registry type | ||
if (semver.satisfies(node.package.version, spec.rawSpec)) { | ||
return | ||
} | ||
} | ||
} | ||
return await getManifest(spec, flatOptions) | ||
} else { | ||
// non-registry spec, or a specific tag. Look up manifest and check | ||
// resolved to see if it's in the tree. | ||
const manifest = await getManifest(spec, flatOptions) | ||
const nodesByManifest = tree.inventory.query('packageName', manifest.name) | ||
for (const node of nodesByManifest) { | ||
if (node.package.resolved === manifest._resolved) { | ||
// we have a package by the same name and the same resolved destination, nothing to add. | ||
return | ||
} | ||
} | ||
return manifest | ||
} | ||
} | ||
const exec = async (opts) => { | ||
@@ -35,4 +84,6 @@ const { | ||
globalBin = '', | ||
globalPath = '', | ||
output, | ||
packages: _packages = [], | ||
// dereference values because we manipulate it later | ||
packages: [...packages] = [], | ||
path = '.', | ||
@@ -45,6 +96,3 @@ runPath = '.', | ||
// dereferences values because we manipulate it later | ||
const packages = [..._packages] | ||
const pathArr = [...PATH] | ||
const _run = () => runScript({ | ||
const run = () => runScript({ | ||
args, | ||
@@ -57,3 +105,3 @@ call, | ||
path, | ||
pathArr, | ||
binPaths, | ||
runPath, | ||
@@ -63,116 +111,92 @@ scriptShell, | ||
// nothing to maybe install, skip the arborist dance | ||
// interactive mode | ||
if (!call && !args.length && !packages.length) { | ||
return await _run() | ||
return 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 | ||
const needPackageCommandSwap = (args.length > 0) && (packages.length === 0) | ||
// If they asked for a command w/o specifying a package, see if there is a | ||
// bin that directly matches that name either globally or in the local tree. | ||
if (needPackageCommandSwap) { | ||
let binExists = false | ||
const dir = dirname(dirname(localBin)) | ||
const localBinPath = await localFileExists(dir, args[0]) | ||
const localBinPath = await localFileExists(dir, args[0], '/') | ||
if (localBinPath) { | ||
pathArr.unshift(localBinPath) | ||
binExists = true | ||
binPaths.push(localBinPath) | ||
return await run() | ||
} else if (await fileExists(`${globalBin}/${args[0]}`)) { | ||
pathArr.unshift(globalBin) | ||
binExists = true | ||
binPaths.push(globalBin) | ||
return await run() | ||
} | ||
if (binExists) { | ||
return await _run() | ||
} | ||
// We swap out args[0] with the bin from the manifest later | ||
packages.push(args[0]) | ||
} | ||
// figure out whether we need to install stuff, or if local is fine | ||
const localArb = new Arborist({ | ||
...flatOptions, | ||
path, | ||
}) | ||
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) | ||
// Find anything that isn't installed locally | ||
const needInstall = [] | ||
await Promise.all(packages.map(async pkg => { | ||
const spec = npa(pkg, path) | ||
const manifest = await missingFromTree({ spec, tree: localTree, flatOptions }) | ||
if (manifest) { | ||
// Package does not exist in the local tree | ||
needInstall.push({ spec, manifest }) | ||
} | ||
})) | ||
if (node) { | ||
return { | ||
_id: node.pkgid, | ||
...node.package, | ||
[_localManifest]: true, | ||
} | ||
if (needPackageCommandSwap) { | ||
// Either we have a scoped package or the bin of our package we inferred | ||
// from arg[0] might not be identical to the package name | ||
const spec = npa(args[0]) | ||
let commandManifest | ||
if (needInstall.length === 0) { | ||
commandManifest = await getManifest(spec, flatOptions) | ||
} else { | ||
commandManifest = needInstall[0].manifest | ||
} | ||
} | ||
// 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 | ||
} | ||
args[0] = getBinFromManifest(commandManifest) | ||
// See if the package is installed globally, and run the translated bin | ||
const globalArb = new Arborist({ ...flatOptions, path: globalPath, global: true }) | ||
const globalTree = await globalArb.loadActual() | ||
const globalManifest = await missingFromTree({ spec, tree: globalTree, flatOptions }) | ||
if (!globalManifest) { | ||
binPaths.push(globalBin) | ||
return await run() | ||
} | ||
// 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 add = [] | ||
if (needInstall.length > 0) { | ||
// Install things to the npx cache, if needed | ||
const { npxCache } = flatOptions | ||
const installDir = cacheInstallDir({ npxCache, packages }) | ||
if (!npxCache) { | ||
throw new Error('Must provide a valid npxCache path') | ||
} | ||
const hash = crypto.createHash('sha512') | ||
.update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n')) | ||
.digest('hex') | ||
.slice(0, 16) | ||
const installDir = resolve(npxCache, hash) | ||
await mkdirp(installDir) | ||
const arb = new Arborist({ | ||
const npxArb = 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 | ||
const npxTree = await npxArb.loadActual() | ||
await Promise.all(needInstall.map(async ({ spec }) => { | ||
const manifest = await missingFromTree({ spec, tree: npxTree, flatOptions }) | ||
if (manifest) { | ||
// Manifest is not in npxCache, we need to install it there | ||
if (!spec.registry) { | ||
add.push(manifest._from) | ||
} else { | ||
add.push(manifest._id) | ||
} | ||
} | ||
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) { | ||
@@ -204,3 +228,3 @@ if (!yes) { | ||
} | ||
await arb.reify({ | ||
await npxArb.reify({ | ||
...flatOptions, | ||
@@ -210,8 +234,8 @@ add, | ||
} | ||
pathArr.unshift(resolve(installDir, 'node_modules/.bin')) | ||
binPaths.push(resolve(installDir, 'node_modules/.bin')) | ||
} | ||
return await _run() | ||
return await run() | ||
} | ||
module.exports = exec |
@@ -1,3 +0,1 @@ | ||
const { delimiter } = require('path') | ||
const chalk = require('chalk') | ||
@@ -25,3 +23,3 @@ const ciDetect = require('@npmcli/ci-detect') | ||
path, | ||
pathArr, | ||
binPaths, | ||
runPath, | ||
@@ -75,7 +73,5 @@ scriptShell, | ||
stdioString: true, | ||
binPaths, | ||
event: 'npx', | ||
args, | ||
env: { | ||
PATH: pathArr.join(delimiter), | ||
}, | ||
stdio: 'inherit', | ||
@@ -82,0 +78,0 @@ }) |
{ | ||
"name": "libnpmexec", | ||
"version": "4.0.8", | ||
"version": "4.0.9", | ||
"files": [ | ||
@@ -60,3 +60,4 @@ "bin/", | ||
"@npmcli/ci-detect": "^2.0.0", | ||
"@npmcli/run-script": "^4.1.3", | ||
"@npmcli/fs": "^2.1.1", | ||
"@npmcli/run-script": "^4.2.0", | ||
"chalk": "^4.1.0", | ||
@@ -70,2 +71,3 @@ "mkdirp-infer-owner": "^2.0.0", | ||
"read-package-json-fast": "^2.0.2", | ||
"semver": "^7.3.7", | ||
"walk-up-path": "^1.0.0" | ||
@@ -72,0 +74,0 @@ }, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
15500
332
1
14
9
+ Added@npmcli/fs@^2.1.1
+ Addedsemver@^7.3.7
Updated@npmcli/run-script@^4.2.0