Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Fritz <fritzy@github.com>2021-12-16 21:01:56 +0300
committerNathan Fritz <fritzy@github.com>2021-12-16 21:05:19 +0300
commitd7265045730555c03b3142c004c7438e9577028c (patch)
tree035d81b3124bdaa09c21854934bf2b2b50e52e44 /workspaces/libnpmexec
parentd8aac8448e983692cacb427e03f4688cd1b62e30 (diff)
Bring in all libnpm modules + arborist as workspaces (#4166)
Added libnpm workspaces and arborist
Diffstat (limited to 'workspaces/libnpmexec')
-rw-r--r--workspaces/libnpmexec/.editorconfig3
-rw-r--r--workspaces/libnpmexec/.eslintrc.js14
-rw-r--r--workspaces/libnpmexec/.gitignore23
-rw-r--r--workspaces/libnpmexec/.npmrc3
-rw-r--r--workspaces/libnpmexec/CHANGELOG.md25
-rw-r--r--workspaces/libnpmexec/CONTRIBUTING.md68
-rw-r--r--workspaces/libnpmexec/LICENSE15
-rw-r--r--workspaces/libnpmexec/README.md50
-rw-r--r--workspaces/libnpmexec/SECURITY.md3
-rw-r--r--workspaces/libnpmexec/lib/cache-install-dir.js20
-rw-r--r--workspaces/libnpmexec/lib/file-exists.js31
-rw-r--r--workspaces/libnpmexec/lib/get-bin-from-manifest.js22
-rw-r--r--workspaces/libnpmexec/lib/index.js192
-rw-r--r--workspaces/libnpmexec/lib/is-windows.js1
-rw-r--r--workspaces/libnpmexec/lib/manifest-missing.js19
-rw-r--r--workspaces/libnpmexec/lib/no-tty.js1
-rw-r--r--workspaces/libnpmexec/lib/run-script.js89
-rw-r--r--workspaces/libnpmexec/package.json67
-rw-r--r--workspaces/libnpmexec/tap-snapshots/test/run-script.js.test.cjs22
-rw-r--r--workspaces/libnpmexec/test/cache-install-dir.js12
-rw-r--r--workspaces/libnpmexec/test/file-exists.js14
-rw-r--r--workspaces/libnpmexec/test/get-bin-from-manifest.js42
-rw-r--r--workspaces/libnpmexec/test/index.js747
-rw-r--r--workspaces/libnpmexec/test/manifest-missing.js32
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json81
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.min.json26
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-index/-/create-index-1.0.0.tgzbin0 -> 490 bytes
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.json81
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.min.json26
-rw-r--r--workspaces/libnpmexec/test/registry/content/ruyadorno/create-test/-/create-test-1.0.0.tgzbin0 -> 487 bytes
-rw-r--r--workspaces/libnpmexec/test/registry/server.js280
-rw-r--r--workspaces/libnpmexec/test/run-script.js156
32 files changed, 2165 insertions, 0 deletions
diff --git a/workspaces/libnpmexec/.editorconfig b/workspaces/libnpmexec/.editorconfig
new file mode 100644
index 000000000..0f3bb618c
--- /dev/null
+++ b/workspaces/libnpmexec/.editorconfig
@@ -0,0 +1,3 @@
+[*.js]
+indent_style = space
+indent_size = 2
diff --git a/workspaces/libnpmexec/.eslintrc.js b/workspaces/libnpmexec/.eslintrc.js
new file mode 100644
index 000000000..022767bc3
--- /dev/null
+++ b/workspaces/libnpmexec/.eslintrc.js
@@ -0,0 +1,14 @@
+// This file is automatically added by @npmcli/template-oss. Do not edit.
+
+const { readdirSync: readdir } = require('fs')
+
+const localConfigs = readdir(__dirname)
+ .filter((file) => file.startsWith('.eslintrc.local.'))
+ .map((file) => `./${file}`)
+
+module.exports = {
+ extends: [
+ '@npmcli',
+ ...localConfigs,
+ ],
+}
diff --git a/workspaces/libnpmexec/.gitignore b/workspaces/libnpmexec/.gitignore
new file mode 100644
index 000000000..6ed44c72b
--- /dev/null
+++ b/workspaces/libnpmexec/.gitignore
@@ -0,0 +1,23 @@
+# This file is automatically added by @npmcli/template-oss. Do not edit.
+
+# ignore everything in the root
+/*
+
+# keep these
+!/.commitlintrc.js
+!/.npmrc
+!/.eslintrc*
+!/.github
+!**/.gitignore
+!/package.json
+!/docs
+!/bin
+!/lib
+!/map.js
+!/tap-snapshots
+!/test
+!/scripts
+!/README*
+!/LICENSE*
+!/SECURITY*
+!/CHANGELOG*
diff --git a/workspaces/libnpmexec/.npmrc b/workspaces/libnpmexec/.npmrc
new file mode 100644
index 000000000..878b7ddef
--- /dev/null
+++ b/workspaces/libnpmexec/.npmrc
@@ -0,0 +1,3 @@
+;This file is automatically added by @npmcli/template-oss. Do not edit.
+
+package-lock=false
diff --git a/workspaces/libnpmexec/CHANGELOG.md b/workspaces/libnpmexec/CHANGELOG.md
new file mode 100644
index 000000000..698d1a4ef
--- /dev/null
+++ b/workspaces/libnpmexec/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+## v2.0.0
+
+- Added a new required `npxCache` option
+
+## v1.2.0
+
+- Added a default value to `scriptShell` option
+
+## v1.1.0
+
+- Add add walk up dir lookup logic to satisfy local bins,
+similar to `@npmcli/run-script`
+
+## v1.0.1
+
+- Fix `scriptShell` option name.
+
+## v1.0.0
+
+- Initial implementation, moves the code that used to live in the **npm cli**,
+ref: https://github.com/npm/cli/blob/release/v7.10.0/lib/exec.js into this
+separate module, providing a programmatic API to the **npm exec** functionality.
+
diff --git a/workspaces/libnpmexec/CONTRIBUTING.md b/workspaces/libnpmexec/CONTRIBUTING.md
new file mode 100644
index 000000000..045ba4b2e
--- /dev/null
+++ b/workspaces/libnpmexec/CONTRIBUTING.md
@@ -0,0 +1,68 @@
+# Contributing
+## Table of Contents
+
+* [Introduction](#introduction)
+* [Running Tests](#running-tests)
+* [Coverage](#coverage)
+* [Types of Contributions](#types-of-contributions)
+ * [Contributing an Issue?](#contributing-an-issue)
+ * [Contributing a Question?](#contributing-a-question)
+ * [Contributing a Bug Fix?](#contributing-a-bug-fix)
+ * [Contributing a Feature?](#contributing-a-feature)
+* [Development Dependencies](#development-dependencies)
+* [Dependencies](#dependencies)
+
+## Introduction
+
+Welcome to the **libnpmexec** Contributor Guide! This document outlines the libnpmexec's process for community interaction and contribution. This includes the issue tracker, pull requests, wiki pages, and, to a certain extent, outside communication in the context of the libnpmexec. This is an entry point for anyone wishing to contribute their time and effort to making libnpmexec a better tool for the JavaScript community!
+
+All interactions in the **libnpmexec** repository are covered by the [npm Code of Conduct](https://www.npmjs.com/policies/conduct)
+
+
+## Running Tests
+
+```
+# Make sure you install the dependencies first before running tests.
+$ npm install
+
+# Run tests for the CLI (it could take awhile).
+$ npm run test
+```
+
+## Coverage
+
+We try and make sure that each new feature or bug fix has tests to go along with them in order to keep code coverages consistent and increasing. We are actively striving for 100% code coverage!
+
+## Types of Contributions
+
+> Before contributing something, double check the issue you're creating doesn't already exist in the repository but doing a quick search. Search of the [current issues](https://github.com/npm/libnpmexec/issues).
+
+### Contributing a Question?
+
+Huh? 🤔 Got a situation you're not sure about?! Perfect!
+
+You can create a new question [here](https://github.com/npm/libnpmexec/issues/new?template=question.md&title=%5BQUESTION%5D+%3Ctitle%3E)!
+
+### Contributing a Bug Fix?
+
+We'd be happy to triage and help! Head over to the issues and [create a new one](https://github.com/npm/libnpmexec/issues/new?template=bug.md&title=%5BBUG%5D+%3Ctitle%3E)!
+
+
+### Contributing a Feature?
+
+Snazzy, we're always up for fancy new things! If the feature is fairly minor [create a new one](https://github.com/npm/libnpmexec/issues/new?template=feature.md&title=%5BFEATURE%5D+%3Ctitle%3E), and the team can triage it and prioritize it into our backlog. However, if the feature is a little more complex, then it's best to create an [RFC](https://en.wikipedia.org/wiki/Request_for_Comments) in our [RFC repository](https://github.com/npm/rfcs). Exactly how to do that is outlined in that repository. If you're not sure _exactly_ how to implement your idea, or don't want to make a document about your idea, then please create an issue on that repository. We consider these RRFC's, or a "Requesting Request For Comment".
+
+## Development Dependencies
+
+You'll need a few things installed in order to update and test **libnpmexec** during development:
+
+
+* [node](https://nodejs.org/) v10 or greater
+
+> We recommend that you have a [node version manager](https://github.com/nvm-sh/nvm) installed if you plan on fixing bugs that might be present in a specific version of node. With a version manager you can easily switch versions of node and test if your changes to the CLI project are working.
+
+* [git](https://git-scm.com/) v2.11+
+
+
+## Dependencies
+<!-- Optional Section -->
diff --git a/workspaces/libnpmexec/LICENSE b/workspaces/libnpmexec/LICENSE
new file mode 100644
index 000000000..d3a1cdfd2
--- /dev/null
+++ b/workspaces/libnpmexec/LICENSE
@@ -0,0 +1,15 @@
+The ISC License
+
+Copyright (c) GitHub Inc.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/workspaces/libnpmexec/README.md b/workspaces/libnpmexec/README.md
new file mode 100644
index 000000000..a48552714
--- /dev/null
+++ b/workspaces/libnpmexec/README.md
@@ -0,0 +1,50 @@
+# libnpmexec
+
+[![npm version](https://img.shields.io/npm/v/libnpmexec.svg)](https://npm.im/libnpmexec)
+[![license](https://img.shields.io/npm/l/libnpmexec.svg)](https://npm.im/libnpmexec)
+[![GitHub Actions](https://github.com/npm/libnpmexec/workflows/node-ci/badge.svg)](https://github.com/npm/libnpmexec/actions?query=workflow%3Anode-ci)
+[![Coverage Status](https://coveralls.io/repos/github/npm/libnpmexec/badge.svg?branch=main)](https://coveralls.io/github/npm/libnpmexec?branch=main)
+
+The `npm exec` (`npx`) Programmatic API
+
+## Install
+
+`npm install libnpmexec`
+
+## Usage:
+
+```js
+const libexec = require('libnpmexec')
+await libexec({
+ args: ['yosay', 'Bom dia!'],
+ cache: '~/.npm/_cacache',
+ npxCache: '~/.npm/_npx',
+ yes: true,
+})
+```
+
+## API:
+
+### `libexec(opts)`
+
+- `opts`:
+ - `args`: List of pkgs to execute **Array<String>**, defaults to `[]`
+ - `call`: An alternative command to run when using `packages` option **String**, defaults to empty string.
+ - `cache`: The path location to where the npm cache folder is placed **String**
+ - `npxCache`: The path location to where the npx cache folder is placed **String**
+ - `color`: Output should use color? **Boolean**, defaults to `false`
+ - `localBin`: Location to the `node_modules/.bin` folder of the local project to start scanning for bin files **String**, defaults to `./node_modules/.bin`. **libexec** will walk up the directory structure looking for `node_modules/.bin` folders in parent folders that might satisfy the current `arg` and will use that bin if found.
+ - `locationMsg`: Overrides "at location" message when entering interactive mode **String**
+ - `log`: Sets an optional logger **Object**, defaults to `proc-log` module usage.
+ - `globalBin`: Location to the global space bin folder, same as: `$(npm bin -g)` **String**, defaults to empty string.
+ - `output`: A function to print output to **Function**
+ - `packages`: A list of packages to be used (possibly fetch from the registry) **Array<String>**, defaults to `[]`
+ - `path`: Location to where to read local project info (`package.json`) **String**, defaults to `.`
+ - `runPath`: Location to where to execute the script **String**, defaults to `.`
+ - `scriptShell`: Default shell to be used **String**, defaults to `sh` on POSIX systems, `process.env.ComSpec` OR `cmd` on Windows
+ - `yes`: Should skip download confirmation prompt when fetching missing packages from the registry? **Boolean**
+ - `registry`, `cache`, and more options that are forwarded to [@npmcli/arborist](https://github.com/npm/arborist/) and [pacote](https://github.com/npm/pacote/#options) **Object**
+
+## LICENSE
+
+[ISC](./LICENSE)
diff --git a/workspaces/libnpmexec/SECURITY.md b/workspaces/libnpmexec/SECURITY.md
new file mode 100644
index 000000000..a93106d0c
--- /dev/null
+++ b/workspaces/libnpmexec/SECURITY.md
@@ -0,0 +1,3 @@
+<!-- This file is automatically added by @npmcli/template-oss. Do not edit. -->
+
+Please send vulnerability reports through [hackerone](https://hackerone.com/github).
diff --git a/workspaces/libnpmexec/lib/cache-install-dir.js b/workspaces/libnpmexec/lib/cache-install-dir.js
new file mode 100644
index 000000000..774669387
--- /dev/null
+++ b/workspaces/libnpmexec/lib/cache-install-dir.js
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 000000000..05dddc89f
--- /dev/null
+++ b/workspaces/libnpmexec/lib/file-exists.js
@@ -0,0 +1,31 @@
+const { resolve } = require('path')
+const { promisify } = require('util')
+const stat = promisify(require('fs').stat)
+const walkUp = require('walk-up-path')
+
+const fileExists = (file) => stat(file)
+ .then((stat) => stat.isFile())
+ .catch(() => false)
+
+const localFileExists = async (dir, binName, root = '/') => {
+ root = resolve(root).toLowerCase()
+
+ for (const path of walkUp(resolve(dir))) {
+ const binDir = resolve(path, 'node_modules', '.bin')
+
+ if (await fileExists(resolve(binDir, binName))) {
+ return binDir
+ }
+
+ if (path.toLowerCase() === root) {
+ return false
+ }
+ }
+
+ return false
+}
+
+module.exports = {
+ fileExists,
+ localFileExists,
+}
diff --git a/workspaces/libnpmexec/lib/get-bin-from-manifest.js b/workspaces/libnpmexec/lib/get-bin-from-manifest.js
new file mode 100644
index 000000000..8ebc0e7a1
--- /dev/null
+++ b/workspaces/libnpmexec/lib/get-bin-from-manifest.js
@@ -0,0 +1,22 @@
+const getBinFromManifest = (mani) => {
+ // if we have a bin matching (unscoped portion of) packagename, use that
+ // otherwise if there's 1 bin or all bin value is the same (alias), use
+ // that, otherwise fail
+ const bin = mani.bin || {}
+ if (new Set(Object.values(bin)).size === 1) {
+ return Object.keys(bin)[0]
+ }
+
+ // XXX probably a util to parse this better?
+ const name = mani.name.replace(/^@[^/]+\//, '')
+ if (bin[name]) {
+ return name
+ }
+
+ // XXX need better error message
+ throw Object.assign(new Error('could not determine executable to run'), {
+ pkgid: mani._id,
+ })
+}
+
+module.exports = getBinFromManifest
diff --git a/workspaces/libnpmexec/lib/index.js b/workspaces/libnpmexec/lib/index.js
new file mode 100644
index 000000000..facafb035
--- /dev/null
+++ b/workspaces/libnpmexec/lib/index.js
@@ -0,0 +1,192 @@
+const { delimiter, dirname, resolve } = require('path')
+const { promisify } = require('util')
+const read = promisify(require('read'))
+
+const Arborist = require('@npmcli/arborist')
+const ciDetect = require('@npmcli/ci-detect')
+const logger = require('proc-log')
+const mkdirp = require('mkdirp-infer-owner')
+const npa = require('npm-package-arg')
+const pacote = require('pacote')
+const readPackageJson = require('read-package-json-fast')
+
+const cacheInstallDir = require('./cache-install-dir.js')
+const { fileExists, localFileExists } = require('./file-exists.js')
+const getBinFromManifest = require('./get-bin-from-manifest.js')
+const manifestMissing = require('./manifest-missing.js')
+const noTTY = require('./no-tty.js')
+const runScript = require('./run-script.js')
+const isWindows = require('./is-windows.js')
+
+/* istanbul ignore next */
+const PATH = (
+ process.env.PATH || process.env.Path || process.env.path
+).split(delimiter)
+
+const exec = async (opts) => {
+ const {
+ args = [],
+ call = '',
+ color = false,
+ localBin = resolve('./node_modules/.bin'),
+ locationMsg = undefined,
+ globalBin = '',
+ output,
+ packages: _packages = [],
+ path = '.',
+ runPath = '.',
+ scriptShell = isWindows ? process.env.ComSpec || 'cmd' : 'sh',
+ yes = undefined,
+ ...flatOptions
+ } = opts
+ const log = flatOptions.log || logger
+
+ // dereferences values because we manipulate it later
+ const packages = [..._packages]
+ const pathArr = [...PATH]
+ const _run = () => runScript({
+ args,
+ call,
+ color,
+ flatOptions,
+ locationMsg,
+ log,
+ output,
+ path,
+ pathArr,
+ runPath,
+ scriptShell,
+ })
+
+ // nothing to maybe install, skip the arborist dance
+ if (!call && !args.length && !packages.length) {
+ return await _run()
+ }
+
+ const needPackageCommandSwap = args.length && !packages.length
+ // if there's an argument and no package has been explicitly asked for
+ // check the local and global bin paths for a binary named the same as
+ // the argument and run it if it exists, otherwise fall through to
+ // the behavior of treating the single argument as a package name
+ if (needPackageCommandSwap) {
+ let binExists = false
+ const dir = dirname(dirname(localBin))
+ const localBinPath = await localFileExists(dir, args[0])
+ if (localBinPath) {
+ pathArr.unshift(localBinPath)
+ binExists = true
+ } else if (await fileExists(`${globalBin}/${args[0]}`)) {
+ pathArr.unshift(globalBin)
+ binExists = true
+ }
+
+ if (binExists) {
+ return await _run()
+ }
+
+ packages.push(args[0])
+ }
+
+ // If we do `npm exec foo`, and have a `foo` locally, then we'll
+ // always use that, so we don't really need to fetch the manifest.
+ // So: run npa on each packages entry, and if it is a name with a
+ // rawSpec==='', then try to readPackageJson at
+ // node_modules/${name}/package.json, and only pacote fetch if
+ // that fails.
+ const manis = await Promise.all(packages.map(async p => {
+ const spec = npa(p, path)
+ if (spec.type === 'tag' && spec.rawSpec === '') {
+ // fall through to the pacote.manifest() approach
+ try {
+ const pj = resolve(path, 'node_modules', spec.name, 'package.json')
+ return await readPackageJson(pj)
+ } catch (er) {}
+ }
+ // Force preferOnline to true so we are making sure to pull in the latest
+ // This is especially useful if the user didn't give us a version, and
+ // they expect to be running @latest
+ return await pacote.manifest(p, {
+ ...flatOptions,
+ preferOnline: true,
+ })
+ }))
+
+ if (needPackageCommandSwap) {
+ args[0] = getBinFromManifest(manis[0])
+ }
+
+ // figure out whether we need to install stuff, or if local is fine
+ const localArb = new Arborist({
+ ...flatOptions,
+ path,
+ })
+ const tree = await localArb.loadActual()
+
+ // do we have all the packages in manifest list?
+ const needInstall =
+ manis.some(manifest => manifestMissing({ tree, manifest }))
+
+ if (needInstall) {
+ const { npxCache } = flatOptions
+ const installDir = cacheInstallDir({ npxCache, packages })
+ await mkdirp(installDir)
+ const arb = new Arborist({
+ ...flatOptions,
+ path: installDir,
+ })
+ const tree = await arb.loadActual()
+
+ // at this point, we have to ensure that we get the exact same
+ // version, because it's something that has only ever been installed
+ // by npm exec in the cache install directory
+ const add = manis.filter(mani => manifestMissing({
+ tree,
+ manifest: {
+ ...mani,
+ _from: `${mani.name}@${mani.version}`,
+ },
+ }))
+ .map(mani => mani._from)
+ .sort((a, b) => a.localeCompare(b, 'en'))
+
+ // no need to install if already present
+ if (add.length) {
+ if (!yes) {
+ // set -n to always say no
+ if (yes === false) {
+ throw new Error('canceled')
+ }
+
+ if (noTTY() || ciDetect()) {
+ log.warn('exec', `The following package${
+ add.length === 1 ? ' was' : 's were'
+ } not found and will be installed: ${
+ add.map((pkg) => pkg.replace(/@$/, '')).join(', ')
+ }`)
+ } else {
+ const addList = add.map(a => ` ${a.replace(/@$/, '')}`)
+ .join('\n') + '\n'
+ const prompt = `Need to install the following packages:\n${
+ addList
+ }Ok to proceed? `
+ if (typeof log.clearProgress === 'function') {
+ log.clearProgress()
+ }
+ const confirm = await read({ prompt, default: 'y' })
+ if (confirm.trim().toLowerCase().charAt(0) !== 'y') {
+ throw new Error('canceled')
+ }
+ }
+ }
+ await arb.reify({
+ ...flatOptions,
+ add,
+ })
+ }
+ pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
+ }
+
+ return await _run()
+}
+
+module.exports = exec
diff --git a/workspaces/libnpmexec/lib/is-windows.js b/workspaces/libnpmexec/lib/is-windows.js
new file mode 100644
index 000000000..fbece90ad
--- /dev/null
+++ b/workspaces/libnpmexec/lib/is-windows.js
@@ -0,0 +1 @@
+module.exports = process.platform === 'win32'
diff --git a/workspaces/libnpmexec/lib/manifest-missing.js b/workspaces/libnpmexec/lib/manifest-missing.js
new file mode 100644
index 000000000..aec1281e3
--- /dev/null
+++ b/workspaces/libnpmexec/lib/manifest-missing.js
@@ -0,0 +1,19 @@
+const manifestMissing = ({ tree, manifest }) => {
+ // if the tree doesn't have a child by that name/version, return true
+ // true means we need to install it
+ const child = tree.children.get(manifest.name)
+ // if no child, we have to load it
+ if (!child) {
+ return true
+ }
+
+ // if no version/tag specified, allow whatever's there
+ if (manifest._from === `${manifest.name}@`) {
+ return false
+ }
+
+ // otherwise the version has to match what we WOULD get
+ return child.version !== manifest.version
+}
+
+module.exports = manifestMissing
diff --git a/workspaces/libnpmexec/lib/no-tty.js b/workspaces/libnpmexec/lib/no-tty.js
new file mode 100644
index 000000000..601798d25
--- /dev/null
+++ b/workspaces/libnpmexec/lib/no-tty.js
@@ -0,0 +1 @@
+module.exports = () => !process.stdin.isTTY
diff --git a/workspaces/libnpmexec/lib/run-script.js b/workspaces/libnpmexec/lib/run-script.js
new file mode 100644
index 000000000..851f5c60b
--- /dev/null
+++ b/workspaces/libnpmexec/lib/run-script.js
@@ -0,0 +1,89 @@
+const { delimiter } = require('path')
+
+const chalk = require('chalk')
+const ciDetect = require('@npmcli/ci-detect')
+const runScript = require('@npmcli/run-script')
+const readPackageJson = require('read-package-json-fast')
+const noTTY = require('./no-tty.js')
+
+const nocolor = {
+ reset: s => s,
+ bold: s => s,
+ dim: s => s,
+}
+
+const run = async ({
+ args,
+ call,
+ color,
+ flatOptions,
+ locationMsg,
+ log,
+ output = () => {},
+ path,
+ pathArr,
+ runPath,
+ scriptShell,
+}) => {
+ // turn list of args into command string
+ const script = call || args.shift() || scriptShell
+ const colorize = color ? chalk : nocolor
+
+ // do the fakey runScript dance
+ // still should work if no package.json in cwd
+ const realPkg = await readPackageJson(`${path}/package.json`)
+ .catch(() => ({}))
+ const pkg = {
+ ...realPkg,
+ scripts: {
+ ...(realPkg.scripts || {}),
+ npx: script,
+ },
+ }
+
+ if (log && log.disableProgress) {
+ log.disableProgress()
+ }
+
+ try {
+ if (script === scriptShell) {
+ const isTTY = !noTTY()
+
+ if (isTTY) {
+ if (ciDetect()) {
+ return log.warn('exec', 'Interactive mode disabled in CI environment')
+ }
+
+ locationMsg = locationMsg || ` at location:\n${colorize.dim(runPath)}`
+
+ output(`${
+ colorize.reset('\nEntering npm script environment')
+ }${
+ colorize.reset(locationMsg)
+ }${
+ colorize.bold('\nType \'exit\' or ^D when finished\n')
+ }`)
+ }
+ }
+ return await runScript({
+ ...flatOptions,
+ pkg,
+ banner: false,
+ // we always run in cwd, not --prefix
+ path: runPath,
+ stdioString: true,
+ event: 'npx',
+ args,
+ env: {
+ PATH: pathArr.join(delimiter),
+ },
+ stdio: 'inherit',
+ })
+ } finally {
+ if (log && log.enableProgress) {
+ log.enableProgress()
+ }
+ }
+}
+
+module.exports = run
diff --git a/workspaces/libnpmexec/package.json b/workspaces/libnpmexec/package.json
new file mode 100644
index 000000000..b21f22dd4
--- /dev/null
+++ b/workspaces/libnpmexec/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "libnpmexec",
+ "version": "3.0.2",
+ "files": [
+ "bin",
+ "lib"
+ ],
+ "main": "lib/index.js",
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16"
+ },
+ "description": "npm exec (npx) programmatic API",
+ "repository": "https://github.com/npm/libnpmexec",
+ "keywords": [
+ "npm",
+ "npmcli",
+ "libnpm",
+ "cli",
+ "workspaces",
+ "libnpmexec"
+ ],
+ "author": "GitHub Inc.",
+ "contributors": [
+ {
+ "name": "Ruy Adorno",
+ "url": "https://ruyadorno.com",
+ "twitter": "ruyadorno"
+ }
+ ],
+ "license": "ISC",
+ "scripts": {
+ "lint": "eslint '**/*.js'",
+ "posttest": "npm run lint",
+ "test": "tap",
+ "snap": "tap",
+ "preversion": "npm test",
+ "postversion": "npm publish",
+ "prepublishOnly": "git push origin --follow-tags",
+ "postlint": "npm-template-check",
+ "lintfix": "npm run lint -- --fix"
+ },
+ "tap": {
+ "color": true,
+ "check-coverage": true,
+ "files": "test/*.js"
+ },
+ "devDependencies": {
+ "bin-links": "^2.2.1",
+ "tap": "^15.0.6"
+ },
+ "dependencies": {
+ "@npmcli/arborist": "^4.0.0",
+ "@npmcli/ci-detect": "^1.3.0",
+ "@npmcli/run-script": "^2.0.0",
+ "chalk": "^4.1.0",
+ "mkdirp-infer-owner": "^2.0.0",
+ "npm-package-arg": "^8.1.2",
+ "pacote": "^12.0.0",
+ "proc-log": "^1.0.0",
+ "read": "^1.0.7",
+ "read-package-json-fast": "^2.0.2",
+ "walk-up-path": "^1.0.0"
+ },
+ "templateOSS": {
+ "version": "2.4.1"
+ }
+}
diff --git a/workspaces/libnpmexec/tap-snapshots/test/run-script.js.test.cjs b/workspaces/libnpmexec/tap-snapshots/test/run-script.js.test.cjs
new file mode 100644
index 000000000..d1d521de2
--- /dev/null
+++ b/workspaces/libnpmexec/tap-snapshots/test/run-script.js.test.cjs
@@ -0,0 +1,22 @@
+/* IMPORTANT
+ * This snapshot file is auto-generated, but designed for humans.
+ * It should be checked into source control and tracked carefully.
+ * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
+ * Make sure to inspect the output below. Do not ignore changes!
+ */
+'use strict'
+exports[`test/run-script.js TAP colorized interactive mode msg > should print colorized output 1`] = `
+
+Entering npm script environment at location:
+/foo/
+Type 'exit' or ^D when finished
+
+`
+
+exports[`test/run-script.js TAP no color interactive mode msg > should print non-colorized output 1`] = `
+
+Entering npm script environment at location:
+/foo/
+Type 'exit' or ^D when finished
+
+`
diff --git a/workspaces/libnpmexec/test/cache-install-dir.js b/workspaces/libnpmexec/test/cache-install-dir.js
new file mode 100644
index 000000000..9a7aee6d7
--- /dev/null
+++ b/workspaces/libnpmexec/test/cache-install-dir.js
@@ -0,0 +1,12 @@
+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/file-exists.js b/workspaces/libnpmexec/test/file-exists.js
new file mode 100644
index 000000000..9a1f53ccf
--- /dev/null
+++ b/workspaces/libnpmexec/test/file-exists.js
@@ -0,0 +1,14 @@
+const t = require('tap')
+const { localFileExists } = require('../lib/file-exists.js')
+
+t.test('missing root value', async t => {
+ const dir = t.testdir({
+ b: {
+ c: {},
+ },
+ })
+
+ // root value a is not part of the file system hierarchy
+ const fileExists = await localFileExists(dir, 'foo', 'a')
+ t.equal(fileExists, false, 'should return false on missing root')
+})
diff --git a/workspaces/libnpmexec/test/get-bin-from-manifest.js b/workspaces/libnpmexec/test/get-bin-from-manifest.js
new file mode 100644
index 000000000..24e7dc8e2
--- /dev/null
+++ b/workspaces/libnpmexec/test/get-bin-from-manifest.js
@@ -0,0 +1,42 @@
+const t = require('tap')
+
+const getBinFromManifest = require('../lib/get-bin-from-manifest.js')
+
+t.test('extract scope from manifest name with multiple bins', t => {
+ const bin = getBinFromManifest({
+ name: '@npmcli/foo',
+ bin: {
+ foo: 'foo',
+ bar: 'bar',
+ },
+ })
+
+ t.equal(bin, 'foo', 'should pick same name as package')
+ t.end()
+})
+
+t.test('can not figure out what executable to run', t => {
+ t.throws(
+ () => getBinFromManifest({
+ name: 'lorem',
+ bin: {
+ foo: 'foo',
+ bar: 'bar',
+ },
+ }),
+ /could not determine executable to run/,
+ 'should throw executable to run'
+ )
+ t.end()
+})
+
+t.test('no bin value', t => {
+ t.throws(
+ () => getBinFromManifest({
+ name: 'foo',
+ }),
+ /could not determine executable to run/,
+ 'should throw executable to run on missing bin'
+ )
+ t.end()
+})
diff --git a/workspaces/libnpmexec/test/index.js b/workspaces/libnpmexec/test/index.js
new file mode 100644
index 000000000..cbd3eda72
--- /dev/null
+++ b/workspaces/libnpmexec/test/index.js
@@ -0,0 +1,747 @@
+const fs = require('fs')
+const { resolve } = require('path')
+const t = require('tap')
+const binLinks = require('bin-links')
+
+const libexec = require('../lib/index.js')
+
+// setup server
+const registryServer = require('./registry/server.js')
+const { registry } = registryServer
+t.test('setup server', { bail: true, buffered: false }, registryServer)
+
+const baseOpts = {
+ audit: false,
+ call: '',
+ color: false,
+ localBin: '',
+ globalBin: '',
+ packages: [],
+ path: '',
+ registry,
+ runPath: '',
+ scriptShell: undefined,
+ yes: true,
+}
+
+t.test('local pkg', async t => {
+ const pkg = {
+ name: 'pkg',
+ bin: {
+ a: 'index.js',
+ },
+ }
+ const path = t.testdir({
+ cache: {},
+ npxCache: {},
+ node_modules: {
+ '.bin': {},
+ a: {
+ 'index.js': `#!/usr/bin/env node
+require('fs').writeFileSync(process.argv.slice(2)[0], 'LOCAL PKG')`,
+ },
+ },
+ 'package.json': JSON.stringify(pkg),
+ })
+ const localBin = resolve(path, 'node_modules/.bin')
+ const runPath = path
+
+ const executable = resolve(path, 'node_modules/a')
+ fs.chmodSync(executable, 0o775)
+
+ await binLinks({
+ path: resolve(path, 'node_modules/a'),
+ pkg,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['a', 'resfile'],
+ localBin,
+ path,
+ runPath,
+ })
+
+ const res = fs.readFileSync(resolve(path, 'resfile')).toString()
+ t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
+})
+
+t.test('local pkg, must not fetch manifest for avail pkg', 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'],
+ 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('local file system path', async t => {
+ const path = t.testdir({
+ cache: {},
+ npxCache: {},
+ 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 npxCache = resolve(path, 'npxCache')
+
+ const executable = resolve(path, 'a/index.js')
+ fs.chmodSync(executable, 0o775)
+
+ await libexec({
+ ...baseOpts,
+ args: [`file:${resolve(path, 'a')}`, 'resfile'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ const res = fs.readFileSync(resolve(path, 'resfile')).toString()
+ t.equal(res, 'LOCAL PKG', 'should run local pkg bin script')
+})
+
+t.test('global space pkg', async t => {
+ const pkg = {
+ name: 'a',
+ bin: {
+ a: 'index.js',
+ },
+ }
+ const path = t.testdir({
+ cache: {},
+ npxCache: {},
+ global: {
+ node_modules: {
+ '.bin': {},
+ a: {
+ 'index.js': `#!/usr/bin/env node
+ require('fs').writeFileSync(process.argv.slice(2)[0], 'GLOBAL PKG')`,
+ 'package.json': JSON.stringify(pkg),
+ },
+ },
+ },
+ })
+ const globalBin = resolve(path, 'global/node_modules/.bin')
+ const runPath = path
+
+ const executable = resolve(path, 'global/node_modules/a')
+ fs.chmodSync(executable, 0o775)
+
+ await binLinks({
+ path: resolve(path, 'global/node_modules/a'),
+ pkg,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['a', 'resfile'],
+ globalBin,
+ path,
+ runPath,
+ })
+
+ const res = fs.readFileSync(resolve(path, 'resfile')).toString()
+ t.equal(res, 'GLOBAL PKG', 'should run local pkg bin script')
+})
+
+t.test('run from registry', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+
+ t.throws(
+ () => fs.statSync(resolve(path, 'index.js')),
+ { code: 'ENOENT' },
+ 'should not have template file'
+ )
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg')
+})
+
+t.test('avoid install when exec from registry an available pkg', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+
+ t.throws(
+ () => fs.statSync(resolve(path, 'index.js')),
+ { code: 'ENOENT' },
+ 'should not have template file'
+ )
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg')
+ fs.unlinkSync(resolve(path, 'index.js'))
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran create pkg again')
+})
+
+t.test('run multiple from registry', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+
+ t.throws(
+ () => fs.statSync(resolve(path, 'index.js')),
+ { code: 'ENOENT' },
+ 'should not have index template file'
+ )
+
+ t.throws(
+ () => fs.statSync(resolve(path, 'test.js')),
+ { code: 'ENOENT' },
+ 'should not have test template file'
+ )
+
+ await libexec({
+ ...baseOpts,
+ packages: ['@ruyadorno/create-test', '@ruyadorno/create-index'],
+ call: ['create-test && create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ t.ok(fs.statSync(resolve(path, 'index.js')).isFile(), 'ran index pkg')
+ t.ok(fs.statSync(resolve(path, 'test.js')).isFile(), 'ran test pkg')
+})
+
+t.test('no args', async t => {
+ const path = t.testdir({})
+ const runPath = path
+ const libexec = t.mock('../lib/index.js', {
+ '../lib/run-script': ({ args }) => {
+ t.ok(args.length === 0, 'should call run-script with no args')
+ },
+ })
+
+ await libexec({
+ ...baseOpts,
+ path,
+ runPath,
+ })
+})
+
+t.test('prompt, accepts', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ t.test('with clearProgress function', async t => {
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => false,
+ 'proc-log': {
+ clearProgress () {
+ t.ok(true, 'should call clearProgress function')
+ },
+ },
+ read (opts, cb) {
+ cb(null, 'y')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ })
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+ t.ok(fs.statSync(installedDir).isFile(), 'installed required packages')
+ })
+
+ t.test('without clearProgress function', async t => {
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => false,
+ 'proc-log': {},
+ read (opts, cb) {
+ cb(null, 'y')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ })
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+ t.ok(fs.statSync(installedDir).isFile(), 'installed required packages')
+ })
+})
+
+t.test('prompt, refuses', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ t.test('with clearProgress function', async t => {
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => false,
+ 'proc-log': {
+ clearProgress () {
+ t.ok(true, 'should call clearProgress function')
+ },
+ },
+ read (opts, cb) {
+ cb(null, 'n')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ await t.rejects(
+ libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ }),
+ /canceled/,
+ 'should throw with canceled error'
+ )
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+
+ t.throws(
+ () => fs.statSync(installedDir),
+ { code: 'ENOENT' },
+ 'should not have installed required packages'
+ )
+ })
+
+ t.test('without clearProgress function', async t => {
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => false,
+ 'proc-log': {},
+ read (opts, cb) {
+ cb(null, 'n')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ await t.rejects(
+ libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ }),
+ /canceled/,
+ 'should throw with canceled error'
+ )
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+
+ t.throws(
+ () => fs.statSync(installedDir),
+ { code: 'ENOENT' },
+ 'should not have installed required packages'
+ )
+ })
+})
+
+t.test('prompt, -n', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+
+ await t.rejects(
+ libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: false,
+ }),
+ /canceled/,
+ 'should throw with canceled error'
+ )
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+
+ t.throws(
+ () => fs.statSync(installedDir),
+ { code: 'ENOENT' },
+ 'should not have installed required packages'
+ )
+})
+
+t.test('no prompt if no tty', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ const libexec = t.mock('../lib/index.js', {
+ '../lib/no-tty.js': () => true,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ })
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+ t.ok(fs.statSync(installedDir).isFile(), 'installed required packages')
+})
+
+t.test('no prompt if CI', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => true,
+ })
+
+ await libexec({
+ ...baseOpts,
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ })
+
+ const installedDir = resolve(npxCache,
+ '0e8e15840a234288/node_modules/@ruyadorno/create-index/package.json')
+ t.ok(fs.statSync(installedDir).isFile(), 'installed required packages')
+})
+
+t.test('no prompt if CI, multiple packages', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const path = resolve(testdir, 'work')
+ const runPath = path
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ const libexec = t.mock('../lib/index.js', {
+ '@npmcli/ci-detect': () => true,
+ '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, @ruyadorno/create-test'
+ t.equal(msg, expected, 'should warn installing pkg')
+ },
+ },
+ })
+
+ await libexec({
+ ...baseOpts,
+ call: 'create-index',
+ packages: ['@ruyadorno/create-index', '@ruyadorno/create-test'],
+ cache,
+ npxCache,
+ path,
+ runPath,
+ yes: undefined,
+ })
+})
+
+t.test('sane defaults', async t => {
+ const testdir = t.testdir({
+ cache: {},
+ npxCache: {},
+ work: {},
+ })
+ const cache = resolve(testdir, 'cache')
+ const npxCache = resolve(testdir, 'npxCache')
+ const workdir = resolve(testdir, 'work')
+
+ const cwd = process.cwd()
+ process.chdir(workdir)
+ t.teardown(() => {
+ process.chdir(cwd)
+ })
+
+ await libexec({
+ args: ['@ruyadorno/create-index'],
+ cache,
+ npxCache,
+ yes: true,
+ })
+
+ t.ok(fs.statSync(resolve(workdir, 'index.js')).isFile(),
+ 'ran create-index pkg')
+})
+
+t.test('scriptShell default value', t => {
+ t.test('/bin/sh platforms', t => {
+ t.plan(1)
+ const libexec = t.mock('../lib/index.js', {
+ '../lib/is-windows.js': false,
+ '../lib/run-script.js': (opt) => {
+ t.equal(opt.scriptShell, 'sh', 'should use expected shell value')
+ },
+ })
+ libexec({ args: [], runPath: t.testDirName })
+ })
+
+ t.test('win32 defined ComSpec env var', t => {
+ t.plan(1)
+ const comspec = process.env.ComSpec
+ process.env.ComSpec = 'CMD'
+ const libexec = t.mock('../lib/index.js', {
+ '../lib/is-windows.js': true,
+ '../lib/run-script.js': ({ scriptShell }) => {
+ t.equal(scriptShell, 'CMD', 'should use expected ComSpec value')
+ process.env.ComSpec = comspec
+ },
+ })
+ libexec({ args: [], runPath: t.testDirName })
+ })
+
+ t.test('win32 cmd', t => {
+ t.plan(1)
+ const comspec = process.env.ComSpec
+ process.env.ComSpec = ''
+ const libexec = t.mock('../lib/index.js', {
+ '../lib/is-windows.js': true,
+ '../lib/run-script.js': ({ scriptShell }) => {
+ t.equal(scriptShell, 'cmd', 'should use expected cmd default value')
+ process.env.ComSpec = comspec
+ },
+ })
+ libexec({ args: [], runPath: t.testDirName })
+ })
+
+ t.end()
+})
+
+t.test('workspaces', 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('resfile', 'LOCAL PKG')`,
+ },
+ },
+ a: t.fixture('symlink', '../a'),
+ },
+ 'package.json': JSON.stringify({
+ name: 'project',
+ workspaces: ['a'],
+ }),
+ a: {
+ 'package.json': JSON.stringify({
+ name: 'a',
+ version: '1.0.0',
+ 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,
+ })
+
+ // runs at the project level
+ await libexec({
+ ...baseOpts,
+ args: ['create-index'],
+ localBin: resolve(path, 'node_modules/.bin'),
+ cache,
+ npxCache,
+ path,
+ runPath,
+ })
+
+ const res = fs.readFileSync(resolve(path, 'resfile')).toString()
+ t.equal(res, 'LOCAL PKG', 'should run existing bin from project level')
+
+ // runs at the child workspace level
+ await libexec({
+ ...baseOpts,
+ args: ['create-index'],
+ cache,
+ npxCache,
+ localBin: resolve(path, 'a/node_modules/.bin'),
+ path: resolve(path, 'a'),
+ runPath: resolve(path, 'a'),
+ })
+
+ const wRes = fs.readFileSync(resolve(path, 'a/resfile')).toString()
+ t.equal(wRes, 'LOCAL PKG', 'should run existing bin from workspace level')
+})
diff --git a/workspaces/libnpmexec/test/manifest-missing.js b/workspaces/libnpmexec/test/manifest-missing.js
new file mode 100644
index 000000000..e7ce1c851
--- /dev/null
+++ b/workspaces/libnpmexec/test/manifest-missing.js
@@ -0,0 +1,32 @@
+const t = require('tap')
+const Arborist = require('@npmcli/arborist')
+
+const manifestMissing = require('../lib/manifest-missing.js')
+
+t.test('missing version', async t => {
+ const path = t.testdir({
+ node_modules: {
+ a: {
+ 'package.json': JSON.stringify({
+ name: 'a',
+ version: '1.0.0',
+ }),
+ },
+ },
+ 'package.json': JSON.stringify({
+ name: 'root',
+ dependencies: {
+ a: '^1.0.0',
+ },
+ }),
+ })
+ const arb = new Arborist({
+ path,
+ })
+ const tree = await arb.loadActual()
+ const manifest = {
+ name: 'a',
+ _from: 'a@',
+ }
+ t.notOk(manifestMissing({ tree, manifest }), 'manifest not missing')
+})
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json
new file mode 100644
index 000000000..1e85cbbab
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.json
@@ -0,0 +1,81 @@
+{
+ "_id": "@ruyadorno/create-index",
+ "name": "@ruyadorno/create-index",
+ "dist-tags": {
+ "latest": "1.0.0"
+ },
+ "versions": {
+ "1.0.0": {
+ "name": "@ruyadorno/create-index",
+ "version": "1.0.0",
+ "description": "Create an empty index.js file",
+ "bin": {
+ "create-index": "create-index.js"
+ },
+ "keywords": [
+ "init",
+ "create",
+ "index"
+ ],
+ "author": {
+ "name": "Ruy Adorno",
+ "url": "https://ruyadorno.com"
+ },
+ "license": "MIT",
+ "gitHead": "0c1b6a4c503d8565439b2b194b4691824a1bc902",
+ "_id": "@ruyadorno/create-index@1.0.0",
+ "_nodeVersion": "15.13.0",
+ "_npmVersion": "7.9.0",
+ "dist": {
+ "integrity": "sha512-2T2JRYWtB9/wN8Vr/SRDcjIbKD5IjR5joO8iCCCYjXfDRZ2lYBSnZQ2kGp34F+T8OEavzJfj9sxNt9Y7QT7Oaw==",
+ "shasum": "a7d15d2ca78c496685b7b2bc24599d4e0983783c",
+ "tarball": "https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-1.0.0.tgz",
+ "fileCount": 3,
+ "unpackedSize": 565,
+ "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgd0fRCRA9TVsSAnZWagAA9ysP/i22HySX0+RYcHUldWcv\neDgd24/wQqNEsQiTrGpIRSSorqbrC5+xoZfFzbvbUA24JaFChgQE1rRtYDab\ntjo5asfUqCspru1X05D3T3lmy3NyBCShqzwsZo88stj8L1w8DcnmU83als4h\n6DqxmwQbPMn+hd5gKtr6ZsUwHZRc/9dXWjn6GI3ztAla73RKXQ4D9Gs/ULyo\nNwS6a/CqThqu4atlA6ZGXum72XsFYSRB712N3Q1l0+8T9L3lAWuitGx/K8L/\n95gxU0e6ME+Wiin62SxH6QYWuVIKD04UNkz14dzfI2RIjT2NDbX6l308uSza\nbWz6aro4w9kUJviDX/hk/o469d+EQ87L+vpFrLDbSfZg8RtvSptHCDdM6mNw\n05xNFji33ujMX54HyGxplioAgnE5X2ZTQuBymsiINHq5gxCn8MSaUxiX45yB\n7Bhf1rWbp5KgiUa0kGXV4eoAutP6HWs1avzkHi9q2xS61wMBdPPHX5GsTTqe\nI+4mdgpNOdQLQjLyCp+ydvSqTHtVHkHDrBJzgkOjDWC7YzDcbzFQt6Fn6uc/\nA4kTlU1yTD2lPz9ICNI6BwqM7aOa9qCVkBL7vWaUUpxblRpzfbKmCtEi704h\nIJ6YZ3z6xwTl59aMXiInOLFsb7upEwtTXTAWqDlsJmTYS7hsVi3gY7wqYp1p\nMwwm\r\n=rrJ8\r\n-----END PGP SIGNATURE-----\r\n"
+ },
+ "_npmUser": {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ },
+ "directories": {},
+ "maintainers": [
+ {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ }
+ ],
+ "_npmOperationalInternal": {
+ "host": "s3://npm-registry-packages",
+ "tmp": "tmp/create-index_1.0.0_1618429905498_0.11104270815832784"
+ },
+ "_hasShrinkwrap": false
+ }
+ },
+ "time": {
+ "created": "2021-04-14T19:51:45.442Z",
+ "1.0.0": "2021-04-14T19:51:45.650Z",
+ "modified": "2021-04-14T19:51:47.833Z"
+ },
+ "maintainers": [
+ {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ }
+ ],
+ "description": "Create an empty index.js file",
+ "keywords": [
+ "init",
+ "create",
+ "index"
+ ],
+ "author": {
+ "name": "Ruy Adorno",
+ "url": "https://ruyadorno.com"
+ },
+ "license": "MIT",
+ "readme": "# create-index\n\nPuts an empty `index.js` into current working dir. Meant for testing only.\n\n## Usage\n\n`npm exec @ruyadorno/create-index`\n\n",
+ "readmeFilename": "README.md",
+ "_cached": false,
+ "_contentLength": 0
+} \ No newline at end of file
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.min.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.min.json
new file mode 100644
index 000000000..e4e998317
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index.min.json
@@ -0,0 +1,26 @@
+{
+ "name": "@ruyadorno/create-index",
+ "dist-tags": {
+ "latest": "1.0.0"
+ },
+ "versions": {
+ "1.0.0": {
+ "name": "@ruyadorno/create-index",
+ "version": "1.0.0",
+ "bin": {
+ "create-index": "create-index.js"
+ },
+ "dist": {
+ "integrity": "sha512-2T2JRYWtB9/wN8Vr/SRDcjIbKD5IjR5joO8iCCCYjXfDRZ2lYBSnZQ2kGp34F+T8OEavzJfj9sxNt9Y7QT7Oaw==",
+ "shasum": "a7d15d2ca78c496685b7b2bc24599d4e0983783c",
+ "tarball": "https://registry.npmjs.org/@ruyadorno/create-index/-/create-index-1.0.0.tgz",
+ "fileCount": 3,
+ "unpackedSize": 565,
+ "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgd0fRCRA9TVsSAnZWagAA9ysP/i22HySX0+RYcHUldWcv\neDgd24/wQqNEsQiTrGpIRSSorqbrC5+xoZfFzbvbUA24JaFChgQE1rRtYDab\ntjo5asfUqCspru1X05D3T3lmy3NyBCShqzwsZo88stj8L1w8DcnmU83als4h\n6DqxmwQbPMn+hd5gKtr6ZsUwHZRc/9dXWjn6GI3ztAla73RKXQ4D9Gs/ULyo\nNwS6a/CqThqu4atlA6ZGXum72XsFYSRB712N3Q1l0+8T9L3lAWuitGx/K8L/\n95gxU0e6ME+Wiin62SxH6QYWuVIKD04UNkz14dzfI2RIjT2NDbX6l308uSza\nbWz6aro4w9kUJviDX/hk/o469d+EQ87L+vpFrLDbSfZg8RtvSptHCDdM6mNw\n05xNFji33ujMX54HyGxplioAgnE5X2ZTQuBymsiINHq5gxCn8MSaUxiX45yB\n7Bhf1rWbp5KgiUa0kGXV4eoAutP6HWs1avzkHi9q2xS61wMBdPPHX5GsTTqe\nI+4mdgpNOdQLQjLyCp+ydvSqTHtVHkHDrBJzgkOjDWC7YzDcbzFQt6Fn6uc/\nA4kTlU1yTD2lPz9ICNI6BwqM7aOa9qCVkBL7vWaUUpxblRpzfbKmCtEi704h\nIJ6YZ3z6xwTl59aMXiInOLFsb7upEwtTXTAWqDlsJmTYS7hsVi3gY7wqYp1p\nMwwm\r\n=rrJ8\r\n-----END PGP SIGNATURE-----\r\n"
+ }
+ }
+ },
+ "modified": "2021-04-14T19:51:47.833Z",
+ "_cached": false,
+ "_contentLength": 1423
+} \ No newline at end of file
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index/-/create-index-1.0.0.tgz b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index/-/create-index-1.0.0.tgz
new file mode 100644
index 000000000..d6ddf7570
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-index/-/create-index-1.0.0.tgz
Binary files differ
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.json
new file mode 100644
index 000000000..d193b83cf
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.json
@@ -0,0 +1,81 @@
+{
+ "_id": "@ruyadorno/create-test",
+ "name": "@ruyadorno/create-test",
+ "dist-tags": {
+ "latest": "1.0.0"
+ },
+ "versions": {
+ "1.0.0": {
+ "name": "@ruyadorno/create-test",
+ "version": "1.0.0",
+ "description": "Creates an empty test.js file",
+ "bin": {
+ "create-test": "create-test.js"
+ },
+ "keywords": [
+ "init",
+ "create",
+ "test"
+ ],
+ "author": {
+ "name": "Ruy Adorno",
+ "url": "https://ruyadorno.com"
+ },
+ "license": "MIT",
+ "gitHead": "707aa293e34f48dcf9cb6b4b452cb1fc8e484c8b",
+ "_id": "@ruyadorno/create-test@1.0.0",
+ "_nodeVersion": "15.13.0",
+ "_npmVersion": "7.9.0",
+ "dist": {
+ "integrity": "sha512-WOifELHCU8nmg0yHsPbSETPaNO1orDPhTSflJsomqGFNwVS44qvkWwMPbDE3L2aAglXLf5AxUznyFkxsXgDF2w==",
+ "shasum": "f0f393449fe5205c54a4ca2181d8355d2372da93",
+ "tarball": "https://registry.npmjs.org/@ruyadorno/create-test/-/create-test-1.0.0.tgz",
+ "fileCount": 3,
+ "unpackedSize": 557,
+ "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgd1SICRA9TVsSAnZWagAAOa4P/jknjgmFaBWz6PCng8qV\nsdfa23GbE4MdmvpB72v6DvNjGQ+51Vgd7PBAJRo+d3LmQ0c2DE/e6PZEgam/\nOtuVbgimxPy85V1MTA66bgML4nFtEOKS/R/Z5s7wMMCrhYqKdMp6ELMUEO07\n7cDIzAmc7WeSLyzhTBC661T0nKPPAf2nKKYXLI+6RisQoXnEgZmgNyNlIt6D\nNDNTRZjaR6s1QvHgWN9h/hLAgKvgaAnSy+JOzcB+SGaClLow3svbvW+klQpA\n8afOTLV4D/pgPDGXvvwDDInH6yccYSOSNiAZgd45hsmo82xIR3n+Cod2qk9Y\njCye36nXzdQTz9A7a3SgH++DV7fA5n87GoahkpGEnKu8gjgMuE6ncDEypbTi\nM4R8JikZrScR2wWXtO+jK4f/5XHVh19ZpqdOrlxzXutkUy0/bMoHFNxcGrXB\n5D1Qk/lOpNO4rd0NoURk6OkpueHOlBHlBNxqrEsltzY2IWs+JICcFaz385H0\nKYyNQrmltEqWVgW+LeFvm3B1sLL5wySqplX/396lC6kCHZyofeeqZFcC1G+m\ntkp0iho63tlm6WjIzw6ddHWu8olNohCk4xFpvNkkZ0u9GR4BaDBRXS60AcoD\nNIYwMuUlqmXAc7ey+xNZCqXokgbtjD7aI2uIDLNUMHELxrRzBccHe76sIQit\nBeBy\r\n=u7P4\r\n-----END PGP SIGNATURE-----\r\n"
+ },
+ "_npmUser": {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ },
+ "directories": {},
+ "maintainers": [
+ {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ }
+ ],
+ "_npmOperationalInternal": {
+ "host": "s3://npm-registry-packages",
+ "tmp": "tmp/create-test_1.0.0_1618433159830_0.5969279363908722"
+ },
+ "_hasShrinkwrap": false
+ }
+ },
+ "time": {
+ "created": "2021-04-14T20:45:59.789Z",
+ "1.0.0": "2021-04-14T20:45:59.974Z",
+ "modified": "2021-04-14T20:46:02.139Z"
+ },
+ "maintainers": [
+ {
+ "name": "ruyadorno",
+ "email": "ruyadorno@hotmail.com"
+ }
+ ],
+ "description": "Creates an empty test.js file",
+ "keywords": [
+ "init",
+ "create",
+ "test"
+ ],
+ "author": {
+ "name": "Ruy Adorno",
+ "url": "https://ruyadorno.com"
+ },
+ "license": "MIT",
+ "readme": "# create-test\n\nPuts an empty `test.js` into current working dir. Meant for testing only.\n\n## Usage\n\n`npm exec @ruyadorno/create-test`\n\n",
+ "readmeFilename": "README.md",
+ "_cached": false,
+ "_contentLength": 0
+} \ No newline at end of file
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.min.json b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.min.json
new file mode 100644
index 000000000..cc1508415
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test.min.json
@@ -0,0 +1,26 @@
+{
+ "name": "@ruyadorno/create-test",
+ "dist-tags": {
+ "latest": "1.0.0"
+ },
+ "versions": {
+ "1.0.0": {
+ "name": "@ruyadorno/create-test",
+ "version": "1.0.0",
+ "bin": {
+ "create-test": "create-test.js"
+ },
+ "dist": {
+ "integrity": "sha512-WOifELHCU8nmg0yHsPbSETPaNO1orDPhTSflJsomqGFNwVS44qvkWwMPbDE3L2aAglXLf5AxUznyFkxsXgDF2w==",
+ "shasum": "f0f393449fe5205c54a4ca2181d8355d2372da93",
+ "tarball": "https://registry.npmjs.org/@ruyadorno/create-test/-/create-test-1.0.0.tgz",
+ "fileCount": 3,
+ "unpackedSize": 557,
+ "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJgd1SICRA9TVsSAnZWagAAOa4P/jknjgmFaBWz6PCng8qV\nsdfa23GbE4MdmvpB72v6DvNjGQ+51Vgd7PBAJRo+d3LmQ0c2DE/e6PZEgam/\nOtuVbgimxPy85V1MTA66bgML4nFtEOKS/R/Z5s7wMMCrhYqKdMp6ELMUEO07\n7cDIzAmc7WeSLyzhTBC661T0nKPPAf2nKKYXLI+6RisQoXnEgZmgNyNlIt6D\nNDNTRZjaR6s1QvHgWN9h/hLAgKvgaAnSy+JOzcB+SGaClLow3svbvW+klQpA\n8afOTLV4D/pgPDGXvvwDDInH6yccYSOSNiAZgd45hsmo82xIR3n+Cod2qk9Y\njCye36nXzdQTz9A7a3SgH++DV7fA5n87GoahkpGEnKu8gjgMuE6ncDEypbTi\nM4R8JikZrScR2wWXtO+jK4f/5XHVh19ZpqdOrlxzXutkUy0/bMoHFNxcGrXB\n5D1Qk/lOpNO4rd0NoURk6OkpueHOlBHlBNxqrEsltzY2IWs+JICcFaz385H0\nKYyNQrmltEqWVgW+LeFvm3B1sLL5wySqplX/396lC6kCHZyofeeqZFcC1G+m\ntkp0iho63tlm6WjIzw6ddHWu8olNohCk4xFpvNkkZ0u9GR4BaDBRXS60AcoD\nNIYwMuUlqmXAc7ey+xNZCqXokgbtjD7aI2uIDLNUMHELxrRzBccHe76sIQit\nBeBy\r\n=u7P4\r\n-----END PGP SIGNATURE-----\r\n"
+ }
+ }
+ },
+ "modified": "2021-04-14T20:46:02.139Z",
+ "_cached": false,
+ "_contentLength": 1417
+} \ No newline at end of file
diff --git a/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test/-/create-test-1.0.0.tgz b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test/-/create-test-1.0.0.tgz
new file mode 100644
index 000000000..34a857020
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/content/ruyadorno/create-test/-/create-test-1.0.0.tgz
Binary files differ
diff --git a/workspaces/libnpmexec/test/registry/server.js b/workspaces/libnpmexec/test/registry/server.js
new file mode 100644
index 000000000..1db583a93
--- /dev/null
+++ b/workspaces/libnpmexec/test/registry/server.js
@@ -0,0 +1,280 @@
+const { join, dirname } = require('path')
+const { existsSync, readFileSync, writeFileSync } = require('fs')
+const PORT = 12345 + (+process.env.TAP_CHILD_ID || 0)
+const http = require('http')
+const https = require('https')
+
+const mkdirp = require('mkdirp')
+const doProxy = process.env.ARBORIST_TEST_PROXY
+const missing = /\/@isaacs(\/|%2[fF])(this-does-not-exist-at-all|testing-missing-tgz\/-\/)/
+const corgiDoc = 'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*'
+const { gzipSync, unzipSync } = require('zlib')
+
+let advisoryBulkResponse = null
+let failAdvisoryBulk = false
+let auditResponse = null
+let failAudit = false
+const startServer = cb => {
+ const server = module.exports.server = http.createServer((req, res) => {
+ res.setHeader('connection', 'close')
+
+ if (req.url === '/-/npm/v1/security/advisories/bulk') {
+ const body = []
+ req.on('data', c => body.push(c))
+ req.on('end', () => {
+ res.setHeader('connection', 'close')
+ if (failAdvisoryBulk) {
+ res.statusCode = 503
+ return res.end('no advisory bulk for you')
+ }
+ if (!advisoryBulkResponse) {
+ if (auditResponse && !failAudit) {
+ // simulate what the registry does when quick audits are allowed,
+ // but advisory bulk requests are not
+ res.statusCode = 405
+ return res.end(JSON.stringify({
+ code: 'MethodNotAllowedError',
+ message: 'POST is not allowed',
+ }))
+ } else {
+ res.statusCode = 404
+ return res.end('not found')
+ }
+ }
+ if (doProxy && !existsSync(advisoryBulkResponse)) {
+ // hit the main registry, then fall back to staging for now
+ // XXX: remove this when bulk advisory endpoint pushed to production!
+ const opts = {
+ host: 'registry.npmjs.org',
+ method: req.method,
+ path: req.url,
+ headers: {
+ ...req.headers,
+ accept: '*',
+ host: 'registry.npmjs.org',
+ connection: 'close',
+ 'if-none-match': '',
+ },
+ }
+ const handleUpstream = upstream => {
+ res.statusCode = upstream.statusCode
+ if (upstream.statusCode >= 300 || upstream.statusCode < 200) {
+ console.error('UPSTREAM ERROR', upstream.statusCode)
+ return upstream.pipe(res)
+ }
+ res.setHeader('content-encoding', upstream.headers['content-encoding'])
+ const file = advisoryBulkResponse
+ console.error('PROXY', `${req.url} -> ${file} ${upstream.statusCode}`)
+ mkdirp.sync(dirname(file))
+ const data = []
+ upstream.on('end', () => {
+ const out = Buffer.concat(data)
+ const obj = JSON.parse(unzipSync(out).toString())
+ writeFileSync(file, JSON.stringify(obj, 0, 2) + '\n')
+ res.end(out)
+ })
+ upstream.on('data', c => data.push(c))
+ }
+ return https.request(opts).on('response', upstream => {
+ if (upstream.statusCode !== 200) {
+ console.error('ATTEMPTING TO PROXY FROM STAGING')
+ console.error('NOTE: THIS WILL FAIL WHEN NOT ON VPN!')
+ opts.host = 'security-microservice-3-west.npm.red'
+ opts.headers.host = opts.host
+ opts.path = '/v1/advisories/bulk'
+ https.request(opts)
+ .on('response', upstream => handleUpstream(upstream))
+ .end(Buffer.concat(body))
+ } else {
+ handleUpstream(upstream)
+ }
+ }).end(Buffer.concat(body))
+ } else {
+ res.setHeader('content-encoding', 'gzip')
+ res.end(gzipSync(readFileSync(advisoryBulkResponse)))
+ }
+ })
+ return
+ } else if (req.url === '/-/npm/v1/security/audits/quick') {
+ const body = []
+ req.on('data', c => body.push(c))
+ req.on('end', () => {
+ res.setHeader('connection', 'close')
+ if (failAudit) {
+ res.statusCode = 503
+ return res.end('no audit for you')
+ }
+ if (!auditResponse) {
+ res.statusCode = 404
+ return res.end('not found')
+ }
+ if (doProxy && !existsSync(auditResponse)) {
+ return https.request({
+ host: 'registry.npmjs.org',
+ method: req.method,
+ path: req.url,
+ headers: {
+ ...req.headers,
+ accept: '*',
+ host: 'registry.npmjs.org',
+ connection: 'close',
+ 'if-none-match': '',
+ },
+ }).on('response', upstream => {
+ res.statusCode = upstream.statusCode
+ if (upstream.statusCode >= 300 || upstream.statusCode < 200) {
+ console.error('UPSTREAM ERROR', upstream.statusCode)
+ // don't save if it's not a valid response
+ return upstream.pipe(res)
+ }
+ res.setHeader('content-encoding', upstream.headers['content-encoding'])
+ const file = auditResponse
+ console.error('PROXY', `${req.url} -> ${file} ${upstream.statusCode}`)
+ mkdirp.sync(dirname(file))
+ const data = []
+ upstream.on('end', () => {
+ const out = Buffer.concat(data)
+ // make it a bit prettier to read later
+ const obj = JSON.parse(unzipSync(out).toString())
+ writeFileSync(file, JSON.stringify(obj, 0, 2) + '\n')
+ res.end(out)
+ })
+ upstream.on('data', c => data.push(c))
+ }).end(Buffer.concat(body))
+ } else {
+ res.setHeader('content-encoding', 'gzip')
+ res.end(gzipSync(readFileSync(auditResponse)))
+ }
+ })
+ return
+ }
+
+ const f = join(__dirname, 'content', join('/', req.url.replace(/@/, '').replace(/%2f/i, '/')))
+ const isCorgi = req.headers.accept.includes('application/vnd.npm.install-v1+json')
+ const file = f + (
+ isCorgi && existsSync(`${f}.min.json`) ? '.min.json'
+ : existsSync(`${f}.json`) ? '.json'
+ : existsSync(`${f}/index.json`) ? 'index.json'
+ : ''
+ )
+
+ try {
+ const body = readFileSync(file)
+ res.setHeader('content-length', body.length)
+ res.setHeader('content-type', /\.min\.json$/.test(file) ? corgiDoc
+ : /\.json$/.test(file) ? 'application/json'
+ : 'application/octet-stream')
+ res.end(body)
+ } catch (er) {
+ // testing things going missing from the registry somehow
+ if (missing.test(req.url)) {
+ res.statusCode = 404
+ res.end('{"error": "not found"}')
+ return
+ }
+
+ if (doProxy) {
+ return https.get({
+ host: 'registry.npmjs.org',
+ path: req.url,
+ headers: {
+ ...req.headers,
+ accept: '*',
+ 'accept-encoding': 'identity',
+ host: 'registry.npmjs.org',
+ connection: 'close',
+ 'if-none-match': '',
+ },
+ }).on('response', upstream => {
+ const errorStatus =
+ upstream.statusCode >= 300 || upstream.statusCode < 200
+
+ if (errorStatus) {
+ console.error('UPSTREAM ERROR', upstream.statusCode)
+ }
+
+ const ct = upstream.headers['content-type']
+ const isJson = ct.includes('application/json')
+ const file = isJson ? f + '.json' : f
+ console.error('PROXY', `${req.url} -> ${file} ${ct}`)
+ mkdirp.sync(dirname(file))
+ const data = []
+ res.statusCode = upstream.statusCode
+ res.setHeader('content-type', ct)
+ upstream.on('end', () => {
+ console.error('ENDING', req.url)
+ const out = Buffer.concat(data)
+ if (!errorStatus) {
+ if (isJson) {
+ const obj = JSON.parse(out.toString())
+ writeFileSync(file, JSON.stringify(obj, 0, 2) + '\n')
+ const mrm = require('minify-registry-metadata')
+ const minFile = file.replace(/\.json$/, '.min.json')
+ writeFileSync(minFile, JSON.stringify(mrm(obj), 0, 2) + '\n')
+ console.error('WROTE JSONS', [file, minFile])
+ } else {
+ writeFileSync(file, out)
+ }
+ }
+ res.end(out)
+ })
+ upstream.on('data', c => data.push(c))
+ }).end()
+ }
+
+ res.statusCode = er.code === 'ENOENT' ? 404 : 500
+ if (res.method === 'GET') {
+ console.error(er)
+ }
+ res.setHeader('content-type', 'text/plain')
+ res.end(er.stack)
+ }
+ })
+ server.listen(PORT, cb)
+}
+
+module.exports = t => startServer(() => {
+ t.parent.teardown(() => module.exports.server.close())
+ t.end()
+})
+
+module.exports.auditResponse = value => {
+ if (auditResponse && auditResponse !== value) {
+ throw new Error('setting audit response, but already set\n' +
+ '(did you forget to call the returned function on teardown?)')
+ }
+ auditResponse = value
+ return () => auditResponse = null
+}
+module.exports.failAudit = () => {
+ failAudit = true
+ return () => failAudit = false
+}
+
+module.exports.advisoryBulkResponse = value => {
+ if (advisoryBulkResponse && advisoryBulkResponse !== value) {
+ throw new Error('setting advisory bulk response, but already set\n' +
+ '(did you forget to call the returned function on teardown?)')
+ }
+ advisoryBulkResponse = value
+ return () => advisoryBulkResponse = null
+}
+module.exports.failAdvisoryBulk = () => {
+ failAdvisoryBulk = true
+ return () => failAdvisoryBulk = false
+}
+
+module.exports.registry = `http://localhost:${PORT}/`
+
+module.exports.start = startServer
+module.exports.stop = () => module.exports.server.close()
+
+if (require.main === module) {
+ startServer(() => {
+ console.log(`Mock registry live at:
+ ${module.exports.registry}
+Press ^D to close gracefully.`)
+ })
+ process.openStdin()
+ process.stdin.on('end', () => module.exports.server.close())
+}
diff --git a/workspaces/libnpmexec/test/run-script.js b/workspaces/libnpmexec/test/run-script.js
new file mode 100644
index 000000000..c86e8e151
--- /dev/null
+++ b/workspaces/libnpmexec/test/run-script.js
@@ -0,0 +1,156 @@
+const t = require('tap')
+
+const baseOpts = {
+ args: [],
+ call: '',
+ color: false,
+ log: {
+ warn () {},
+ },
+ path: '',
+ pathArr: [''],
+ runPath: '',
+ shell: process.platform === 'win32'
+ ? process.env.ComSpec || 'cmd'
+ : process.env.SHELL || 'sh',
+}
+
+t.test('disable, enable log progress', t => {
+ t.plan(3)
+
+ const path = t.testdir({
+ 'package.json': JSON.stringify({
+ name: 'pkg',
+ }),
+ })
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => false,
+ '@npmcli/run-script': async () => {
+ t.ok('should call run-script')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+ const log = {
+ ...baseOpts.log,
+ disableProgress () {
+ t.ok('should disable progress')
+ },
+ enableProgress () {
+ t.ok('should enable progress')
+ },
+ }
+
+ runScript({
+ ...baseOpts,
+ log,
+ path,
+ })
+})
+
+t.test('no package.json', t => {
+ t.plan(1)
+
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => false,
+ '@npmcli/run-script': async () => {
+ t.ok('should call run-script')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ runScript(baseOpts)
+})
+
+t.test('colorized interactive mode msg', t => {
+ t.plan(2)
+
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => false,
+ '@npmcli/run-script': async () => {
+ t.ok('should call run-script')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ const OUTPUT = []
+ runScript({
+ ...baseOpts,
+ output: msg => {
+ OUTPUT.push(msg)
+ },
+ runPath: '/foo/',
+ color: true,
+ })
+ .then(() => {
+ t.matchSnapshot(OUTPUT.join('\n'), 'should print colorized output')
+ })
+ .catch(err => {
+ throw err
+ })
+})
+
+t.test('no color interactive mode msg', t => {
+ t.plan(2)
+
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => false,
+ '@npmcli/run-script': async () => {
+ t.ok('should call run-script')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+
+ const OUTPUT = []
+ runScript({
+ ...baseOpts,
+ output: msg => {
+ OUTPUT.push(msg)
+ },
+ runPath: '/foo/',
+ })
+ .then(() => {
+ t.matchSnapshot(OUTPUT.join('\n'), 'should print non-colorized output')
+ })
+ .catch(err => {
+ throw err
+ })
+})
+
+t.test('no tty', t => {
+ t.plan(1)
+
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => false,
+ '@npmcli/run-script': async () => {
+ t.ok('should call run-script')
+ },
+ '../lib/no-tty.js': () => true,
+ })
+
+ runScript(baseOpts)
+})
+
+t.test('ci env', t => {
+ t.plan(2)
+
+ const runScript = t.mock('../lib/run-script.js', {
+ '@npmcli/ci-detect': () => true,
+ '@npmcli/run-script': async () => {
+ throw new Error('should not call run-script')
+ },
+ '../lib/no-tty.js': () => false,
+ })
+ const log = {
+ ...baseOpts.log,
+ warn (title, msg) {
+ t.equal(title, 'exec', 'should have expected title')
+ t.equal(
+ msg,
+ 'Interactive mode disabled in CI environment',
+ 'should have expected ci environment message'
+ )
+ },
+ }
+
+ runScript({ ...baseOpts, log })
+})