diff --git a/lib/run-script.js b/lib/run-script.js index 270a91ab7d1cb..3a335db380217 100644 --- a/lib/run-script.js +++ b/lib/run-script.js @@ -1,58 +1,33 @@ -module.exports = runScriptCmd - const run = require('@npmcli/run-script') const npm = require('./npm.js') const readJson = require('read-package-json-fast') const { resolve, join } = require('path') const output = require('./utils/output.js') const log = require('npmlog') -const usage = require('./utils/usage') +const usageUtil = require('./utils/usage') const didYouMean = require('./utils/did-you-mean') const isWindowsShell = require('./utils/is-windows-shell.js') -runScriptCmd.usage = usage( +const usage = usageUtil( 'run-script', - 'npm run-script [-- ...]' + 'npm run-script [-- ]' ) -runScriptCmd.completion = function (opts, cb) { - // see if there's already a package specified. - var argv = opts.conf.argv.remain - - if (argv.length >= 4) return cb() - - if (argv.length === 3) { - // either specified a script locally, in which case, done, - // or a package, in which case, complete against its scripts - var json = join(npm.localPrefix, 'package.json') - return readJson(json, function (er, d) { - if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er) - if (er) d = {} - var scripts = Object.keys(d.scripts || {}) - if (scripts.indexOf(argv[2]) !== -1) return cb() - // ok, try to find out which package it was, then - var pref = npm.config.get('global') ? npm.config.get('prefix') - : npm.localPrefix - var pkgDir = resolve(pref, 'node_modules', argv[2], 'package.json') - readJson(pkgDir, function (er, d) { - if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er) - if (er) d = {} - var scripts = Object.keys(d.scripts || {}) - return cb(null, scripts) - }) - }) +const completion = async (opts, cb) => { + const argv = opts.conf.argv.remain + if (argv.length === 2) { + // find the script name + const json = resolve(npm.localPrefix, 'package.json') + const { scripts = {} } = await readJson(json).catch(er => ({})) + return cb(null, Object.keys(scripts)) } - - readJson(join(npm.localPrefix, 'package.json'), function (er, d) { - if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er) - d = d || {} - cb(null, Object.keys(d.scripts || {})) - }) + // otherwise nothing to do, just let the system handle it + return cb() } -function runScriptCmd (args, cb) { +const cmd = (args, cb) => { const fn = args.length ? runScript : list - fn(args).then(() => cb()).catch(cb) + return fn(args).then(() => cb()).catch(cb) } const runScript = async (args) => { @@ -64,10 +39,11 @@ const runScript = async (args) => { const { _id, scripts = {} } = pkg if (event === 'restart' && !scripts.restart) { - scripts.restart = 'npm stop && npm start' + scripts.restart = 'npm stop --if-present && npm start' } else if (event === 'env') { scripts.env = isWindowsShell ? 'SET' : 'env' } + pkg.scripts = scripts if (!scripts[event]) { if (npm.config.get('if-present')) { @@ -117,13 +93,13 @@ const list = async () => { 'start', 'restart', 'version' - ].reduce((l, p) => l.concat(['pre' + p, p, 'post' + p])) + ].reduce((l, p) => l.concat(['pre' + p, p, 'post' + p]), []) if (!scripts) { return [] } - const allScripts = scripts ? Object.keys(scripts) : [] + const allScripts = Object.keys(scripts) if (log.level === 'silent') { return allScripts } @@ -165,3 +141,5 @@ const list = async () => { } return allScripts } + +module.exports = Object.assign(cmd, { completion, usage }) diff --git a/test/lib/run-script.js b/test/lib/run-script.js new file mode 100644 index 0000000000000..db81196b7635b --- /dev/null +++ b/test/lib/run-script.js @@ -0,0 +1,395 @@ +const t = require('tap') +const requireInject = require('require-inject') + +let RUN_FAIL = null +const RUN_SCRIPTS = [] +const npm = { + localPrefix: __dirname, + flatOptions: { + scriptShell: undefined, + json: false, + parseable: false + }, + config: { + settings: { + 'if-present': false + }, + get: k => npm.config.settings[k], + set: (k, v) => { + npm.config.settings[k] = v + } + } +} + +const output = [] + +const npmlog = { level: 'warn' } +const getRS = windows => requireInject('../../lib/run-script.js', { + '@npmcli/run-script': async opts => { + RUN_SCRIPTS.push(opts) + }, + npmlog, + '../../lib/npm.js': npm, + '../../lib/utils/is-windows-shell.js': windows, + '../../lib/utils/output.js': (...msg) => output.push(msg) +}) + +const runScript = getRS(false) +const runScriptWin = getRS(true) + +const { writeFileSync } = require('fs') +t.test('completion', t => { + const dir = t.testdir() + npm.localPrefix = dir + t.test('already have a script name', t => { + runScript.completion({conf:{argv:{remain: ['npm','run','x']}}}, (er, results) => { + if (er) { + throw er + } + t.equal(results, undefined) + t.end() + }) + }) + t.test('no package.json', t => { + runScript.completion({conf:{argv:{remain: ['npm','run']}}}, (er, results) => { + if (er) { + throw er + } + t.strictSame(results, []) + t.end() + }) + }) + t.test('has package.json, no scripts', t => { + writeFileSync(`${dir}/package.json`, JSON.stringify({})) + runScript.completion({conf:{argv:{remain: ['npm', 'run']}}}, (er, results) => { + if (er) { + throw er + } + t.strictSame(results, []) + t.end() + }) + }) + t.test('has package.json, with scripts', t => { + writeFileSync(`${dir}/package.json`, JSON.stringify({ + scripts: { hello: 'echo hello', world: 'echo world' } + })) + runScript.completion({conf:{argv:{remain: ['npm', 'run']}}}, (er, results) => { + if (er) { + throw er + } + t.strictSame(results, ['hello', 'world']) + t.end() + }) + }) + t.end() +}) + +t.test('fail if no package.json', async t => { + npm.localPrefix = t.testdir() + await runScript([], er => t.match(er, { code: 'ENOENT' })) + await runScript(['test'], er => t.match(er, { code: 'ENOENT' })) +}) + +t.test('default env and restart scripts', async t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ name: 'x', version: '1.2.3' }) + }) + + await runScript(['env'], er => { + if (er) { + throw er + } + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: { + env: 'env' + } }, + event: 'env' + } + ]) + }) + RUN_SCRIPTS.length = 0 + + await runScriptWin(['env'], er => { + if (er) { + throw er + } + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: { + env: 'SET' + } }, + event: 'env' + } + ]) + }) + RUN_SCRIPTS.length = 0 + + await runScript(['restart'], er => { + if (er) { + throw er + } + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: { + restart: 'npm stop --if-present && npm start' + } }, + event: 'restart' + } + ]) + }) + RUN_SCRIPTS.length = 0 +}) + +t.test('try to run missing script', t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + scripts: { hello: 'world' } + }) + }) + t.test('no suggestions', async t => { + await runScript(['notevenclose'], er => { + t.match(er, { + message: 'missing script: notevenclose' + }) + }) + }) + t.test('suggestions', async t => { + await runScript(['helo'], er => { + t.match(er, { + message: 'missing script: helo\n\nDid you mean this?\n hello' + }) + }) + }) + t.test('with --if-present', async t => { + npm.config.set('if-present', true) + await runScript(['goodbye'], er => { + if (er) { + throw er + } + t.strictSame(RUN_SCRIPTS, [], 'did not try to run anything') + }) + }) + t.end() +}) + +t.test('run pre/post hooks', async t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + preenv: 'echo before the env', + postenv: 'echo after the env' + } + }) + }) + + await runScript(['env'], er => { + if (er) { + throw er + } + t.match(RUN_SCRIPTS, [ + { event: 'preenv' }, + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: { + env: 'env' + } }, + event: 'env' + }, + { event: 'postenv' } + ]) + }) + RUN_SCRIPTS.length = 0 +}) + +t.test('run silent', async t => { + npmlog.level = 'silent' + t.teardown(() => { npmlog.level = 'warn' }) + + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + preenv: 'echo before the env', + postenv: 'echo after the env' + } + }) + }) + + await runScript(['env'], er => { + if (er) { + throw er + } + t.match(RUN_SCRIPTS, [ + { + event: 'preenv', + stdio: 'pipe' + }, + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'pipe', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: { + env: 'env' + } }, + event: 'env' + }, + { + event: 'postenv', + stdio: 'pipe' + } + ]) + }) + RUN_SCRIPTS.length = 0 +}) + +t.test('list scripts', async t => { + const scripts = { + test: 'exit 2', + start: 'node server.js', + stop: 'node kill-server.js', + preenv: 'echo before the env', + postenv: 'echo after the env' + } + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts + }) + }) + + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [ + [ 'Lifecycle scripts included in x:' ], + [ ' test\n exit 2' ], + [ ' start\n node server.js' ], + [ ' stop\n node kill-server.js' ], + [ '\navailable via `npm run-script`:' ], + [ ' preenv\n echo before the env' ], + [ ' postenv\n echo after the env' ] + ], 'basic report') + output.length = 0 + + npmlog.level = 'silent' + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, []) + npmlog.level = 'warn' + + npm.flatOptions.json = true + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [[JSON.stringify(scripts, 0, 2)]], 'json report') + output.length = 0 + npm.flatOptions.json = false + + npm.flatOptions.parseable = true + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [ + [ 'test:exit 2' ], + [ 'start:node server.js' ], + [ 'stop:node kill-server.js' ], + [ 'preenv:echo before the env' ], + [ 'postenv:echo after the env' ] + ]) + output.length = 0 + npm.flatOptions.parseable = false +}) + +t.test('list scripts when no scripts', async t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3' + }) + }) + + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [], 'nothing to report') + output.length = 0 +}) + +t.test('list scripts, only commands', async t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { preversion: 'echo doing the version dance' } + }) + }) + + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [ + ["Lifecycle scripts included in x:"], + [" preversion\n echo doing the version dance"], + ]) + output.length = 0 +}) + +t.test('list scripts, only non-commands', async t => { + npm.localPrefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { glorp: 'echo doing the glerp glop' } + }) + }) + + await runScript([], er => { + if (er) { + throw er + } + }) + t.strictSame(output, [ + ["Scripts available in x via `npm run-script`:"], + [" glorp\n echo doing the glerp glop"] + ]) + output.length = 0 +})