diff options
author | Ruy Adorno <ruyadorno@hotmail.com> | 2021-04-07 18:03:41 +0300 |
---|---|---|
committer | Ruy Adorno <ruyadorno@hotmail.com> | 2021-04-22 22:06:29 +0300 |
commit | 4c1f16d2c29a7a56c19b97f2820e6305a6075083 (patch) | |
tree | 460fd0dfa65b168d3f35bac5e0ad363b14684a1d /node_modules/libnpmexec | |
parent | 2aecec591df6866e27d0b17dc49cef8f7d738d77 (diff) |
feat: add init workspaces
Add workspaces support to `npm init`
- Fixes `npm exec` respecting `script-shell` option value
- Refactored `lib/exec.js` into `libnpmexec`
- Updates init-package-json@2.0.3
- Added ability to create a new workspace using the -w config
PR-URL: https://github.com/npm/cli/pull/3095
Credit: @ruyadorno
Close: #3095
Reviewed-by: @wraithgar
Diffstat (limited to 'node_modules/libnpmexec')
-rw-r--r-- | node_modules/libnpmexec/CHANGELOG.md | 8 | ||||
-rw-r--r-- | node_modules/libnpmexec/LICENSE | 15 | ||||
-rw-r--r-- | node_modules/libnpmexec/README.md | 48 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/cache-install-dir.js | 19 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/get-bin-from-manifest.js | 20 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/index.js | 185 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/manifest-missing.js | 17 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/no-tty.js | 1 | ||||
-rw-r--r-- | node_modules/libnpmexec/lib/run-script.js | 86 | ||||
-rw-r--r-- | node_modules/libnpmexec/package.json | 63 |
10 files changed, 462 insertions, 0 deletions
diff --git a/node_modules/libnpmexec/CHANGELOG.md b/node_modules/libnpmexec/CHANGELOG.md new file mode 100644 index 000000000..fe3ac0def --- /dev/null +++ b/node_modules/libnpmexec/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 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/node_modules/libnpmexec/LICENSE b/node_modules/libnpmexec/LICENSE new file mode 100644 index 000000000..d3a1cdfd2 --- /dev/null +++ b/node_modules/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/node_modules/libnpmexec/README.md b/node_modules/libnpmexec/README.md new file mode 100644 index 000000000..a436c9a5a --- /dev/null +++ b/node_modules/libnpmexec/README.md @@ -0,0 +1,48 @@ +# 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', + 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** + - `color`: Output should use color? **Boolean**, defaults to `false` + - `localBin`: Location to the `node_modules/.bin` folder of the local project **String**, defaults to empty string. + - `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** + - `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/node_modules/libnpmexec/lib/cache-install-dir.js b/node_modules/libnpmexec/lib/cache-install-dir.js new file mode 100644 index 000000000..1bee28989 --- /dev/null +++ b/node_modules/libnpmexec/lib/cache-install-dir.js @@ -0,0 +1,19 @@ +const crypto = require('crypto') + +const { resolve } = require('path') + +const cacheInstallDir = ({ cache, packages }) => { + if (!cache) + throw new Error('Must provide a valid cache path') + + // only packages not found in ${prefix}/node_modules + return resolve(cache, '_npx', getHash(packages)) +} + +const getHash = (packages) => + crypto.createHash('sha512') + .update(packages.sort((a, b) => a.localeCompare(b)).join('\n')) + .digest('hex') + .slice(0, 16) + +module.exports = cacheInstallDir diff --git a/node_modules/libnpmexec/lib/get-bin-from-manifest.js b/node_modules/libnpmexec/lib/get-bin-from-manifest.js new file mode 100644 index 000000000..038095b50 --- /dev/null +++ b/node_modules/libnpmexec/lib/get-bin-from-manifest.js @@ -0,0 +1,20 @@ +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/node_modules/libnpmexec/lib/index.js b/node_modules/libnpmexec/lib/index.js new file mode 100644 index 000000000..906a0b540 --- /dev/null +++ b/node_modules/libnpmexec/lib/index.js @@ -0,0 +1,185 @@ +const { delimiter, resolve } = require('path') +const { promisify } = require('util') +const read = promisify(require('read')) +const stat = promisify(require('fs').stat) + +const Arborist = require('@npmcli/arborist') +const ciDetect = require('@npmcli/ci-detect') +const logger = require('proc-log') +const mkdirp = require('mkdirp-infer-owner') +const npa = require('npm-package-arg') +const pacote = require('pacote') +const readPackageJson = require('read-package-json-fast') + +const cacheInstallDir = require('./cache-install-dir.js') +const getBinFromManifest = require('./get-bin-from-manifest.js') +const manifestMissing = require('./manifest-missing.js') +const noTTY = require('./no-tty.js') +const runScript = require('./run-script.js') + +const fileExists = (file) => stat(file) + .then((stat) => stat.isFile()) + .catch(() => false) + +/* istanbul ignore next */ +const PATH = ( + process.env.PATH || process.env.Path || process.env.path +).split(delimiter) + +const exec = async (opts) => { + const { + args = [], + call = '', + color = false, + localBin = '', + locationMsg = undefined, + globalBin = '', + output, + packages: _packages = [], + path = '.', + runPath = '.', + scriptShell = undefined, + yes = undefined, + ...flatOptions + } = opts + const log = flatOptions.log || logger + + // dereferences values because we manipulate it later + const packages = [..._packages] + const pathArr = [...PATH] + const _run = () => runScript({ + args, + call, + color, + flatOptions, + locationMsg, + log, + output, + path, + pathArr, + runPath, + scriptShell, + }) + + // nothing to maybe install, skip the arborist dance + if (!call && !args.length && !packages.length) + return await _run() + + const needPackageCommandSwap = args.length && !packages.length + // if there's an argument and no package has been explicitly asked for + // check the local and global bin paths for a binary named the same as + // the argument and run it if it exists, otherwise fall through to + // the behavior of treating the single argument as a package name + if (needPackageCommandSwap) { + let binExists = false + if (await fileExists(`${localBin}/${args[0]}`)) { + pathArr.unshift(localBin) + binExists = true + } else if (await fileExists(`${globalBin}/${args[0]}`)) { + pathArr.unshift(globalBin) + binExists = true + } + + if (binExists) + return await _run() + + packages.push(args[0]) + } + + // If we do `npm exec foo`, and have a `foo` locally, then we'll + // always use that, so we don't really need to fetch the manifest. + // So: run npa on each packages entry, and if it is a name with a + // rawSpec==='', then try to readPackageJson at + // node_modules/${name}/package.json, and only pacote fetch if + // that fails. + const manis = await Promise.all(packages.map(async p => { + const spec = npa(p, path) + if (spec.type === 'tag' && spec.rawSpec === '') { + // fall through to the pacote.manifest() approach + try { + const pj = resolve(path, 'node_modules', spec.name, 'package.json') + return await readPackageJson(pj) + } catch (er) {} + } + // Force preferOnline to true so we are making sure to pull in the latest + // This is especially useful if the user didn't give us a version, and + // they expect to be running @latest + return await pacote.manifest(p, { + ...flatOptions, + preferOnline: true, + }) + })) + + if (needPackageCommandSwap) + args[0] = getBinFromManifest(manis[0]) + + // figure out whether we need to install stuff, or if local is fine + const localArb = new Arborist({ + ...flatOptions, + path, + }) + const tree = await localArb.loadActual() + + // do we have all the packages in manifest list? + const needInstall = + manis.some(manifest => manifestMissing({ tree, manifest })) + + if (needInstall) { + const { cache } = flatOptions + const installDir = cacheInstallDir({ cache, packages }) + await mkdirp(installDir) + const arb = new Arborist({ + ...flatOptions, + path: installDir, + }) + const tree = await arb.loadActual() + + // at this point, we have to ensure that we get the exact same + // version, because it's something that has only ever been installed + // by npm exec in the cache install directory + const add = manis.filter(mani => manifestMissing({ + tree, + manifest: { + ...mani, + _from: `${mani.name}@${mani.version}`, + }, + })) + .map(mani => mani._from) + .sort((a, b) => a.localeCompare(b)) + + // no need to install if already present + if (add.length) { + if (!yes) { + // set -n to always say no + if (yes === false) + throw new Error('canceled') + + if (noTTY() || ciDetect()) { + log.warn('exec', `The following package${ + add.length === 1 ? ' was' : 's were' + } not found and will be installed: ${ + add.map((pkg) => pkg.replace(/@$/, '')).join(', ') + }`) + } else { + const addList = add.map(a => ` ${a.replace(/@$/, '')}`) + .join('\n') + '\n' + const prompt = `Need to install the following packages:\n${ + addList + }Ok to proceed? ` + const confirm = await read({ prompt, default: 'y' }) + if (confirm.trim().toLowerCase().charAt(0) !== 'y') + throw new Error('canceled') + } + } + await arb.reify({ + ...flatOptions, + add, + }) + } + pathArr.unshift(resolve(installDir, 'node_modules/.bin')) + } + + return await _run() +} + +module.exports = exec diff --git a/node_modules/libnpmexec/lib/manifest-missing.js b/node_modules/libnpmexec/lib/manifest-missing.js new file mode 100644 index 000000000..471468096 --- /dev/null +++ b/node_modules/libnpmexec/lib/manifest-missing.js @@ -0,0 +1,17 @@ +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/node_modules/libnpmexec/lib/no-tty.js b/node_modules/libnpmexec/lib/no-tty.js new file mode 100644 index 000000000..601798d25 --- /dev/null +++ b/node_modules/libnpmexec/lib/no-tty.js @@ -0,0 +1 @@ +module.exports = () => !process.stdin.isTTY diff --git a/node_modules/libnpmexec/lib/run-script.js b/node_modules/libnpmexec/lib/run-script.js new file mode 100644 index 000000000..819dacb8b --- /dev/null +++ b/node_modules/libnpmexec/lib/run-script.js @@ -0,0 +1,86 @@ +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/node_modules/libnpmexec/package.json b/node_modules/libnpmexec/package.json new file mode 100644 index 000000000..1b7d24103 --- /dev/null +++ b/node_modules/libnpmexec/package.json @@ -0,0 +1,63 @@ +{ + "name": "libnpmexec", + "version": "1.0.1", + "files": [ + "lib" + ], + "main": "lib/index.js", + "engines": { + "node": ">=10" + }, + "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 lib/*.js", + "pretest": "npm run lint", + "test": "tap test/*.js", + "snap": "tap test/*.js", + "preversion": "npm test", + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags" + }, + "tap": { + "check-coverage": true + }, + "devDependencies": { + "bin-links": "^2.2.1", + "eslint": "^7.24.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^5.1.0", + "eslint-plugin-standard": "^5.0.0", + "tap": "^15.0.2" + }, + "dependencies": { + "@npmcli/arborist": "^2.3.0", + "@npmcli/ci-detect": "^1.3.0", + "@npmcli/run-script": "^1.8.4", + "chalk": "^4.1.0", + "mkdirp-infer-owner": "^2.0.0", + "npm-package-arg": "^8.1.2", + "pacote": "^11.3.1", + "proc-log": "^1.0.0", + "read": "^1.0.7", + "read-package-json-fast": "^2.0.2" + } +} |