Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/v7.1.0 #2287

Merged
merged 15 commits into from
Dec 4, 2020
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -738,3 +738,4 @@ Hollow Man <hollowman@hollowman.ml>
kai zhu <kaizhu256@gmail.com>
Alex Woollam <alexjhwoollam@gmail.com>
Daniel Fischer <daniel@d-fischer.dev>
Yash-Singh1 <saiansh2525@gmail.com>
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
## 7.1.0 (2020-12-04)

### FEATURES

* [`6b1575110`](https://github.com/npm/cli/commit/6b15751106beb99234aa4bf39ae05cf40076d42a)
[#2237](https://github.com/npm/cli/pull/2237)
add `npm set-script` command
([@Yash-Singh1](https://github.com/Yash-Singh1))
* [`15d7333f8`](https://github.com/npm/cli/commit/15d7333f832e3d68ae16895569f27a27ef86573e)
add interactive `npm exec`
([@isaacs](https://github.com/isaacs))

### BUG FIXES

* [`2a1192e4b`](https://github.com/npm/cli/commit/2a1192e4b03acdf6e6e24e58de68f736ab9bb35f)
[#2202](https://github.com/npm/cli/pull/2202)
Do not run interactive `npm exec` in CI when a TTY
([@isaacs](https://github.com/isaacs))

### DOCUMENTATION

* [`0599cc37d`](https://github.com/npm/cli/commit/0599cc37df453bf79d47490eb4fca3cd63f67f80)
[#2271](https://github.com/npm/cli/pull/2271)
don't wrap code block
([@ethomson](https://github.com/ethomson))

### DEPENDENCIES

* [`def85c726`](https://github.com/npm/cli/commit/def85c72640ffe2d27977c56b7aa06c6f6346ca9)
`@npmcli/arborist@1.0.14`
* fixes running `npm exec` from file system root folder
* [`4c94673ab`](https://github.com/npm/cli/commit/4c94673ab5399d27e5a48e52f7a65b038a456265)
`semver@7.3.4`

## 7.0.15 (2020-11-27)

### DEPENDENCIES
Expand Down
4 changes: 2 additions & 2 deletions docs/content/commands/npm-dist-tag.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ This command used to be known as `npm tag`, which only created new tags,
and so had a different syntax.

Tags must share a namespace with version numbers, because they are
specified in the same slot: `npm install <pkg>@<version>` vs `npm install
<pkg>@<tag>`.
specified in the same slot: `npm install <pkg>@<version>` vs
`npm install <pkg>@<tag>`.

Tags that can be interpreted as valid semver ranges will be rejected. For
example, `v1.4` cannot be used as a tag, because it is interpreted by
Expand Down
7 changes: 7 additions & 0 deletions docs/content/commands/npm-exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ npx <pkg>[@<specifier>] [args...]
npx -p <pkg>[@<specifier>] <cmd> [args...]
npx -c '<cmd> [args...]'
npx -p <pkg>[@<specifier>] -c '<cmd> [args...]'
Run without --call or positional args to open interactive subshell

alias: npm x, npx

common options:
--package=<pkg> (may be specified multiple times)
-p is a shorthand for --package only when using npx executable
-c <cmd> --call=<cmd> (may not be mixed with positional arguments)
Expand All @@ -30,6 +32,11 @@ This command allows you to run an arbitrary command from an npm package
(either one installed locally, or fetched remotely), in a similar context
as running it via `npm run`.

Run without positional arguments or `--call`, this allows you to
interactively run commands in the same sort of shell environment that
`package.json` scripts are run. Interactive mode is not supported in CI
environments when standard input is a TTY, to prevent hangs.

Whatever packages are specified by the `--package` option will be
provided in the `PATH` of the executed command, along with any locally
installed package executables. The `--package` option may be
Expand Down
34 changes: 34 additions & 0 deletions docs/content/commands/npm-set-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: npm-set-script
section: 1
description: Set tasks in the scripts section of package.json
---

### Synopsis
An npm command that lets you create a task in the scripts section of the package.json.

```bash
npm set-script [<script>] [<command>]
```


**Example:**

* `npm set-script start "http-server ."`

```json
{
"name": "my-project",
"scripts": {
"start": "http-server .",
"test": "some existing value"
}
}
```

### See Also

* [npm run-script](/commands/npm-run-script)
* [npm install](/commands/npm-install)
* [npm test](/commands/npm-test)
* [npm start](/commands/npm-start)
48 changes: 35 additions & 13 deletions lib/exec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const npm = require('./npm.js')

const output = require('./utils/output.js')
const usageUtil = require('./utils/usage.js')

const usage = usageUtil('exec',
'Run a command from a local or remote npm package.\n\n' +

Expand All @@ -13,7 +12,9 @@ const usage = usageUtil('exec',
'npx <pkg>[@<specifier>] [args...]\n' +
'npx -p <pkg>[@<specifier>] <cmd> [args...]\n' +
'npx -c \'<cmd> [args...]\'\n' +
'npx -p <pkg>[@<specifier>] -c \'<cmd> [args...]\'',
'npx -p <pkg>[@<specifier>] -c \'<cmd> [args...]\'' +
'\n' +
'Run without --call or positional args to open interactive subshell\n',

'\n--package=<pkg> (may be specified multiple times)\n' +
'-p is a shorthand for --package only when using npx executable\n' +
Expand Down Expand Up @@ -59,15 +60,14 @@ const ciDetect = require('@npmcli/ci-detect')
const crypto = require('crypto')
const pacote = require('pacote')
const npa = require('npm-package-arg')
const escapeArg = require('./utils/escape-arg.js')
const fileExists = require('./utils/file-exists.js')
const PATH = require('./utils/path.js')

const cmd = (args, cb) => exec(args).then(() => cb()).catch(cb)

const run = async ({ args, call, pathArr }) => {
const run = async ({ args, call, pathArr, shell }) => {
// turn list of args into command string
const script = call || args.map(escapeArg).join(' ').trim()
const script = call || args.join(' ').trim() || shell

// do the fakey runScript dance
// still should work if no package.json in cwd
Expand All @@ -83,7 +83,15 @@ const run = async ({ args, call, pathArr }) => {

npm.log.disableProgress()
try {
if (script === shell) {
if (process.stdin.isTTY) {
if (ciDetect())
return npm.log.warn('exec', 'Interactive mode disabled in CI environment')
output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`)
}
}
return await runScript({
...npm.flatOptions,
pkg,
banner: false,
// we always run in cwd, not --prefix
Expand All @@ -101,13 +109,23 @@ const run = async ({ args, call, pathArr }) => {
}

const exec = async args => {
const { package: packages, call } = npm.flatOptions
const { package: packages, call, shell } = npm.flatOptions

if (call && args.length)
throw usage

const pathArr = [...PATH]

// nothing to maybe install, skip the arborist dance
if (!call && !args.length && !packages.length) {
return await run({
args,
call,
shell,
pathArr,
})
}

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
Expand All @@ -126,8 +144,9 @@ const exec = async args => {
if (binExists) {
return await run({
args,
call: [args[0], ...args.slice(1).map(escapeArg)].join(' ').trim(),
call: [args[0], ...args.slice(1)].join(' ').trim(),
pathArr,
shell,
})
}

Expand Down Expand Up @@ -181,15 +200,18 @@ const exec = async args => {

// no need to install if already present
if (add.length) {
const isTTY = process.stdin.isTTY && process.stdout.isTTY
if (!npm.flatOptions.yes) {
// set -n to always say no
if (npm.flatOptions.yes === false)
throw 'canceled'

if (!isTTY || ciDetect())
npm.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 {
if (!process.stdin.isTTY || ciDetect()) {
npm.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${
Expand All @@ -205,7 +227,7 @@ const exec = async args => {
pathArr.unshift(resolve(installDir, 'node_modules/.bin'))
}

return await run({ args, call, pathArr })
return await run({ args, call, pathArr, shell })
}

const manifestMissing = (tree, mani) => {
Expand Down
76 changes: 41 additions & 35 deletions lib/explore.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,65 @@
const usageUtil = require('./utils/usage.js')
const completion = require('./utils/completion/installed-shallow.js')
const usage = usageUtil('explore', 'npm explore <pkg> [ -- <command>]')
const rpj = require('read-package-json-fast')

const cmd = (args, cb) => explore(args).then(() => cb()).catch(cb)

const output = require('./utils/output.js')
const npm = require('./npm.js')
const isWindows = require('./utils/is-windows.js')
const escapeArg = require('./utils/escape-arg.js')
const escapeExecPath = require('./utils/escape-exec-path.js')
const log = require('npmlog')

const spawn = require('@npmcli/promise-spawn')

const { resolve } = require('path')
const { promisify } = require('util')
const stat = promisify(require('fs').stat)
const runScript = require('@npmcli/run-script')
const { join, resolve, relative } = require('path')

const explore = async args => {
if (args.length < 1 || !args[0])
throw usage

const pkg = args.shift()
const cwd = resolve(npm.dir, pkg)
const opts = { cwd, stdio: 'inherit', stdioString: true }

const shellArgs = []
if (args.length) {
if (isWindows) {
const execCmd = escapeExecPath(args.shift())
opts.windowsVerbatimArguments = true
shellArgs.push('/d', '/s', '/c', execCmd, ...args.map(escapeArg))
} else
shellArgs.push('-c', args.map(escapeArg).join(' ').trim())
}
const pkgname = args.shift()

await stat(cwd).catch(er => {
throw new Error(`It doesn't look like ${pkg} is installed.`)
})
// detect and prevent any .. shenanigans
const path = join(npm.dir, join('/', pkgname))
if (relative(path, npm.dir) === '')
throw usage

const sh = npm.flatOptions.shell
log.disableProgress()
// run as if running a script named '_explore', which we set to either
// the set of arguments, or the shell config, and let @npmcli/run-script
// handle all the escaping and PATH setup stuff.

if (!shellArgs.length)
output(`\nExploring ${cwd}\nType 'exit' or ^D when finished\n`)
const pkg = await rpj(resolve(path, 'package.json')).catch(er => {
npm.log.error('explore', `It doesn't look like ${pkgname} is installed.`)
throw er
})

log.silly('explore', { sh, shellArgs, opts })
const { shell } = npm.flatOptions
pkg.scripts = {
...(pkg.scripts || {}),
_explore: args.join(' ').trim() || shell,
}

// only noisily fail if non-interactive, but still keep exit code intact
const proc = spawn(sh, shellArgs, opts)
if (!args.length)
output(`\nExploring ${path}\nType 'exit' or ^D when finished\n`)
npm.log.disableProgress()
try {
const res = await (shellArgs.length ? proc : proc.catch(er => er))
process.exitCode = res.code
return await runScript({
...npm.flatOptions,
pkg,
banner: false,
path,
stdioString: true,
event: '_explore',
stdio: 'inherit',
}).catch(er => {
process.exitCode = typeof er.code === 'number' && er.code !== 0 ? er.code
: 1
// if it's not an exit error, or non-interactive, throw it
const isProcExit = er.message === 'command failed' &&
(typeof er.code === 'number' || /^SIG/.test(er.signal || ''))
if (args.length || !isProcExit)
throw er
})
} finally {
log.enableProgress()
npm.log.enableProgress()
}
}

Expand Down
55 changes: 55 additions & 0 deletions lib/set-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict'

const log = require('npmlog')
const usageUtil = require('./utils/usage.js')
const { localPrefix } = require('./npm.js')
const fs = require('fs')
const usage = usageUtil('set-script', 'npm set-script [<script>] [<command>]')
const completion = require('./utils/completion/none.js')
const parseJSON = require('json-parse-even-better-errors')
const rpj = require('read-package-json-fast')

const cmd = (args, cb) => set(args).then(() => cb()).catch(cb)

const set = async function (args) {
if (process.env.npm_lifecycle_event === 'postinstall')
throw new Error('Scripts can’t set from the postinstall script')

// Parse arguments
if (args.length !== 2)
throw new Error(`Expected 2 arguments: got ${args.length}`)

// Set the script
let manifest
let warn = false
try {
manifest = fs.readFileSync(localPrefix + '/package.json', 'utf-8')
} catch (error) {
throw new Error('package.json not found')
}
try {
manifest = parseJSON(manifest)
} catch (error) {
throw new Error(`Invalid package.json: ${error}`)
}
if (!manifest.scripts)
manifest.scripts = {}
if (manifest.scripts[args[0]] && manifest.scripts[args[0]] !== args[1])
warn = true
manifest.scripts[args[0]] = args[1]
// format content
const packageJsonInfo = await rpj(localPrefix + '/package.json')
const {
[Symbol.for('indent')]: indent,
[Symbol.for('newline')]: newline,
} = packageJsonInfo
const format = indent === undefined ? ' ' : indent
const eol = newline === undefined ? '\n' : newline
const content = (JSON.stringify(manifest, null, format) + '\n')
.replace(/\n/g, eol)
fs.writeFileSync(localPrefix + '/package.json', content)
if (warn)
log.warn('set-script', `Script "${args[0]}" was overwritten`)
}

module.exports = Object.assign(cmd, { usage, completion })
Loading