diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 421efae116298..1888f053c62b9 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -35,6 +35,7 @@ graph LR; libnpmexec-->npmcli-arborist["@npmcli/arborist"]; libnpmexec-->npmcli-ci-detect["@npmcli/ci-detect"]; libnpmexec-->npmcli-eslint-config["@npmcli/eslint-config"]; + libnpmexec-->npmcli-fs["@npmcli/fs"]; libnpmexec-->npmcli-run-script["@npmcli/run-script"]; libnpmexec-->npmcli-template-oss["@npmcli/template-oss"]; libnpmexec-->npmlog; @@ -42,6 +43,7 @@ graph LR; libnpmexec-->proc-log; libnpmexec-->read-package-json-fast; libnpmexec-->read; + libnpmexec-->semver; libnpmfund-->npmcli-arborist["@npmcli/arborist"]; libnpmfund-->npmcli-eslint-config["@npmcli/eslint-config"]; libnpmfund-->npmcli-template-oss["@npmcli/template-oss"]; @@ -342,6 +344,7 @@ graph LR; libnpmexec-->npmcli-arborist["@npmcli/arborist"]; libnpmexec-->npmcli-ci-detect["@npmcli/ci-detect"]; libnpmexec-->npmcli-eslint-config["@npmcli/eslint-config"]; + libnpmexec-->npmcli-fs["@npmcli/fs"]; libnpmexec-->npmcli-run-script["@npmcli/run-script"]; libnpmexec-->npmcli-template-oss["@npmcli/template-oss"]; libnpmexec-->npmlog; @@ -349,6 +352,7 @@ graph LR; libnpmexec-->proc-log; libnpmexec-->read-package-json-fast; libnpmexec-->read; + libnpmexec-->semver; libnpmexec-->tap; libnpmexec-->walk-up-path; libnpmfund-->npmcli-arborist["@npmcli/arborist"]; diff --git a/docs/content/commands/npm-exec.md b/docs/content/commands/npm-exec.md index 8ccfa75c73386..3d8de1ea54ad6 100644 --- a/docs/content/commands/npm-exec.md +++ b/docs/content/commands/npm-exec.md @@ -127,7 +127,7 @@ $ npm exec -- foo@latest bar --package=@npmcli/foo * Default: * Type: String (can be set multiple times) -The package to install for [`npm exec`](/commands/npm-exec) +The package or packages to install for [`npm exec`](/commands/npm-exec) diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index e3e1bd6c73bb3..7ff2227c16ea1 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -1244,7 +1244,7 @@ Directory in which `npm pack` will save tarballs. * Default: * Type: String (can be set multiple times) -The package to install for [`npm exec`](/commands/npm-exec) +The package or packages to install for [`npm exec`](/commands/npm-exec) diff --git a/lib/commands/exec.js b/lib/commands/exec.js index d9a686cc9a3be..85a71923f521f 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -1,31 +1,5 @@ const libexec = require('libnpmexec') const BaseCommand = require('../base-command.js') -const getLocationMsg = require('../exec/get-workspace-location-msg.js') - -// it's like this: -// -// npm x pkg@version <-- runs the bin named "pkg" or the only bin if only 1 -// -// { name: 'pkg', bin: { pkg: 'pkg.js', foo: 'foo.js' }} <-- run pkg -// { name: 'pkg', bin: { foo: 'foo.js' }} <-- run foo? -// -// npm x -p pkg@version -- foo -// -// npm x -p pkg@version -- foo --registry=/dev/null -// -// const pkg = npm.config.get('package') || getPackageFrom(args[0]) -// const cmd = getCommand(pkg, args[0]) -// --> npm x -c 'cmd ...args.slice(1)' -// -// we've resolved cmd and args, and escaped them properly, and installed the -// relevant packages. -// -// Add the ${npx install prefix}/node_modules/.bin to PATH -// -// pkg = readPackageJson('./package.json') -// pkg.scripts.___npx = ${the -c arg} -// runScript({ pkg, event: 'npx', ... }) -// process.env.npm_lifecycle_event = 'npx' class Exec extends BaseCommand { static description = 'Run a command from a local or remote npm package' @@ -49,8 +23,10 @@ class Exec extends BaseCommand { static isShellout = true async exec (_args, { locationMsg, runPath } = {}) { + // This is where libnpmexec will look for locally installed packages const path = this.npm.localPrefix + // This is where libnpmexec will actually run the scripts from if (!runPath) { runPath = process.cwd() } @@ -62,7 +38,7 @@ class Exec extends BaseCommand { localBin, globalBin, } = this.npm - const output = (...outputArgs) => this.npm.output(...outputArgs) + const output = this.npm.output.bind(this.npm) const scriptShell = this.npm.config.get('script-shell') || undefined const packages = this.npm.config.get('package') const yes = this.npm.config.get('yes') @@ -92,10 +68,10 @@ class Exec extends BaseCommand { async execWorkspaces (args, filters) { await this.setWorkspaces(filters) - const color = this.npm.color - for (const path of this.workspacePaths) { - const locationMsg = await getLocationMsg({ color, path }) + for (const [name, path] of this.workspaces) { + const locationMsg = + `in workspace ${this.npm.chalk.green(name)} at location:\n${this.npm.chalk.dim(path)}` await this.exec(args, { locationMsg, runPath: path }) } } diff --git a/lib/commands/init.js b/lib/commands/init.js index cff8340dcd707..039f08e06059e 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -10,7 +10,6 @@ const PackageJson = require('@npmcli/package-json') const log = require('../utils/log-shim.js') const updateWorkspaces = require('../workspaces/update-workspaces.js') -const getLocationMsg = require('../exec/get-workspace-location-msg.js') const BaseCommand = require('../base-command.js') class Init extends BaseCommand { @@ -119,13 +118,7 @@ class Init extends BaseCommand { localBin, globalBin, } = this.npm - // this function is definitely called. But because of coverage map stuff - // it ends up both uncovered, and the coverage report doesn't even mention. - // the tests do assert that some output happens, so we know this line is - // being hit. - /* istanbul ignore next */ - const output = (...outputArgs) => this.npm.output(...outputArgs) - const locationMsg = await getLocationMsg({ color, path }) + const output = this.npm.output.bind(this.npm) const runPath = path const scriptShell = this.npm.config.get('script-shell') || undefined const yes = this.npm.config.get('yes') @@ -135,7 +128,6 @@ class Init extends BaseCommand { args: newArgs, color, localBin, - locationMsg, globalBin, output, path, diff --git a/lib/exec/get-workspace-location-msg.js b/lib/exec/get-workspace-location-msg.js deleted file mode 100644 index 813b11e789222..0000000000000 --- a/lib/exec/get-workspace-location-msg.js +++ /dev/null @@ -1,25 +0,0 @@ -const chalk = require('chalk') -const readPackageJson = require('read-package-json-fast') - -const nocolor = { - dim: s => s, - green: s => s, -} - -const getLocationMsg = async ({ color, path }) => { - const colorize = color ? chalk : nocolor - const { _id } = - await readPackageJson(`${path}/package.json`) - .catch(() => ({})) - - const workspaceMsg = _id - ? ` in workspace ${colorize.green(_id)}` - : ` in a ${colorize.green('new')} workspace` - const locationMsg = ` at location:\n${ - colorize.dim(path) - }` - - return `${workspaceMsg}${locationMsg}` -} - -module.exports = getLocationMsg diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 7d6af2473f2bd..592219736b321 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1470,7 +1470,7 @@ define('package', { hint: '', type: [String, Array], description: ` - The package to install for [\`npm exec\`](/commands/npm-exec) + The package or packages to install for [\`npm exec\`](/commands/npm-exec) `, flatten, }) diff --git a/node_modules/@npmcli/fs/lib/common/file-url-to-path/index.js b/node_modules/@npmcli/fs/lib/common/file-url-to-path/index.js deleted file mode 100644 index 7755d1c10e6d0..0000000000000 --- a/node_modules/@npmcli/fs/lib/common/file-url-to-path/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const url = require('url') - -const node = require('../node.js') -const polyfill = require('./polyfill.js') - -const useNative = node.satisfies('>=10.12.0') - -const fileURLToPath = (path) => { - // the polyfill is tested separately from this module, no need to hack - // process.version to try to trigger it just for coverage - // istanbul ignore next - return useNative - ? url.fileURLToPath(path) - : polyfill(path) -} - -module.exports = fileURLToPath diff --git a/node_modules/@npmcli/fs/lib/common/file-url-to-path/polyfill.js b/node_modules/@npmcli/fs/lib/common/file-url-to-path/polyfill.js deleted file mode 100644 index 6cc90f0b07d79..0000000000000 --- a/node_modules/@npmcli/fs/lib/common/file-url-to-path/polyfill.js +++ /dev/null @@ -1,121 +0,0 @@ -const { URL, domainToUnicode } = require('url') - -const CHAR_LOWERCASE_A = 97 -const CHAR_LOWERCASE_Z = 122 - -const isWindows = process.platform === 'win32' - -class ERR_INVALID_FILE_URL_HOST extends TypeError { - constructor (platform) { - super(`File URL host must be "localhost" or empty on ${platform}`) - this.code = 'ERR_INVALID_FILE_URL_HOST' - } - - toString () { - return `${this.name} [${this.code}]: ${this.message}` - } -} - -class ERR_INVALID_FILE_URL_PATH extends TypeError { - constructor (msg) { - super(`File URL path ${msg}`) - this.code = 'ERR_INVALID_FILE_URL_PATH' - } - - toString () { - return `${this.name} [${this.code}]: ${this.message}` - } -} - -class ERR_INVALID_ARG_TYPE extends TypeError { - constructor (name, actual) { - super(`The "${name}" argument must be one of type string or an instance ` + - `of URL. Received type ${typeof actual} ${actual}`) - this.code = 'ERR_INVALID_ARG_TYPE' - } - - toString () { - return `${this.name} [${this.code}]: ${this.message}` - } -} - -class ERR_INVALID_URL_SCHEME extends TypeError { - constructor (expected) { - super(`The URL must be of scheme ${expected}`) - this.code = 'ERR_INVALID_URL_SCHEME' - } - - toString () { - return `${this.name} [${this.code}]: ${this.message}` - } -} - -const isURLInstance = (input) => { - return input != null && input.href && input.origin -} - -const getPathFromURLWin32 = (url) => { - const hostname = url.hostname - let pathname = url.pathname - for (let n = 0; n < pathname.length; n++) { - if (pathname[n] === '%') { - const third = pathname.codePointAt(n + 2) | 0x20 - if ((pathname[n + 1] === '2' && third === 102) || - (pathname[n + 1] === '5' && third === 99)) { - throw new ERR_INVALID_FILE_URL_PATH('must not include encoded \\ or / characters') - } - } - } - - pathname = pathname.replace(/\//g, '\\') - pathname = decodeURIComponent(pathname) - if (hostname !== '') { - return `\\\\${domainToUnicode(hostname)}${pathname}` - } - - const letter = pathname.codePointAt(1) | 0x20 - const sep = pathname[2] - if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || - (sep !== ':')) { - throw new ERR_INVALID_FILE_URL_PATH('must be absolute') - } - - return pathname.slice(1) -} - -const getPathFromURLPosix = (url) => { - if (url.hostname !== '') { - throw new ERR_INVALID_FILE_URL_HOST(process.platform) - } - - const pathname = url.pathname - - for (let n = 0; n < pathname.length; n++) { - if (pathname[n] === '%') { - const third = pathname.codePointAt(n + 2) | 0x20 - if (pathname[n + 1] === '2' && third === 102) { - throw new ERR_INVALID_FILE_URL_PATH('must not include encoded / characters') - } - } - } - - return decodeURIComponent(pathname) -} - -const fileURLToPath = (path) => { - if (typeof path === 'string') { - path = new URL(path) - } else if (!isURLInstance(path)) { - throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path) - } - - if (path.protocol !== 'file:') { - throw new ERR_INVALID_URL_SCHEME('file') - } - - return isWindows - ? getPathFromURLWin32(path) - : getPathFromURLPosix(path) -} - -module.exports = fileURLToPath diff --git a/node_modules/@npmcli/fs/lib/common/owner-sync.js b/node_modules/@npmcli/fs/lib/common/owner-sync.js index 2055c4b21dec9..8fa18d5121eff 100644 --- a/node_modules/@npmcli/fs/lib/common/owner-sync.js +++ b/node_modules/@npmcli/fs/lib/common/owner-sync.js @@ -1,6 +1,6 @@ const { dirname, resolve } = require('path') +const url = require('url') -const fileURLToPath = require('./file-url-to-path/index.js') const fs = require('../fs.js') // given a path, find the owner of the nearest parent @@ -13,7 +13,7 @@ const find = (path) => { // fs methods accept URL objects with a scheme of file: so we need to unwrap // those into an actual path string before we can resolve it const resolved = path != null && path.href && path.origin - ? resolve(fileURLToPath(path)) + ? resolve(url.fileURLToPath(path)) : resolve(path) let stat diff --git a/node_modules/@npmcli/fs/lib/common/owner.js b/node_modules/@npmcli/fs/lib/common/owner.js index e3468b077d00e..3fe167cfc30aa 100644 --- a/node_modules/@npmcli/fs/lib/common/owner.js +++ b/node_modules/@npmcli/fs/lib/common/owner.js @@ -1,6 +1,6 @@ const { dirname, resolve } = require('path') +const url = require('url') -const fileURLToPath = require('./file-url-to-path/index.js') const fs = require('../fs.js') // given a path, find the owner of the nearest parent @@ -13,7 +13,7 @@ const find = async (path) => { // fs methods accept URL objects with a scheme of file: so we need to unwrap // those into an actual path string before we can resolve it const resolved = path != null && path.href && path.origin - ? resolve(fileURLToPath(path)) + ? resolve(url.fileURLToPath(path)) : resolve(path) let stat diff --git a/node_modules/@npmcli/fs/lib/index.js b/node_modules/@npmcli/fs/lib/index.js index 43892df5fee07..3a98648eca9a1 100644 --- a/node_modules/@npmcli/fs/lib/index.js +++ b/node_modules/@npmcli/fs/lib/index.js @@ -2,7 +2,7 @@ module.exports = { ...require('./fs.js'), copyFile: require('./copy-file.js'), cp: require('./cp/index.js'), - mkdir: require('./mkdir/index.js'), + mkdir: require('./mkdir.js'), mkdtemp: require('./mkdtemp.js'), rm: require('./rm/index.js'), withTempDir: require('./with-temp-dir.js'), diff --git a/node_modules/@npmcli/fs/lib/mkdir.js b/node_modules/@npmcli/fs/lib/mkdir.js new file mode 100644 index 0000000000000..098d8d0a09ae3 --- /dev/null +++ b/node_modules/@npmcli/fs/lib/mkdir.js @@ -0,0 +1,19 @@ +const fs = require('./fs.js') +const getOptions = require('./common/get-options.js') +const withOwner = require('./with-owner.js') + +// extends mkdir with the ability to specify an owner of the new dir +const mkdir = async (path, opts) => { + const options = getOptions(opts, { + copy: ['mode', 'recursive'], + wrap: 'mode', + }) + + return withOwner( + path, + () => fs.mkdir(path, options), + opts + ) +} + +module.exports = mkdir diff --git a/node_modules/@npmcli/fs/lib/mkdir/index.js b/node_modules/@npmcli/fs/lib/mkdir/index.js deleted file mode 100644 index e2691042daa26..0000000000000 --- a/node_modules/@npmcli/fs/lib/mkdir/index.js +++ /dev/null @@ -1,29 +0,0 @@ -const fs = require('../fs.js') -const getOptions = require('../common/get-options.js') -const node = require('../common/node.js') -const withOwner = require('../with-owner.js') - -const polyfill = require('./polyfill.js') - -// node 10.12.0 added the options parameter, which allows recursive and mode -// properties to be passed -const useNative = node.satisfies('>=10.12.0') - -// extends mkdir with the ability to specify an owner of the new dir -const mkdir = async (path, opts) => { - const options = getOptions(opts, { - copy: ['mode', 'recursive'], - wrap: 'mode', - }) - - // the polyfill is tested separately from this module, no need to hack - // process.version to try to trigger it just for coverage - // istanbul ignore next - return withOwner( - path, - () => useNative ? fs.mkdir(path, options) : polyfill(path, options), - opts - ) -} - -module.exports = mkdir diff --git a/node_modules/@npmcli/fs/lib/mkdir/polyfill.js b/node_modules/@npmcli/fs/lib/mkdir/polyfill.js deleted file mode 100644 index 4f8e6f006a30e..0000000000000 --- a/node_modules/@npmcli/fs/lib/mkdir/polyfill.js +++ /dev/null @@ -1,81 +0,0 @@ -const { dirname } = require('path') - -const fileURLToPath = require('../common/file-url-to-path/index.js') -const fs = require('../fs.js') - -const defaultOptions = { - mode: 0o777, - recursive: false, -} - -const mkdir = async (path, opts) => { - const options = { ...defaultOptions, ...opts } - - // if we're not in recursive mode, just call the real mkdir with the path and - // the mode option only - if (!options.recursive) { - return fs.mkdir(path, options.mode) - } - - const makeDirectory = async (dir, mode) => { - // we can't use dirname directly since these functions support URL - // objects with the file: protocol as the path input, so first we get a - // string path, then we can call dirname on that - const parent = dir != null && dir.href && dir.origin - ? dirname(fileURLToPath(dir)) - : dirname(dir) - - // if the parent is the dir itself, try to create it. anything but EISDIR - // should be rethrown - if (parent === dir) { - try { - await fs.mkdir(dir, opts) - } catch (err) { - if (err.code !== 'EISDIR') { - throw err - } - } - return undefined - } - - try { - await fs.mkdir(dir, mode) - return dir - } catch (err) { - // ENOENT means the parent wasn't there, so create that - if (err.code === 'ENOENT') { - const made = await makeDirectory(parent, mode) - await makeDirectory(dir, mode) - // return the shallowest path we created, i.e. the result of creating - // the parent - return made - } - - // an EEXIST means there's already something there - // an EROFS means we have a read-only filesystem and can't create a dir - // any other error is fatal and we should give up now - if (err.code !== 'EEXIST' && err.code !== 'EROFS') { - throw err - } - - // stat the directory, if the result is a directory, then we successfully - // created this one so return its path. otherwise, we reject with the - // original error by ignoring the error in the catch - try { - const stat = await fs.stat(dir) - if (stat.isDirectory()) { - // if it already existed, we didn't create anything so return - // undefined - return undefined - } - } catch (_) {} - - // if the thing that's there isn't a directory, then just re-throw - throw err - } - } - - return makeDirectory(path, options.mode) -} - -module.exports = mkdir diff --git a/node_modules/@npmcli/fs/lib/with-temp-dir.js b/node_modules/@npmcli/fs/lib/with-temp-dir.js index ac9ebb714b989..ad08e6ee6e6d6 100644 --- a/node_modules/@npmcli/fs/lib/with-temp-dir.js +++ b/node_modules/@npmcli/fs/lib/with-temp-dir.js @@ -1,7 +1,7 @@ const { join, sep } = require('path') const getOptions = require('./common/get-options.js') -const mkdir = require('./mkdir/index.js') +const mkdir = require('./mkdir.js') const mkdtemp = require('./mkdtemp.js') const rm = require('./rm/index.js') diff --git a/node_modules/@npmcli/fs/package.json b/node_modules/@npmcli/fs/package.json index 799bf514f200b..9e18028218d1a 100644 --- a/node_modules/@npmcli/fs/package.json +++ b/node_modules/@npmcli/fs/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/fs", - "version": "2.1.0", + "version": "2.1.1", "description": "filesystem utilities for the npm cli", "main": "lib/index.js", "files": [ @@ -33,8 +33,8 @@ "license": "ISC", "devDependencies": { "@npmcli/eslint-config": "^3.0.1", - "@npmcli/template-oss": "3.1.2", - "tap": "^15.1.6" + "@npmcli/template-oss": "3.5.0", + "tap": "^16.0.1" }, "dependencies": { "@gar/promisify": "^1.1.3", @@ -45,6 +45,6 @@ }, "templateOSS": { "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", - "version": "3.1.2" + "version": "3.5.0" } } diff --git a/node_modules/@npmcli/run-script/lib/make-spawn-args.js b/node_modules/@npmcli/run-script/lib/make-spawn-args.js index f2253d7cc607b..7725fd976c893 100644 --- a/node_modules/@npmcli/run-script/lib/make-spawn-args.js +++ b/node_modules/@npmcli/run-script/lib/make-spawn-args.js @@ -20,6 +20,7 @@ const makeSpawnArgs = options => { event, path, scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh', + binPaths, env = {}, stdio, cmd, @@ -27,7 +28,7 @@ const makeSpawnArgs = options => { stdioString = false, } = options - const spawnEnv = setPATH(path, { + const spawnEnv = setPATH(path, binPaths, { // we need to at least save the PATH environment var ...process.env, ...env, @@ -100,7 +101,9 @@ const makeSpawnArgs = options => { // delete the script, this is just a best effort try { unlink(scriptFile) - } catch (err) {} + } catch (err) { + // ignore errors + } } return [scriptShell, spawnArgs, spawnOpts, cleanup] diff --git a/node_modules/@npmcli/run-script/lib/run-script-pkg.js b/node_modules/@npmcli/run-script/lib/run-script-pkg.js index 84c5e2bfe0c52..ec6ef31e50ab0 100644 --- a/node_modules/@npmcli/run-script/lib/run-script-pkg.js +++ b/node_modules/@npmcli/run-script/lib/run-script-pkg.js @@ -14,6 +14,7 @@ const runScriptPkg = async options => { event, path, scriptShell, + binPaths = false, env = {}, stdio = 'pipe', pkg, @@ -58,6 +59,7 @@ const runScriptPkg = async options => { event, path, scriptShell, + binPaths, env: packageEnvs(env, pkg), stdio, cmd, diff --git a/node_modules/@npmcli/run-script/lib/set-path.js b/node_modules/@npmcli/run-script/lib/set-path.js index 07671f44579dc..c59c270d9969a 100644 --- a/node_modules/@npmcli/run-script/lib/set-path.js +++ b/node_modules/@npmcli/run-script/lib/set-path.js @@ -1,5 +1,4 @@ -const { resolve, dirname } = require('path') -const isWindows = require('./is-windows.js') +const { resolve, dirname, delimiter } = require('path') // the path here is relative, even though it does not need to be // in order to make the posix tests pass in windows const nodeGypPath = resolve(__dirname, '../lib/node-gyp-bin') @@ -7,18 +6,19 @@ const nodeGypPath = resolve(__dirname, '../lib/node-gyp-bin') // Windows typically calls its PATH environ 'Path', but this is not // guaranteed, nor is it guaranteed to be the only one. Merge them // all together in the order they appear in the object. -const setPATH = (projectPath, env) => { - // not require('path').delimiter, because we fake this for testing - const delimiter = isWindows ? ';' : ':' +const setPATH = (projectPath, binPaths, env) => { const PATH = Object.keys(env).filter(p => /^path$/i.test(p) && env[p]) .map(p => env[p].split(delimiter)) .reduce((set, p) => set.concat(p.filter(concatted => !set.includes(concatted))), []) .join(delimiter) const pathArr = [] + if (binPaths) { + pathArr.push(...binPaths) + } // unshift the ./node_modules/.bin from every folder // walk up until dirname() does nothing, at the root - // XXX should we specify a cwd that we don't go above? + // XXX we should specify a cwd that we don't go above let p = projectPath let pp do { diff --git a/node_modules/@npmcli/run-script/package.json b/node_modules/@npmcli/run-script/package.json index c096f39421bf1..a6629826d29c2 100644 --- a/node_modules/@npmcli/run-script/package.json +++ b/node_modules/@npmcli/run-script/package.json @@ -1,6 +1,6 @@ { "name": "@npmcli/run-script", - "version": "4.1.7", + "version": "4.2.0", "description": "Run a lifecycle script for a package (descendant of npm-lifecycle)", "author": "GitHub Inc.", "license": "ISC", diff --git a/package-lock.json b/package-lock.json index 3eba393001c46..231448802f748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", - "@npmcli/run-script": "^4.1.7", + "@npmcli/run-script": "^4.2.0", "abbrev": "~1.1.1", "archy": "~1.0.0", "cacache": "^16.1.1", @@ -916,9 +916,9 @@ } }, "node_modules/@npmcli/fs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.0.tgz", - "integrity": "sha512-DmfBvNXGaetMxj9LTp8NAN9vEidXURrf5ZTslQzEAi/6GbW+4yjaLFQc6Tue5cpZ9Frlk4OBo/Snf1Bh/S7qTQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.1.tgz", + "integrity": "sha512-1Q0uzx6c/NVNGszePbr5Gc2riSU1zLpNlo/1YWntH+eaPmMgBssAW0qXofCVkpdj3ce4swZtlDYQu+NKiYcptg==", "inBundle": true, "dependencies": { "@gar/promisify": "^1.1.3", @@ -1057,9 +1057,9 @@ } }, "node_modules/@npmcli/run-script": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.1.7.tgz", - "integrity": "sha512-WXr/MyM4tpKA4BotB81NccGAv8B48lNH0gRoILucbcAhTQXLCoi6HflMV3KdXubIqvP9SuLsFn68Z7r4jl+ppw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.0.tgz", + "integrity": "sha512-e/QgLg7j2wSJp1/7JRl0GC8c7PMX+uYlA/1Tb+IDOLdSM4T7K1VQ9mm9IGU3WRtY5vEIObpqCLb3aCNCug18DA==", "inBundle": true, "dependencies": { "@npmcli/node-gyp": "^2.0.0", @@ -10136,7 +10136,8 @@ "dependencies": { "@npmcli/arborist": "^5.0.0", "@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", "mkdirp-infer-owner": "^2.0.0", "npm-package-arg": "^9.0.1", @@ -10145,6 +10146,7 @@ "proc-log": "^2.0.0", "read": "^1.0.7", "read-package-json-fast": "^2.0.2", + "semver": "^7.3.7", "walk-up-path": "^1.0.0" }, "devDependencies": { diff --git a/package.json b/package.json index 7526c4754b0ff..1adb37652a075 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@npmcli/fs": "^2.1.0", "@npmcli/map-workspaces": "^2.0.3", "@npmcli/package-json": "^2.0.0", - "@npmcli/run-script": "^4.1.7", + "@npmcli/run-script": "^4.2.0", "abbrev": "~1.1.1", "archy": "~1.0.0", "cacache": "^16.1.1", diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 89c9969d69424..0ae253dc65f0f 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -1315,7 +1315,7 @@ exports[`test/lib/utils/config/definitions.js TAP > config description for packa * Default: * Type: String (can be set multiple times) -The package to install for [\`npm exec\`](/commands/npm-exec) +The package or packages to install for [\`npm exec\`](/commands/npm-exec) ` exports[`test/lib/utils/config/definitions.js TAP > config description for package-lock 1`] = ` diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index a9247f49c0418..0c569bcc56b3a 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -1117,7 +1117,7 @@ Directory in which \`npm pack\` will save tarballs. * Default: * Type: String (can be set multiple times) -The package to install for [\`npm exec\`](/commands/npm-exec) +The package or packages to install for [\`npm exec\`](/commands/npm-exec) diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 049ed327c8fc5..6ac719122bfb2 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -1,1547 +1,97 @@ const t = require('tap') -const { fake: mockNpm } = require('../../fixtures/mock-npm') -const { resolve, delimiter } = require('path') +const fs = require('@npmcli/fs') +const path = require('path') +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') +const MockRegistry = require('../../fixtures/mock-registry.js') -const ARB_CTOR = [] -const ARB_ACTUAL_TREE = {} -const ARB_REIFY = [] -class Arborist { - constructor (options) { - ARB_CTOR.push(options) - this.path = options.path - } - - async loadActual () { - return ARB_ACTUAL_TREE[this.path] - } - - async reify (options) { - ARB_REIFY.push(options) - } -} - -let PROGRESS_ENABLED = true -const LOG_WARN = [] -let PROGRESS_IGNORED = false -const flatOptions = { - npxCache: 'npx-cache-dir', - color: false, - cache: 'cache-dir', - legacyPeerDeps: false, - package: [], -} -const config = { - cache: 'bad-cache-dir', // this should never show up passed into libnpmexec - yes: true, - call: '', - package: [], - 'script-shell': 'shell-cmd', -} - -const npm = mockNpm({ - flatOptions, - config, - localPrefix: 'local-prefix', - localBin: 'local-bin', - globalBin: 'global-bin', -}) - -const RUN_SCRIPTS = [] -const runScript = async opt => { - RUN_SCRIPTS.push(opt) - if (!PROGRESS_IGNORED && PROGRESS_ENABLED) { - throw new Error('progress not disabled during run script!') - } -} - -const MANIFESTS = {} -const pacote = { - manifest: async (spec, options) => { - return MANIFESTS[spec] - }, -} - -const MKDIRPS = [] -const mkdirp = async path => MKDIRPS.push(path) - -let READ_RESULT = '' -let READ_ERROR = null -const READ = [] -const read = (options, cb) => { - READ.push(options) - process.nextTick(() => cb(READ_ERROR, READ_RESULT)) -} - -let CI_NAME = 'travis-ci' - -const log = { - 'proc-log': { - warn: (...args) => { - LOG_WARN.push(args) - }, - }, - npmlog: { - disableProgress: () => { - PROGRESS_ENABLED = false - }, - enableProgress: () => { - PROGRESS_ENABLED = true - }, - clearProgress: () => {}, - }, -} - -const mocks = { - libnpmexec: t.mock('libnpmexec', { - '@npmcli/arborist': Arborist, - '@npmcli/run-script': runScript, - '@npmcli/ci-detect': () => CI_NAME, - pacote, - read, - 'mkdirp-infer-owner': mkdirp, - ...log, - }), - ...log, -} -const Exec = t.mock('../../../lib/commands/exec.js', mocks) -const exec = new Exec(npm) - -t.afterEach(() => { - MKDIRPS.length = 0 - ARB_CTOR.length = 0 - ARB_REIFY.length = 0 - RUN_SCRIPTS.length = 0 - READ.length = 0 - READ_RESULT = '' - READ_ERROR = null - LOG_WARN.length = 0 - PROGRESS_IGNORED = false - flatOptions.legacyPeerDeps = false - flatOptions.color = false - config['script-shell'] = 'shell-cmd' - config.package = [] - flatOptions.package = [] - config.call = '' - config.yes = true - npm.color = false - npm.localBin = 'local-bin' - npm.globalBin = 'global-bin' -}) - -t.test('npx foo, bin already exists locally', async t => { - const path = t.testdir({ - node_modules: { - '.bin': { - foo: 'just some file', - }, - }, - }) - - PROGRESS_IGNORED = true - npm.localBin = resolve(path, 'node_modules', '.bin') - - await exec.exec(['foo', 'one arg', 'two arg']) - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - cache: flatOptions.cache, - npxCache: flatOptions.npxCache, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npx foo, bin already exists globally', async t => { - const path = t.testdir({ - node_modules: { - '.bin': { - foo: 'just some file', - }, - }, - }) - - PROGRESS_IGNORED = true - npm.globalBin = resolve(path, 'node_modules', '.bin') - - await exec.exec(['foo', 'one arg', 'two arg']) - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [npm.globalBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, already present locally', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec , run interactive shell', t => { - CI_NAME = null - const { isTTY } = process.stdin - process.stdin.isTTY = true - t.teardown(() => (process.stdin.isTTY = isTTY)) - - const run = async (t, doRun) => { - LOG_WARN.length = 0 - ARB_CTOR.length = 0 - MKDIRPS.length = 0 - ARB_REIFY.length = 0 - npm._mockOutputs.length = 0 - await exec.exec([]) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist') - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - if (doRun) { - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'shell-cmd' } }, - args: [], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) - } else { - t.strictSame(RUN_SCRIPTS, []) - } - - RUN_SCRIPTS.length = 0 - } - t.test('print message when tty and not in CI', async t => { - CI_NAME = null - process.stdin.isTTY = true - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\nEntering npm script environment at location:\n${process.cwd()}\nType 'exit' or ^D when finished\n`, - ], - ], - 'printed message about interactive shell' - ) - }) - - t.test('print message with color when tty and not in CI', async t => { - CI_NAME = null - process.stdin.isTTY = true - npm.color = true - flatOptions.color = true - - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m at location:\u001b[0m\n\u001b[0m\u001b[2m${process.cwd()}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`, - ], - ], - 'printed message about interactive shell' - ) - }) - - t.test('no message when not TTY', async t => { - CI_NAME = null - process.stdin.isTTY = false - await run(t, true) - t.strictSame(LOG_WARN, []) - t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') - }) - - t.test('print warning when in CI and interactive', async t => { - CI_NAME = 'travis-ci' - process.stdin.isTTY = true - await run(t, false) - t.strictSame(LOG_WARN, [['exec', 'Interactive mode disabled in CI environment']]) - t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') - }) - - t.test('not defined script-shell config value', async t => { - CI_NAME = null - process.stdin.isTTY = true - config['script-shell'] = undefined - - await exec.exec([]) - - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: /sh|cmd/ } }, - }, - ]) - - LOG_WARN.length = 0 - ARB_CTOR.length = 0 - MKDIRPS.length = 0 - ARB_REIFY.length = 0 - npm._mockOutputs.length = 0 - RUN_SCRIPTS.length = 0 - }) - - t.end() -}) - -t.test('npm exec foo, not present locally or in central loc', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo@'], legacyPeerDeps: false }], 'need to install foo@') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, packageLockOnly set to true', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - npm.config.set('package-lock-only', true) - t.teardown(() => { - npm.config.set('package-lock-only', false) - }) - - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ - path, - packageLockOnly: false, - }]) - t.match(ARB_REIFY, [{ - add: ['foo@'], - legacyPeerDeps: false, - packageLockOnly: false, - }], 'need to install foo@') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, not present locally but in central loc', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to install again, already there') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, present locally but wrong version', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/2badf4630f1cfaad') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['foo@2.x'] = { - name: 'foo', - version: '2.3.4', - bin: { - foo: 'foo', - }, - _from: 'foo@2.x', - } - await exec.exec(['foo@2.x', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo@2.x'], legacyPeerDeps: false }], 'need to add foo@2.x') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec foo, present locally but outdated version', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '2.3.4', - bin: { - foo: 'foo', - }, - _from: 'foo@2.x', - } - await exec.exec(['foo', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add: ['foo'], legacyPeerDeps: false }], 'need to add foo@2.x') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec --package=foo bar', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - config.package = ['foo'] - flatOptions.package = ['foo'] - await exec.exec(['bar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'bar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => { - const foobarManifest = { - name: '@foo/bar', - version: '1.2.3', - bin: { - foo: 'foo', - bar: 'bar', - }, - } - const path = t.testdir({ - node_modules: { - '@foo/bar': { - 'package.json': JSON.stringify(foobarManifest), - }, - }, - }) - const pkg = { - name: '@foo/bar', - version: '1.2.3', - bin: { foo: 'foo', bar: 'bar' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['@foo/bar'] = foobarManifest - await exec.exec(['@foo/bar', '--some=arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'bar' } }, - args: ['--some=arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test( - 'npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', - async t => { - const pkg = { - name: '@foo/bar', - version: '1.2.3', - bin: { - baz: 'corge', // pick the first one - qux: 'corge', - quux: 'corge', - }, - } - const path = t.testdir({ - node_modules: { - '@foo/bar': { - 'package.json': JSON.stringify(pkg), - }, - }, - }) - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS['@foo/bar'] = pkg - await exec.exec(['@foo/bar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'baz' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) - } -) - -t.test( - 'npm exec @foo/bar, with different bin alias and no unscoped named bin, locally installed', - async t => { - const path = t.testdir() - const pkg = { - name: '@foo/bar', - version: '1.2.3.', - bin: { foo: 'qux', corge: 'qux', baz: 'quux' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS['@foo/bar'] = { - name: '@foo/bar', - version: '1.2.3', - bin: { - foo: 'qux', - corge: 'qux', - baz: 'quux', - }, - _from: 'foo@', - _id: '@foo/bar@1.2.3', - } - await t.rejects(exec.exec(['@foo/bar']), { - message: 'could not determine executable to run', - pkgid: '@foo/bar@1.2.3', - }) - } -) - -t.test('run command with 2 packages, need install, verify sort', async t => { - // test both directions, should use same install dir both times - // also test the read() call here, verify that the prompts match - const cases = [ - ['foo', 'bar'], - ['bar', 'foo'], - ] - t.plan(cases.length) - for (const packages of cases) { - t.test(packages.join(', '), async t => { - config.package = packages - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await exec.exec(['foobar', 'one arg', 'two arg']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - }) - } -}) - -t.test('npm exec foo, no bin in package', async t => { - const pkg = { name: 'foo', version: '1.2.3' } - const path = t.testdir({ - node_modules: { - foo: { - 'package.json': JSON.stringify(pkg), - }, - }, - }) - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - _from: 'foo@', - _id: 'foo@1.2.3', - } - await t.rejects(exec.exec(['foo']), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', - }) -}) - -t.test('npm exec foo, many bins in package, none named foo', async t => { - const path = t.testdir() - const pkg = { - name: 'foo', - version: '1.2.3', - bin: { bar: 'bar', baz: 'baz' }, - } - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ - ...pkg, - package: pkg, - pkgid: `${pkg.name}@${pkg.version}`, - }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - bar: 'bar', - baz: 'baz', - }, - _from: 'foo@', - _id: 'foo@1.2.3', - } - await t.rejects(exec.exec(['foo']), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', - }) -}) - -t.test('npm exec -p foo -c "ls -laF"', async t => { - const path = t.testdir() - const pkg = { name: 'foo', version: '1.2.3' } - npm.localPrefix = path - config.package = ['foo'] - config.call = 'ls -laF' - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - _from: 'foo@', - } - await exec.exec([]) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'ls -laF' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }, - ]) -}) - -t.test('positional args and --call together is an error', async t => { - config.call = 'true' - await t.rejects(exec.exec(['foo']), exec.usage) -}) - -t.test('prompt when installs are needed if not already present and shell is a TTY', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', +t.test('call with args', async t => { + const { npm } = await loadMockNpm(t, { + config: { + call: 'foo', }, - _from: 'bar@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) -}) - -t.test( - /* eslint-disable-next-line max-len */ - 'skip prompt when installs are needed if not already present and shell is not a tty (multiple packages)', - async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = false - process.stdin.isTTY = false - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame( - LOG_WARN, - [['exec', 'The following packages were not found and will be installed: bar, foo']], - 'should have printed a warning' - ) - } -) - -t.test( - /* eslint-disable-next-line max-len */ - 'skip prompt when installs are needed if not already present and shell is not a tty (single package)', - async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' - }) - process.stdout.isTTY = false - process.stdin.isTTY = false - CI_NAME = false - - const packages = ['foo'] - READ_RESULT = 'yolo' - - config.package = packages - config.yes = undefined - - const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b, 'en')) - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - await exec.exec(['foobar']) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.match(ARB_REIFY, [{ add, legacyPeerDeps: false }], 'need to install the package') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }, - ]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame( - LOG_WARN, - [['exec', 'The following package was not found and will be installed: foo']], - 'should have printed a warning' - ) - } -) - -t.test('abort if prompt rejected', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_RESULT = 'no, why would I want such a thing??' - config.package = packages - config.yes = undefined - - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), /canceled/, 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) + await t.rejects( + npm.exec('exec', ['bar']), + { code: 'EUSAGE' } + ) }) -t.test('abort if prompt false', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' +t.test('registry package', async t => { + const registry = new MockRegistry({ + tap: t, + registry: 'https://registry.npmjs.org/', }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - - const packages = ['foo', 'bar'] - READ_ERROR = 'canceled' - config.package = packages - config.yes = undefined + const manifest = registry.manifest({ name: '@npmcli/npx-test' }) + manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' } - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, + const { npm } = await loadMockNpm(t, { + config: { + yes: true, }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() + prefixDir: { + 'npm-exec-test': { + 'package.json': JSON.stringify(manifest), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, }, }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), 'canceled', 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [ - { - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }, - ]) -}) - -t.test('abort if -n provided', async t => { - const stdoutTTY = process.stdout.isTTY - const stdinTTY = process.stdin.isTTY - t.teardown(() => { - process.stdout.isTTY = stdoutTTY - process.stdin.isTTY = stdinTTY - CI_NAME = 'travis-ci' + globals: ({ prefix }) => ({ + 'process.cwd': () => prefix, + }), }) - process.stdout.isTTY = true - process.stdin.isTTY = true - CI_NAME = false - const packages = ['foo', 'bar'] + await registry.package({ manifest, + tarballs: { + '1.0.0': path.join(npm.prefix, 'npm-exec-test'), + } }) - config.package = packages - config.yes = false - - const path = t.testdir() - const installDir = resolve('npx-cache-dir/07de77790e5f40f2') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - MANIFESTS.bar = { - name: 'bar', - version: '1.2.3', - bin: { - bar: 'bar', - }, - _from: 'bar@', - } - await t.rejects(exec.exec(['foobar']), /canceled/, 'should be canceled') - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, []) -}) - -t.test('forward legacyPeerDeps opt', async t => { - const path = t.testdir() - const installDir = resolve('npx-cache-dir/f7fbba6e0636f890') - npm.localPrefix = path - ARB_ACTUAL_TREE[path] = { - inventory: { - query () { - return new Set() - }, - }, - } - ARB_ACTUAL_TREE[installDir] = { - inventory: { - query () { - return new Set() - }, - }, - } - MANIFESTS.foo = { - name: 'foo', - version: '1.2.3', - bin: { - foo: 'foo', - }, - _from: 'foo@', - } - config.yes = true - flatOptions.legacyPeerDeps = true - await exec.exec(['foo']) - t.match( - ARB_REIFY, - [{ add: ['foo@'], legacyPeerDeps: true }], - 'need to install foo@ using legacyPeerDeps opt' - ) + await npm.exec('exec', ['@npmcli/npx-test']) + const exists = await fs.stat(path.join(npm.prefix, 'npm-exec-test-success')) + t.ok(exists.isFile(), 'bin ran, creating file') }) t.test('workspaces', async t => { - npm.localPrefix = t.testdir({ - node_modules: { - '.bin': { - a: '', - foo: '', - }, - }, - packages: { - a: { + const registry = new MockRegistry({ + tap: t, + registry: 'https://registry.npmjs.org/', + }) + + const manifest = registry.manifest({ name: '@npmcli/npx-test' }) + manifest.versions['1.0.0'].bin = { 'npx-test': 'index.js' } + + const { npm } = await loadMockNpm(t, { + config: { + yes: true, + workspace: ['workspace-a'], + }, + prefixDir: { + 'npm-exec-test': { + 'package.json': JSON.stringify(manifest), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('npm-exec-test-success', '')`, + }, + 'package.json': JSON.stringify({ + name: '@npmcli/npx-workspace-test', + workspaces: ['workspace-a'], + }), + 'workspace-a': { 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - bin: 'cli.js', - }), - 'cli.js': '', - }, - b: { - 'package.json': JSON.stringify({ - name: 'b', - version: '1.0.0', + name: 'workspace-a', }), }, }, - 'package.json': JSON.stringify({ - name: 'root', - version: '1.0.0', - workspaces: ['packages/*'], + globals: ({ prefix }) => ({ + 'process.cwd': () => prefix, }), }) - const pkg = { name: 'foo', version: '1.2.3', bin: { foo: 'foo' } } - PROGRESS_IGNORED = true - npm.localBin = resolve(npm.localPrefix, 'node_modules', '.bin') - - // with arg matching existing bin, run scripts in the context of a workspace - await exec.execWorkspaces(['foo', 'one arg', 'two arg'], ['a', 'b']) - - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: npm.localPrefix, - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - { - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: npm.localPrefix, - stdioString: true, - event: 'npx', - env: { - PATH: [npm.localBin, process.env.PATH].join(delimiter), - }, - stdio: 'inherit', - }, - ], 'should run with multiple args across multiple workspaces') - - // clean up - RUN_SCRIPTS.length = 0 - - // with packages, run scripts in the context of a workspace - config.package = ['foo'] - config.call = 'foo' - config.yes = false - - ARB_ACTUAL_TREE[npm.localPrefix] = { - inventory: { - query () { - return new Set([{ ...pkg, package: pkg }]) - }, - }, - } - - await exec.execWorkspaces([], ['a', 'b']) - - // path should point to the workspace folder - t.match(RUN_SCRIPTS, [ - { - pkg: { scripts: { npx: 'foo' } }, - args: [], - banner: false, - path: resolve(npm.localPrefix, 'packages', 'a'), - stdioString: true, - event: 'npx', - stdio: 'inherit', - }, - { - pkg: { scripts: { npx: 'foo' } }, - args: [], - banner: false, - path: resolve(npm.localPrefix, 'packages', 'b'), - stdioString: true, - event: 'npx', - stdio: 'inherit', - }, - ], 'should run without args in multiple workspaces') - - t.match(ARB_CTOR, [ - { path: npm.localPrefix }, - { path: npm.localPrefix }, - ]) - - // no args, spawn interactive shell - CI_NAME = null - config.package = [] - config.call = '' - process.stdin.isTTY = true - - await exec.execWorkspaces([], ['a']) - - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - `\nEntering npm script environment in workspace a@1.0.0 at location:\n${resolve( - npm.localPrefix, - 'packages/a' - )}\nType 'exit' or ^D when finished\n`, - ], - ], - 'printed message about interactive shell' - ) - - npm.color = true - flatOptions.color = true - npm._mockOutputs.length = 0 - await exec.execWorkspaces([], ['a']) - - t.strictSame(LOG_WARN, []) - t.strictSame( - npm._mockOutputs, - [ - [ - /* eslint-disable-next-line max-len */ - `\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[32ma@1.0.0\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve( - npm.localPrefix, - 'packages/a' - /* eslint-disable-next-line max-len */ - )}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`, - ], - ], - 'printed message about interactive shell' - ) + await registry.package({ manifest, + tarballs: { + '1.0.0': path.join(npm.prefix, 'npm-exec-test'), + } }) + await npm.exec('exec', ['@npmcli/npx-test']) + const exists = await fs.stat(path.join(npm.prefix, 'workspace-a', 'npm-exec-test-success')) + t.ok(exists.isFile(), 'bin ran, creating file inside workspace') }) diff --git a/workspaces/libnpmexec/lib/cache-install-dir.js b/workspaces/libnpmexec/lib/cache-install-dir.js deleted file mode 100644 index 77466938762d0..0000000000000 --- a/workspaces/libnpmexec/lib/cache-install-dir.js +++ /dev/null @@ -1,20 +0,0 @@ -const crypto = require('crypto') - -const { resolve } = require('path') - -const cacheInstallDir = ({ npxCache, packages }) => { - if (!npxCache) { - throw new Error('Must provide a valid npxCache path') - } - - // only packages not found in ${prefix}/node_modules - return resolve(npxCache, getHash(packages)) -} - -const getHash = (packages) => - crypto.createHash('sha512') - .update(packages.sort((a, b) => a.localeCompare(b, 'en')).join('\n')) - .digest('hex') - .slice(0, 16) - -module.exports = cacheInstallDir diff --git a/workspaces/libnpmexec/lib/file-exists.js b/workspaces/libnpmexec/lib/file-exists.js index f89cfc217d61d..e5cd474dac388 100644 --- a/workspaces/libnpmexec/lib/file-exists.js +++ b/workspaces/libnpmexec/lib/file-exists.js @@ -1,23 +1,25 @@ 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 localFileExists = async (dir, binName, root = '/') => { - root = resolve(root).toLowerCase() +const fileExists = async (file) => { + try { + const res = await fs.stat(file) + return res.isFile() + } catch { + return false + } +} - 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') if (await fileExists(resolve(binDir, binName))) { return binDir } - if (path.toLowerCase() === root) { + if (path.toLowerCase() === resolve(root).toLowerCase()) { return false } } diff --git a/workspaces/libnpmexec/lib/index.js b/workspaces/libnpmexec/lib/index.js index dfe7560120702..0c66a2836aa15 100644 --- a/workspaces/libnpmexec/lib/index.js +++ b/workspaces/libnpmexec/lib/index.js @@ -1,27 +1,74 @@ -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') 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 { 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() + +// Returns the required manifest if the spec is missing from the tree +const missingFromTree = async ({ spec, tree, pacoteOpts }) => { + 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 + } + } + } + if (!manifests.get(spec.raw)) { + manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts)) + } + return manifests.get(spec.raw) + } else { + // non-registry spec, or a specific tag. Look up manifest and check + // resolved to see if it's in the tree. + if (!manifests.get(spec.raw)) { + manifests.set(spec.raw, await pacote.manifest(spec, pacoteOpts)) + } + const manifest = manifests.get(spec.raw) + 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) => { const { @@ -32,7 +79,8 @@ const exec = async (opts) => { locationMsg = undefined, globalBin = '', output, - packages: _packages = [], + // dereference values because we manipulate it later + packages: [...packages] = [], path = '.', runPath = '.', scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh', @@ -40,10 +88,7 @@ const exec = async (opts) => { ...flatOptions } = opts - // dereferences values because we manipulate it later - const packages = [..._packages] - const pathArr = [...PATH] - const _run = () => runScript({ + const run = () => runScript({ args, call, color, @@ -51,125 +96,92 @@ const exec = async (opts) => { locationMsg, output, path, - pathArr, + binPaths, runPath, 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 pacoteOpts = { ...flatOptions, perferOnline: true } + + const needPackageCommandSwap = (args.length > 0) && (packages.length === 0) 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 - } - - if (binExists) { - return await _run() + binPaths.push(globalBin) + 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) - - 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 - } + // 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, pacoteOpts }) + if (manifest) { + needInstall.push({ spec, manifest }) } - // 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]) + // Either we have a scoped package or the bin of our package we inferred + // from arg[0] is not identical to the package name + let commandManifest + if (needInstall.length === 0) { + commandManifest = await pacote.manifest(args[0], { + ...flatOptions, + preferOnline: true, + }) + } else { + commandManifest = needInstall[0].manifest + } + args[0] = getBinFromManifest(commandManifest) } - // 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, pacoteOpts }) + 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) { if (!yes) { // set -n to always say no @@ -196,15 +208,15 @@ const exec = async (opts) => { } } } - await arb.reify({ + await npxArb.reify({ ...flatOptions, add, }) } - pathArr.unshift(resolve(installDir, 'node_modules/.bin')) + binPaths.push(resolve(installDir, 'node_modules/.bin')) } - return await _run() + return await run() } module.exports = exec diff --git a/workspaces/libnpmexec/lib/run-script.js b/workspaces/libnpmexec/lib/run-script.js index 97543e6ff0d08..18dcf7d8356c5 100644 --- a/workspaces/libnpmexec/lib/run-script.js +++ b/workspaces/libnpmexec/lib/run-script.js @@ -1,5 +1,3 @@ -const { delimiter } = require('path') - const chalk = require('chalk') const ciDetect = require('@npmcli/ci-detect') const runScript = require('@npmcli/run-script') @@ -22,7 +20,7 @@ const run = async ({ locationMsg, output = () => {}, path, - pathArr, + binPaths, runPath, scriptShell, }) => { @@ -71,11 +69,9 @@ const run = async ({ // we always run in cwd, not --prefix path: runPath, stdioString: true, + binPaths, event: 'npx', args, - env: { - PATH: pathArr.join(delimiter), - }, stdio: 'inherit', }) } finally { diff --git a/workspaces/libnpmexec/package.json b/workspaces/libnpmexec/package.json index d163103ea2b0b..349e82715f9af 100644 --- a/workspaces/libnpmexec/package.json +++ b/workspaces/libnpmexec/package.json @@ -57,7 +57,8 @@ "dependencies": { "@npmcli/arborist": "^5.0.0", "@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", "mkdirp-infer-owner": "^2.0.0", "npm-package-arg": "^9.0.1", @@ -66,6 +67,7 @@ "proc-log": "^2.0.0", "read": "^1.0.7", "read-package-json-fast": "^2.0.2", + "semver": "^7.3.7", "walk-up-path": "^1.0.0" }, "templateOSS": { diff --git a/workspaces/libnpmexec/test/cache-install-dir.js b/workspaces/libnpmexec/test/cache-install-dir.js deleted file mode 100644 index 9a7aee6d7c7e3..0000000000000 --- a/workspaces/libnpmexec/test/cache-install-dir.js +++ /dev/null @@ -1,12 +0,0 @@ -const t = require('tap') - -const cacheInstallDir = require('../lib/cache-install-dir.js') - -t.test('invalid npxCache path', t => { - t.throws( - () => cacheInstallDir({}), - /Must provide a valid npxCache path/, - 'should throw invalid path error' - ) - t.end() -}) diff --git a/workspaces/libnpmexec/test/index.js b/workspaces/libnpmexec/test/index.js index d42947e83687c..0eae95910848e 100644 --- a/workspaces/libnpmexec/test/index.js +++ b/workspaces/libnpmexec/test/index.js @@ -66,7 +66,7 @@ require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') }) -t.test('local pkg, must not fetch manifest for avail pkg', async t => { +t.test('locally available pkg - by name', async t => { const pkg = { name: '@ruyadorno/create-index', version: '2.0.0', @@ -121,6 +121,186 @@ t.test('local pkg, must not fetch manifest for avail pkg', async t => { t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') }) +t.test('locally available pkg - by version', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '1.0.0', + bin: { + 'create-index': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync('resfile', 'LOCAL PKG')`, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^1.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + args: ['@ruyadorno/create-index@1.0.0'], + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + +t.test('locally available pkg - by range', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '2.0.0', + bin: { + 'create-index': './index.js', + }, + } + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + packages: ['@ruyadorno/create-index@^2.0.0'], + call: 'create-index resfile', + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + +t.test('locally available pkg - by tag', async t => { + const pkg = { + name: '@ruyadorno/create-index', + version: '1.0.0', + bin: { + 'create-index': './index.js', + }, + } + + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '.bin': {}, + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify(pkg), + 'index.js': `#!/usr/bin/env node + require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }, + '.package-lock.json': JSON.stringify({ + name: 'lock', + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/@ruyadorno/create-index': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-1.0.0.tgz', + bin: { + 'create-index': 'create-index.js', + }, + }, + }, + + }), + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^1.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + const executable = + resolve(path, 'node_modules/@ruyadorno/create-index/index.js') + fs.chmodSync(executable, 0o775) + + await binLinks({ + path: resolve(path, 'node_modules/@ruyadorno/create-index'), + pkg, + }) + + await libexec({ + ...baseOpts, + cache, + npxCache, + packages: ['@ruyadorno/create-index@latest'], + call: 'create-index resfile', + path, + runPath, + }) + + const res = fs.readFileSync(resolve(path, 'resfile')).toString() + t.equal(res, 'LOCAL PKG', 'should run local pkg bin script') +}) + t.test('multiple local pkgs', async t => { const foo = { name: '@ruyadorno/create-foo', @@ -197,6 +377,35 @@ t.test('multiple local pkgs', async t => { t.equal(resBar, 'bar', 'should run local pkg bin script') }) +t.test('no npxCache', async t => { + const path = t.testdir({ + cache: {}, + a: { + 'package.json': JSON.stringify({ + name: 'a', + bin: { + a: './index.js', + }, + }), + 'index.js': `#!/usr/bin/env node +require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`, + }, + }) + const runPath = path + const cache = resolve(path, 'cache') + + const executable = resolve(path, 'a/index.js') + fs.chmodSync(executable, 0o775) + + await t.rejects(libexec({ + ...baseOpts, + args: [`file:${resolve(path, 'a')}`, 'resfile'], + cache, + path, + runPath, + }), /Must provide a valid npxCache path/) +}) + t.test('local file system path', async t => { const path = t.testdir({ cache: {}, @@ -276,7 +485,7 @@ t.test('global space pkg', async t => { t.equal(res, 'GLOBAL PKG', 'should run local pkg bin script') }) -t.test('run from registry', async t => { +t.test('run from registry - no local packages', async t => { const testdir = t.testdir({ cache: {}, npxCache: {}, @@ -305,6 +514,132 @@ t.test('run from registry', async t => { t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg') }) +t.test('run from registry - local version mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@1.0.0'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + +t.test('run from registry - local range mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@^1.0.0'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + +t.test('run from registry - local tag mismatch', async t => { + const path = t.testdir({ + cache: {}, + npxCache: {}, + node_modules: { + '@ruyadorno': { + 'create-index': { + 'package.json': JSON.stringify({ + name: '@ruyadorno/create-index', + version: '2.0.0', + }), + }, + }, + '.package-lock.json': JSON.stringify({ + name: 'lock', + lockfileVersion: 3, + requires: true, + packages: { + 'node_modules/@ruyadorno/create-index': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-2.0.0.tgz', + bin: { + 'create-index': 'create-index.js', + }, + }, + }, + + }), + }, + 'package.json': JSON.stringify({ + name: 'pkg', + dependencies: { + '@ruyadorno/create-index': '^2.0.0', + }, + }), + }) + const runPath = path + const cache = resolve(path, 'cache') + const npxCache = resolve(path, 'npxCache') + + await libexec({ + ...baseOpts, + args: ['@ruyadorno/create-index@latest'], + cache, + npxCache, + path, + runPath, + }) + + t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg from registry') +}) + t.test('avoid install when exec from registry an available pkg', async t => { const testdir = t.testdir({ cache: {}, @@ -656,9 +991,15 @@ t.test('no prompt if CI, multiple packages', async t => { 'proc-log': { warn (title, msg) { t.equal(title, 'exec', 'should warn exec title') - const expected = 'The following packages were not found and will be ' + - 'installed: @ruyadorno/create-index@1.0.0, @ruyadorno/create-test@1.0.0' - t.equal(msg, expected, 'should warn installing pkg') + // this message is nondeterministic as it queries manifests so we just + // test the constituent parts + t.match( + msg, + 'The following packages were not found and will be installed:', + 'should warn installing packages' + ) + t.match(msg, '@ruyadorno/create-index@1.0.0', 'includes package being installed') + t.match(msg, '@ruyadorno/create-test@1.0.0', 'includes package being installed') }, }, }) @@ -675,7 +1016,7 @@ t.test('no prompt if CI, multiple packages', async t => { }) }) -t.test('sane defaults', async t => { +t.test('defaults', async t => { const testdir = t.testdir({ cache: {}, npxCache: {}, diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json index 1e85cbbabec73..6118dec7dfd87 100644 --- a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json +++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json @@ -78,4 +78,4 @@ "readmeFilename": "README.md", "_cached": false, "_contentLength": 0 -} \ No newline at end of file +} diff --git a/workspaces/libnpmexec/test/run-script.js b/workspaces/libnpmexec/test/run-script.js index 0b97a72ac04cb..9e0db1367fed1 100644 --- a/workspaces/libnpmexec/test/run-script.js +++ b/workspaces/libnpmexec/test/run-script.js @@ -5,7 +5,6 @@ const baseOpts = { call: '', color: false, path: '', - pathArr: [''], runPath: '', shell: process.platform === 'win32' ? process.env.ComSpec || 'cmd'