From 697f21e8fd815553e8fd07153e7c49b1336a1aff Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 14:00:59 -0700 Subject: [PATCH 01/11] unit test for npm explore --- lib/explore.js | 103 ++++++++-------- test/lib/explore.js | 282 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+), 49 deletions(-) create mode 100644 test/lib/explore.js diff --git a/lib/explore.js b/lib/explore.js index acf9dc9d5e147..6455157fe7659 100644 --- a/lib/explore.js +++ b/lib/explore.js @@ -1,61 +1,66 @@ // npm explore [@] // open a subshell to the package folder. -module.exports = explore -explore.usage = 'npm explore [ -- ]' -explore.completion = require('./utils/completion/installed-shallow.js') - -var npm = require('./npm.js') -var spawn = require('./utils/spawn') -var path = require('path') -var fs = require('graceful-fs') -var isWindows = require('./utils/is-windows.js') -var escapeExecPath = require('./utils/escape-exec-path.js') -var escapeArg = require('./utils/escape-arg.js') -var output = require('./utils/output.js') -var log = require('npmlog') - -function explore (args, cb) { - if (args.length < 1 || !args[0]) return cb(explore.usage) - var p = args.shift() - - var cwd = path.resolve(npm.dir, p) - var opts = { cwd: cwd, stdio: 'inherit' } - - var shellArgs = [] - if (args) { +const usageUtil = require('./utils/usage.js') +const completion = require('./utils/completion/installed-shallow.js') +const usage = usageUtil('explore', 'npm explore [ -- ]') + +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 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) { - var execCmd = escapeExecPath(args.shift()) - var execArgs = [execCmd].concat(args.map(escapeArg)) + const execCmd = escapeExecPath(args.shift()) opts.windowsVerbatimArguments = true - shellArgs = ['/d', '/s', '/c'].concat(execArgs) + shellArgs.push('/d', '/s', '/c', execCmd, ...args.map(escapeArg)) } else { - shellArgs.unshift('-c') - shellArgs = ['-c', args.map(escapeArg).join(' ').trim()] + shellArgs.push('-c', args.map(escapeArg).join(' ').trim()) } } - var sh = npm.config.get('shell') - fs.stat(cwd, function (er, s) { - if (er || !s.isDirectory()) { - return cb(new Error( - "It doesn't look like " + p + ' is installed.' - )) - } + await stat(cwd).catch(er => { + throw new Error(`It doesn't look like ${pkg} is installed.`) + }) - if (!shellArgs.length) { - output( - '\nExploring ' + cwd + '\n' + - "Type 'exit' or ^D when finished\n" - ) - } + const sh = npm.flatOptions.shell + log.disableProgress() - log.silly('explore', { sh, shellArgs, opts }) - var shell = spawn(sh, shellArgs, opts) - shell.on('close', function (er) { - // only fail if non-interactive. - if (!shellArgs.length) return cb() - cb(er) - }) - }) + if (!shellArgs.length) { + output(`\nExploring ${cwd}\nType 'exit' or ^D when finished\n`) + } + + log.silly('explore', {sh, shellArgs, opts}) + + // only noisily fail if non-interactive, but still keep exit code intact + const proc = spawn(sh, shellArgs, opts) + try { + const res = await (shellArgs.length ? proc : proc.catch(er => er)) + process.exitCode = res.code + } finally { + log.enableProgress() + } } + +module.exports = Object.assign(cmd, { completion, usage }) diff --git a/test/lib/explore.js b/test/lib/explore.js new file mode 100644 index 0000000000000..03ad230489d17 --- /dev/null +++ b/test/lib/explore.js @@ -0,0 +1,282 @@ +const t = require('tap') +const requireInject = require('require-inject') + +let STAT_ERROR = null +let STAT_CALLED = '' +const mockStat = (path, cb) => { + STAT_CALLED = path + cb(STAT_ERROR, {}) +} + +let SPAWN_ERROR = null +let SPAWN_EXIT_CODE = 0 +let SPAWN_SHELL_EXEC = null +let SPAWN_SHELL_ARGS = null +const mockSpawn = (sh, shellArgs, opts) => { + if (sh !== 'shell-command') { + throw new Error('got wrong shell command') + } + if (SPAWN_ERROR) { + return Promise.reject(SPAWN_ERROR) + } + SPAWN_SHELL_EXEC = sh + SPAWN_SHELL_ARGS = shellArgs + return Promise.resolve({ code: SPAWN_EXIT_CODE }) +} + +const output = [] +let ERROR_HANDLER_CALLED = null +const getExplore = windows => requireInject('../../lib/explore.js', { + '../../lib/utils/is-windows.js': windows, + '../../lib/utils/escape-arg.js': requireInject('../../lib/utils/escape-arg.js', { + '../../lib/utils/is-windows.js': windows + }), + path: require('path')[windows ? 'win32' : 'posix'], + '../../lib/utils/escape-exec-path.js': requireInject('../../lib/utils/escape-arg.js', { + '../../lib/utils/is-windows.js': windows + }), + '../../lib/utils/error-handler.js': er => { + ERROR_HANDLER_CALLED = er + }, + fs: { + stat: mockStat + }, + '../../lib/npm.js': { + dir: windows ? 'c:\\npm\\dir' : '/npm/dir', + flatOptions: { + shell: 'shell-command' + } + }, + '@npmcli/promise-spawn': mockSpawn, + '../../lib/utils/output.js': out => { + output.push(out) + } +}) + +const windowsExplore = getExplore(true) +const posixExplore = getExplore(false) + +t.test('basic interactive', t => { + t.afterEach((cb) => { + output.length = 0 + cb() + }) + + t.test('windows', t => windowsExplore(['pkg'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: 'c:\\npm\\dir\\pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: [] + }) + t.strictSame(output, [ + "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n" + ]) + })) + + t.test('posix', t => posixExplore(['pkg'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: '/npm/dir/pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: [] + }) + t.strictSame(output, [ + "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n" + ]) + })) + + t.end() +}) + +t.test('interactive tracks exit code', t => { + const { exitCode } = process + t.beforeEach((cb) => { + process.exitCode = exitCode + SPAWN_EXIT_CODE = 99 + cb() + }) + t.afterEach((cb) => { + SPAWN_EXIT_CODE = 0 + output.length = 0 + process.exitCode = exitCode + cb() + }) + + t.test('windows', t => windowsExplore(['pkg'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: 'c:\\npm\\dir\\pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: [] + }) + t.strictSame(output, [ + "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n" + ]) + t.equal(process.exitCode, 99) + })) + + t.test('posix', t => posixExplore(['pkg'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: '/npm/dir/pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: [] + }) + t.strictSame(output, [ + "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n" + ]) + t.equal(process.exitCode, 99) + })) + + t.test('posix spawn fail', t => { + t.teardown(() => { + SPAWN_ERROR = null + }) + SPAWN_ERROR = Object.assign(new Error('glorb'), { + code: 33 + }) + return posixExplore(['pkg'], er => { + if (er) { + throw er + } + t.strictSame(output, [ + "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n" + ]) + t.equal(process.exitCode, 33) + }) + }) + + t.end() +}) + +t.test('basic non-interactive', t => { + t.afterEach((cb) => { + output.length = 0 + cb() + }) + + t.test('windows', t => windowsExplore(['pkg', 'ls'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: 'c:\\npm\\dir\\pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: [ + '/d', + '/s', + '/c', + '"ls"', + ] + }) + t.strictSame(output, []) + })) + + t.test('posix', t => posixExplore(['pkg', 'ls'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: '/npm/dir/pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: ['-c', 'ls'] + }) + t.strictSame(output, []) + })) + + t.end() +}) + +t.test('usage if no pkg provided', t => { + t.teardown(() => { + output.length = 0 + ERROR_HANDLER_CALLED = null + }) + t.plan(1) + posixExplore([], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED: null, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: '/npm/dir/pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: ['-c', 'ls'] + }) + }).catch(er => t.equal(er, 'npm explore [ -- ]')) +}) + +t.test('pkg not installed', t => { + STAT_ERROR = new Error('plurple') + t.plan(1) + + posixExplore(['pkg', 'ls'], er => { + if (er) { + throw er + } + t.strictSame({ + ERROR_HANDLER_CALLED, + STAT_CALLED, + SPAWN_SHELL_EXEC, + SPAWN_SHELL_ARGS + }, { + ERROR_HANDLER_CALLED: null, + STAT_CALLED: '/npm/dir/pkg', + SPAWN_SHELL_EXEC: 'shell-command', + SPAWN_SHELL_ARGS: ['-c', 'ls'] + }) + t.strictSame(output, []) + }).catch(er => { + t.match(er, { message: `It doesn't look like pkg is installed.` }) + }) +}) From 9b1ebfda40d2d7c1ee743205799593b1ca02d07f Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 14:56:34 -0700 Subject: [PATCH 02/11] unit test for flat-options in proper location Also get it up to 100% coverage --- lib/config/flat-options.js | 1 + ...st-lib-config-flat-options.js-TAP.test.js} | 6 +- test/{tap => lib/config}/flat-options.js | 61 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) rename tap-snapshots/{test-tap-flat-options.js-TAP.test.js => test-lib-config-flat-options.js-TAP.test.js} (95%) rename test/{tap => lib/config}/flat-options.js (82%) diff --git a/lib/config/flat-options.js b/lib/config/flat-options.js index e2e0bfdd96f63..398bc7632ca08 100644 --- a/lib/config/flat-options.js +++ b/lib/config/flat-options.js @@ -165,6 +165,7 @@ const flatOptions = npm => npm.flatOptions || Object.freeze({ globalStyle: npm.config.get('global-style'), legacyBundling: npm.config.get('legacy-bundling'), scriptShell: npm.config.get('script-shell') || undefined, + shell: npm.config.get('shell'), omit: buildOmitList(npm), legacyPeerDeps: npm.config.get('legacy-peer-deps'), diff --git a/tap-snapshots/test-tap-flat-options.js-TAP.test.js b/tap-snapshots/test-lib-config-flat-options.js-TAP.test.js similarity index 95% rename from tap-snapshots/test-tap-flat-options.js-TAP.test.js rename to tap-snapshots/test-lib-config-flat-options.js-TAP.test.js index d443e491e7c25..568e2ac7e642a 100644 --- a/tap-snapshots/test-tap-flat-options.js-TAP.test.js +++ b/tap-snapshots/test-lib-config-flat-options.js-TAP.test.js @@ -5,11 +5,12 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' -exports[`test/tap/flat-options.js TAP basic > flat options 1`] = ` +exports[`test/lib/config/flat-options.js TAP basic > flat options 1`] = ` Object { "@scope:registry": "@scope:registry", "//nerf.dart:_authToken": "//nerf.dart:_authToken", "access": "access", + "all": undefined, "allowSameVersion": "allow-same-version", "alwaysAuth": "always-auth", "audit": "audit", @@ -51,6 +52,7 @@ Object { "json": undefined, "key": "key", "legacyBundling": "legacy-bundling", + "legacyPeerDeps": undefined, "localPrefix": "/path/to/npm/cli", "lockFile": Object { "retries": "cache-lock-retries", @@ -102,6 +104,7 @@ Object { "staleness": "searchstaleness", }, "sendMetrics": "send-metrics", + "shell": undefined, "signGitCommit": "sign-git-commit", "signGitTag": "sign-git-tag", "ssoPollFrequency": undefined, @@ -114,5 +117,6 @@ Object { "unicode": undefined, "userAgent": "user-agent", "viewer": "viewer", + "which": undefined, } ` diff --git a/test/tap/flat-options.js b/test/lib/config/flat-options.js similarity index 82% rename from test/tap/flat-options.js rename to test/lib/config/flat-options.js index 4a8fc4c3d2490..9e482a12bf136 100644 --- a/test/tap/flat-options.js +++ b/test/lib/config/flat-options.js @@ -1,4 +1,3 @@ -// const common = require('../common-tap.js') const t = require('tap') process.env.NODE = '/path/to/some/node' @@ -118,7 +117,7 @@ class MockConfig { } } -const flatOptions = require('../../lib/config/flat-options.js') +const flatOptions = require('../../../lib/config/flat-options.js') t.match(logs, [[ 'verbose', 'npm-session', @@ -133,7 +132,8 @@ t.test('basic', t => { ...generatedFlat, npmBin: '/path/to/npm/bin.js', log: {}, - npmSession: '12345' + npmSession: '12345', + cache: generatedFlat.cache.replace(/\\/g, '/') } t.matchSnapshot(clean, 'flat options') t.equal(generatedFlat.npmBin, require.main.filename) @@ -270,3 +270,58 @@ t.test('get the node without the environ', t => { t.equal(flatOptions(new Mocknpm()).nodeBin, process.execPath) t.end() }) + +t.test('various default values and falsey fallbacks', t => { + const npm = new Mocknpm({ + 'script-shell': false, + registry: 'http://example.com', + 'metrics-registry': null, + 'searchlimit': 0, + 'save-exact': false, + 'save-prefix': '>=' + }) + const opts = flatOptions(npm) + t.equal(opts.scriptShell, undefined, 'scriptShell is undefined if falsey') + t.equal(opts.metricsRegistry, 'http://example.com', + 'metricsRegistry defaults to registry') + t.equal(opts.search.limit, 20, 'searchLimit defaults to 20') + t.equal(opts.savePrefix, '>=', 'save-prefix respected if no save-exact') + logs.length = 0 + t.end() +}) + +t.test('save-type', t => { + const base = { + 'save-optional': false, + 'save-peer': false, + 'save-dev': false, + 'save-prod': false + } + const cases = [ + ['peerOptional', { + 'save-optional': true, + 'save-peer': true, + }], + ['optional', { + 'save-optional': true + }], + ['dev', { + 'save-dev': true + }], + ['peer', { + 'save-peer': true + }], + ['prod', { + 'save-prod': true + }], + [null, {}] + ] + for (const [expect, options] of cases) { + const opts = flatOptions(new Mocknpm({ + ...base, + ...options + })) + t.equal(opts.saveType, expect, JSON.stringify(options)) + } + t.end() +}) From 4c6c1fcd30693390ba82bb88a9df7d6e6c6812a6 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 15:05:06 -0700 Subject: [PATCH 03/11] unit test for escape-arg util --- lib/utils/escape-arg.js | 25 ++++++++----------------- test/lib/utils/escape-arg.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 test/lib/utils/escape-arg.js diff --git a/lib/utils/escape-arg.js b/lib/utils/escape-arg.js index 114abaadaa090..4fddcb8c9ba80 100644 --- a/lib/utils/escape-arg.js +++ b/lib/utils/escape-arg.js @@ -1,6 +1,5 @@ -'use strict' -var path = require('path') -var isWindows = require('./is-windows.js') +const { normalize } = require('path') +const isWindows = require('./is-windows.js') /* Escape the name of an executable suitable for passing to the system shell. @@ -9,19 +8,11 @@ Windows is easy, wrap in double quotes and you're done, as there's no facility to create files with quotes in their names. Unix-likes are a little more complicated, wrap in single quotes and escape -any single quotes in the filename. +any single quotes in the filename. The '"'"' construction ends the quoted +block, creates a new " quoted string with ' in it. So, `foo'bar` becomes +`'foo'"'"'bar'`, which is the bash way of saying `'foo' + "'" + 'bar'`. */ -module.exports = escapify - -function escapify (str) { - if (isWindows) { - return '"' + path.normalize(str) + '"' - } else { - if (/[^-_.~/\w]/.test(str)) { - return "'" + str.replace(/'/g, "'\"'\"'") + "'" - } else { - return str - } - } -} +module.exports = str => isWindows ? '"' + normalize(str) + '"' + : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, `'"'"'`) + "'" + : str diff --git a/test/lib/utils/escape-arg.js b/test/lib/utils/escape-arg.js new file mode 100644 index 0000000000000..413fa47838bac --- /dev/null +++ b/test/lib/utils/escape-arg.js @@ -0,0 +1,15 @@ +const requireInject = require('require-inject') +const t = require('tap') +const getEscape = win => requireInject('../../../lib/utils/escape-arg.js', { + '../../../lib/utils/is-windows.js': win, + path: require('path')[win ? 'win32' : 'posix'] +}) + +const winEscape = getEscape(true) +const nixEscape = getEscape(false) + +t.equal(winEscape('hello/to the/world'), '"hello\\to the\\world"') +t.equal(nixEscape(`hello/to-the/world`), `hello/to-the/world`) +t.equal(nixEscape(`hello/to the/world`), `'hello/to the/world'`) +t.equal(nixEscape(`hello/to%the/world`), `'hello/to%the/world'`) +t.equal(nixEscape(`hello/to'the/world`), `'hello/to'"'"'the/world'`) From ebd52fdbe7a8a5cab6fbcb3c0c4f2c38587f46c0 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 15:07:06 -0700 Subject: [PATCH 04/11] unit test for escape-exec-path util --- lib/utils/escape-exec-path.js | 29 ++++++++++------------------- test/lib/utils/escape-exec-path.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 test/lib/utils/escape-exec-path.js diff --git a/lib/utils/escape-exec-path.js b/lib/utils/escape-exec-path.js index 42b64934867dd..e941e0cb9d5f7 100644 --- a/lib/utils/escape-exec-path.js +++ b/lib/utils/escape-exec-path.js @@ -1,6 +1,5 @@ -'use strict' -var path = require('path') -var isWindows = require('./is-windows.js') +const { normalize } = require('path') +const isWindows = require('./is-windows.js') /* Escape the name of an executable suitable for passing to the system shell. @@ -9,22 +8,14 @@ Windows is easy, wrap in double quotes and you're done, as there's no facility to create files with quotes in their names. Unix-likes are a little more complicated, wrap in single quotes and escape -any single quotes in the filename. +any single quotes in the filename. The '"'"' construction ends the quoted +block, creates a new " quoted string with ' in it. So, `foo'bar` becomes +`'foo'"'"'bar'`, which is the bash way of saying `'foo' + "'" + 'bar'`. */ -module.exports = escapify +const winQuote = str => !/ /.test(str) ? str : '"' + str + '"' +const winEsc = str => normalize(str).split(/\\/).map(winQuote).join('\\') -function windowsQuotes (str) { - if (!/ /.test(str)) return str - return '"' + str + '"' -} - -function escapify (str) { - if (isWindows) { - return path.normalize(str).split(/\\/).map(windowsQuotes).join('\\') - } else if (/[^-_.~/\w]/.test(str)) { - return "'" + str.replace(/'/g, "'\"'\"'") + "'" - } else { - return str - } -} +module.exports = str => isWindows ? winEsc(str) + : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, `'"'"'`) + "'" + : str diff --git a/test/lib/utils/escape-exec-path.js b/test/lib/utils/escape-exec-path.js new file mode 100644 index 0000000000000..28fe75c2a98f4 --- /dev/null +++ b/test/lib/utils/escape-exec-path.js @@ -0,0 +1,15 @@ +const requireInject = require('require-inject') +const t = require('tap') +const getEscape = win => requireInject('../../../lib/utils/escape-exec-path.js', { + '../../../lib/utils/is-windows.js': win, + path: require('path')[win ? 'win32' : 'posix'] +}) + +const winEscape = getEscape(true) +const nixEscape = getEscape(false) + +t.equal(winEscape('hello/to the/world'), 'hello\\"to the"\\world') +t.equal(nixEscape(`hello/to-the/world`), `hello/to-the/world`) +t.equal(nixEscape(`hello/to the/world`), `'hello/to the/world'`) +t.equal(nixEscape(`hello/to%the/world`), `'hello/to%the/world'`) +t.equal(nixEscape(`hello/to'the/world`), `'hello/to'"'"'the/world'`) From 99e233c710d374e1cfd5758659aa239be5df8657 Mon Sep 17 00:00:00 2001 From: isaacs Date: Wed, 29 Jul 2020 14:25:19 -0700 Subject: [PATCH 05/11] fix lintfix script, use flat ternaries --- .eslintrc.json | 2 +- lib/config/defaults.js | 2 +- lib/config/flat-options.js | 10 +++++----- lib/config/set-envs.js | 2 +- lib/explore.js | 2 +- lib/help-search.js | 10 +++++----- lib/help.js | 2 +- lib/outdated.js | 4 ++-- lib/repo.js | 4 ++-- lib/utils/escape-arg.js | 2 +- lib/utils/escape-exec-path.js | 2 +- lib/utils/hosted-git-info-from-manifest.js | 4 ++-- lib/utils/setup-log.js | 4 ++-- lib/utils/update-notifier.js | 4 ++-- package.json | 2 +- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 9d213d17c3c78..9347c7d8eb38d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -62,7 +62,7 @@ "ArrayExpression": 1, "ObjectExpression": 1, "ImportDeclaration": 1, - "flatTernaryExpressions": false, + "flatTernaryExpressions": true, "ignoreComments": false, "ignoredNodes": ["TemplateLiteral *"] }], diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 93c6b2cb13fe6..1fe815e6ec9b4 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -158,7 +158,7 @@ Object.defineProperty(exports, 'defaults', { globalconfig: path.resolve(globalPrefix, 'etc', 'npmrc'), 'global-style': false, group: process.platform === 'win32' ? 0 - : process.env.SUDO_GID || (process.getgid && process.getgid()), + : process.env.SUDO_GID || (process.getgid && process.getgid()), 'ham-it-up': false, heading: 'npm', 'if-present': false, diff --git a/lib/config/flat-options.js b/lib/config/flat-options.js index 398bc7632ca08..9834afb5c402e 100644 --- a/lib/config/flat-options.js +++ b/lib/config/flat-options.js @@ -175,12 +175,12 @@ const flatOptions = npm => npm.flatOptions || Object.freeze({ saveType: npm.config.get('save-optional') && npm.config.get('save-peer') ? 'peerOptional' : npm.config.get('save-optional') ? 'optional' - : npm.config.get('save-dev') ? 'dev' - : npm.config.get('save-peer') ? 'peer' - : npm.config.get('save-prod') ? 'prod' - : null, + : npm.config.get('save-dev') ? 'dev' + : npm.config.get('save-peer') ? 'peer' + : npm.config.get('save-prod') ? 'prod' + : null, savePrefix: npm.config.get('save-exact') ? '' - : npm.config.get('save-prefix'), + : npm.config.get('save-prefix'), // configs for npm-registry-fetch otp: npm.config.get('otp'), diff --git a/lib/config/set-envs.js b/lib/config/set-envs.js index cd719c6bc6326..0367a37ea281f 100644 --- a/lib/config/set-envs.js +++ b/lib/config/set-envs.js @@ -16,7 +16,7 @@ const envVal = val => Array.isArray(val) ? val.join('\n\n') : val const sameConfigValue = (def, val) => !Array.isArray(val) || !Array.isArray(def) ? def === val - : sameArrayValue(def, val) + : sameArrayValue(def, val) const sameArrayValue = (def, val) => { if (def.length !== val.length) { diff --git a/lib/explore.js b/lib/explore.js index 6455157fe7659..cbc30ba093972 100644 --- a/lib/explore.js +++ b/lib/explore.js @@ -51,7 +51,7 @@ const explore = async args => { output(`\nExploring ${cwd}\nType 'exit' or ^D when finished\n`) } - log.silly('explore', {sh, shellArgs, opts}) + log.silly('explore', { sh, shellArgs, opts }) // only noisily fail if non-interactive, but still keep exit code intact const proc = spawn(sh, shellArgs, opts) diff --git a/lib/help-search.js b/lib/help-search.js index 475f305e49103..1c2a71c8a0038 100644 --- a/lib/help-search.js +++ b/lib/help-search.js @@ -145,11 +145,11 @@ function searchFiles (args, files, cb) { results = results.sort(function (a, b) { return a.found.length > b.found.length ? -1 : a.found.length < b.found.length ? 1 - : a.totalHits > b.totalHits ? -1 - : a.totalHits < b.totalHits ? 1 - : a.lines.length > b.lines.length ? -1 - : a.lines.length < b.lines.length ? 1 - : 0 + : a.totalHits > b.totalHits ? -1 + : a.totalHits < b.totalHits ? 1 + : a.lines.length > b.lines.length ? -1 + : a.lines.length < b.lines.length ? 1 + : 0 }) cb(null, results) diff --git a/lib/help.js b/lib/help.js index a1fc11dabceb1..7d20489b532e1 100644 --- a/lib/help.js +++ b/lib/help.js @@ -102,7 +102,7 @@ function pickMan (mans, pref_) { var bn = b.match(nre)[1] return an === bn ? (a > b ? -1 : 1) : pref[an] < pref[bn] ? -1 - : 1 + : 1 }) return mans[0] } diff --git a/lib/outdated.js b/lib/outdated.js index f86550be916e3..660f1cd05704e 100644 --- a/lib/outdated.js +++ b/lib/outdated.js @@ -134,8 +134,8 @@ async function outdated_ (tree, deps, opts) { const type = edge.optional ? 'optionalDependencies' : edge.peer ? 'peerDependencies' - : edge.dev ? 'devDependencies' - : 'dependencies' + : edge.dev ? 'devDependencies' + : 'dependencies' // deps different from prod not currently // on disk are not included in the output diff --git a/lib/repo.js b/lib/repo.js index fb2a03ab0ba3f..7b415ee234289 100644 --- a/lib/repo.js +++ b/lib/repo.js @@ -26,8 +26,8 @@ const getRepo = async pkg => { const r = mani.repository const rurl = !r ? null : typeof r === 'string' ? r - : typeof r === 'object' && typeof r.url === 'string' ? r.url - : null + : typeof r === 'object' && typeof r.url === 'string' ? r.url + : null if (!rurl) { throw Object.assign(new Error('no repository'), { diff --git a/lib/utils/escape-arg.js b/lib/utils/escape-arg.js index 4fddcb8c9ba80..135d380fc2d5a 100644 --- a/lib/utils/escape-arg.js +++ b/lib/utils/escape-arg.js @@ -14,5 +14,5 @@ block, creates a new " quoted string with ' in it. So, `foo'bar` becomes */ module.exports = str => isWindows ? '"' + normalize(str) + '"' - : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, `'"'"'`) + "'" + : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, '\'"\'"\'') + "'" : str diff --git a/lib/utils/escape-exec-path.js b/lib/utils/escape-exec-path.js index e941e0cb9d5f7..83c70680b4da2 100644 --- a/lib/utils/escape-exec-path.js +++ b/lib/utils/escape-exec-path.js @@ -17,5 +17,5 @@ const winQuote = str => !/ /.test(str) ? str : '"' + str + '"' const winEsc = str => normalize(str).split(/\\/).map(winQuote).join('\\') module.exports = str => isWindows ? winEsc(str) - : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, `'"'"'`) + "'" + : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, '\'"\'"\'') + "'" : str diff --git a/lib/utils/hosted-git-info-from-manifest.js b/lib/utils/hosted-git-info-from-manifest.js index 583e722bf7ccd..9592b0b3a937b 100644 --- a/lib/utils/hosted-git-info-from-manifest.js +++ b/lib/utils/hosted-git-info-from-manifest.js @@ -6,8 +6,8 @@ module.exports = mani => { const r = mani.repository const rurl = !r ? null : typeof r === 'string' ? r - : typeof r === 'object' && typeof r.url === 'string' ? r.url - : null + : typeof r === 'object' && typeof r.url === 'string' ? r.url + : null // hgi returns undefined sometimes, but let's always return null here return (rurl && hostedGitInfo.fromUrl(rurl.replace(/^git\+/, ''))) || null diff --git a/lib/utils/setup-log.js b/lib/utils/setup-log.js index 34ff2330637c6..9e845de0e2310 100644 --- a/lib/utils/setup-log.js +++ b/lib/utils/setup-log.js @@ -19,11 +19,11 @@ module.exports = (config) => { const enableColorStderr = color === 'always' ? true : color === false ? false - : stderrTTY + : stderrTTY const enableColorStdout = color === 'always' ? true : color === false ? false - : stdoutTTY + : stdoutTTY if (enableColorStderr) { log.enableColor() diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js index 7ae4a03c3a97f..5a88e54f10684 100644 --- a/lib/utils/update-notifier.js +++ b/lib/utils/update-notifier.js @@ -32,8 +32,8 @@ module.exports = (npm) => { const type = notifier.update.type const typec = !useColor ? type : type === 'major' ? chalk.red(type) - : type === 'minor' ? chalk.yellow(type) - : chalk.green(type) + : type === 'minor' ? chalk.yellow(type) + : chalk.green(type) const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` notifier.notify({ diff --git a/package.json b/package.json index 7bfbf99536ac9..dbc29a496cdaa 100644 --- a/package.json +++ b/package.json @@ -288,7 +288,7 @@ "sudotest": "sudo npm run test --", "sudotest:nocleanup": "sudo NO_TEST_CLEANUP=1 npm run test --", "posttest": "npm run lint", - "eslint": "eslint --", + "eslint": "eslint", "lint": "npm run eslint -- \"lib/**/*.js\"", "lintfix": "npm run lint -- --fix", "prelint": "rimraf test/npm_cache*" From c776d87f0b28e4588a52e2c881b607dc7efa4ad5 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 15:59:10 -0700 Subject: [PATCH 06/11] unit test and bugfix for lifecycle-cmd util It should not be completing to all installed packages. No completion makes sense, as of npm v4. --- lib/utils/lifecycle-cmd.js | 23 ++++++++--------------- test/lib/utils/lifecycle-cmd.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 test/lib/utils/lifecycle-cmd.js diff --git a/lib/utils/lifecycle-cmd.js b/lib/utils/lifecycle-cmd.js index bb802f45ee08c..35e05d508bcad 100644 --- a/lib/utils/lifecycle-cmd.js +++ b/lib/utils/lifecycle-cmd.js @@ -1,18 +1,11 @@ -exports = module.exports = cmd +// The implementation of commands that are just "run a script" +// test, start, stop, restart -var npm = require('../npm.js') -var usage = require('./usage.js') +const npm = require('../npm.js') +const usageUtil = require('./usage.js') -function cmd (stage) { - function CMD (args, cb) { - npm.commands['run-script']([stage].concat(args), cb) - } - CMD.usage = usage(stage, 'npm ' + stage + ' [-- ]') - var installedShallow = require('./completion/installed-shallow.js') - CMD.completion = function (opts, cb) { - installedShallow(opts, function (d) { - return d.scripts && d.scripts[stage] - }, cb) - } - return CMD +module.exports = stage => { + const cmd = (args, cb) => npm.commands.run([stage, ...args], cb) + const usage = usageUtil(stage, `npm ${stage} [-- cb(null, 'called npm.commands.run') + } + } +}) + +t.test('create a lifecycle command', t => { + const cmd = lifecycleCmd('asdf') + t.equal(cmd.completion, undefined, 'no completion') + cmd(['some', 'args'], (er, result) => { + t.strictSame(result, 'called npm.commands.run') + t.end() + }) +}) From d809afc98feb6d33d3c3cfd161603b6af134f276 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 16:08:29 -0700 Subject: [PATCH 07/11] test for npm test (obligatory 'yo dawg' reference) --- lib/test.js | 23 +++++++++-------------- test/lib/test.js | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 test/lib/test.js diff --git a/lib/test.js b/lib/test.js index 05bffed86d59c..7ad76951a5fde 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,16 +1,11 @@ -/* eslint-disable standard/no-callback-literal */ -module.exports = test - const testCmd = require('./utils/lifecycle-cmd.js')('test') +const { completion, usage } = testCmd +const cmd = (args, cb) => testCmd(args, er => { + if (er && er.code === 'ELIFECYCLE') { + cb('Test failed. See above for more details.') + } else { + cb(er) + } +}) -test.usage = testCmd.usage - -function test (args, cb) { - testCmd(args, function (er) { - if (!er) return cb() - if (er.code === 'ELIFECYCLE') { - return cb('Test failed. See above for more details.') - } - return cb(er) - }) -} +module.exports = Object.assign(cmd, { completion, usage }) diff --git a/test/lib/test.js b/test/lib/test.js new file mode 100644 index 0000000000000..8b6d0662659cf --- /dev/null +++ b/test/lib/test.js @@ -0,0 +1,40 @@ +const t = require('tap') +const requireInject = require('require-inject') +let RUN_ARGS = null +const npmock = { + commands: { + run: (args, cb) => { + RUN_ARGS = args + cb() + } + } +} +const test = requireInject('../../lib/test.js', { + '../../lib/npm.js': npmock +}) + +t.test('run a test', t => { + test([], (er) => { + t.strictSame(RUN_ARGS, ['test'], 'added "test" to the args') + }) + test(['hello', 'world'], (er) => { + t.strictSame(RUN_ARGS, ['test', 'hello', 'world'], 'added positional args') + }) + + const lcErr = Object.assign(new Error('should not see this'), { + code: 'ELIFECYCLE' + }) + const otherErr = new Error('should see this') + + npmock.commands.run = (args, cb) => cb(lcErr) + test([], (er) => { + t.equal(er, 'Test failed. See above for more details.') + }) + + npmock.commands.run = (args, cb) => cb(otherErr) + test([], (er) => { + t.match(er, { message: 'should see this' }) + }) + + t.end() +}) From edb7810e43c5aa36b3f664c6a48c2dd06634d6f1 Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 16:11:38 -0700 Subject: [PATCH 08/11] tests for start, stop, restart --- lib/utils/lifecycle-cmd.js | 2 +- test/lib/restart.js | 5 +++++ test/lib/start.js | 5 +++++ test/lib/stop.js | 5 +++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 test/lib/restart.js create mode 100644 test/lib/start.js create mode 100644 test/lib/stop.js diff --git a/lib/utils/lifecycle-cmd.js b/lib/utils/lifecycle-cmd.js index 35e05d508bcad..3a1eb229b52e8 100644 --- a/lib/utils/lifecycle-cmd.js +++ b/lib/utils/lifecycle-cmd.js @@ -6,6 +6,6 @@ const usageUtil = require('./usage.js') module.exports = stage => { const cmd = (args, cb) => npm.commands.run([stage, ...args], cb) - const usage = usageUtil(stage, `npm ${stage} [-- ]`) return Object.assign(cmd, { usage }) } diff --git a/test/lib/restart.js b/test/lib/restart.js new file mode 100644 index 0000000000000..f705d5f600747 --- /dev/null +++ b/test/lib/restart.js @@ -0,0 +1,5 @@ +const t = require('tap') +const restart = require('../../lib/restart.js') +t.isa(restart, Function) +t.equal(restart.completion, undefined, 'no completion') +t.equal(restart.usage, 'npm restart [-- ]') diff --git a/test/lib/start.js b/test/lib/start.js new file mode 100644 index 0000000000000..a40126d71af3a --- /dev/null +++ b/test/lib/start.js @@ -0,0 +1,5 @@ +const t = require('tap') +const start = require('../../lib/start.js') +t.isa(start, Function) +t.equal(start.completion, undefined, 'no completion') +t.equal(start.usage, 'npm start [-- ]') diff --git a/test/lib/stop.js b/test/lib/stop.js new file mode 100644 index 0000000000000..12523935b4f1f --- /dev/null +++ b/test/lib/stop.js @@ -0,0 +1,5 @@ +const t = require('tap') +const stop = require('../../lib/stop.js') +t.isa(stop, Function) +t.equal(stop.completion, undefined, 'no completion') +t.equal(stop.usage, 'npm stop [-- ]') From 72cd572dbd4e48e0a3b271408715113b9c095d3d Mon Sep 17 00:00:00 2001 From: isaacs Date: Mon, 20 Jul 2020 16:46:00 -0700 Subject: [PATCH 09/11] Fix completion for partial commands --- lib/completion.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/completion.js b/lib/completion.js index a682c134a7737..803f246e1e262 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -5,6 +5,9 @@ completion.usage = 'source <(npm completion)' var npm = require('./npm.js') var npmconf = require('./config/core.js') var configDefs = npmconf.defs +const { aliases, cmdList, plumbing } = require('./config/cmd-list.js') +const aliasNames = Object.keys(aliases) +const fullList = cmdList.concat(aliasNames).filter(c => !plumbing.includes(c)) var configTypes = configDefs.types var shorthands = configDefs.shorthands var nopt = require('nopt') @@ -121,8 +124,9 @@ function completion (args, cb) { var parsed = opts.conf = nopt(configTypes, shorthands, partialWords.slice(0, -1), 0) // check if there's a command already. - console.error(parsed) + console.error('PARSED', parsed) var cmd = parsed.argv.remain[1] + console.error('CMD', cmd) if (!cmd) return cmdCompl(opts, cb) Object.keys(parsed).forEach(function (k) { @@ -244,5 +248,5 @@ function isFlag (word) { // complete against the npm commands function cmdCompl (opts, cb) { - return cb(null, npm.fullList) + return cb(null, fullList) } From a27ec2549140439acc822dec50873c4b2efccccb Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 21 Jul 2020 13:25:40 -0700 Subject: [PATCH 10/11] unit tests for npm run-script --- lib/run-script.js | 62 +++---- test/lib/run-script.js | 395 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 415 insertions(+), 42 deletions(-) create mode 100644 test/lib/run-script.js diff --git a/lib/run-script.js b/lib/run-script.js index 3564d1c9e3721..7cbcce2ded3e4 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 { 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 +}) From f115fa60f49d1c41a4fb21c4ba995807fd359d28 Mon Sep 17 00:00:00 2001 From: isaacs Date: Wed, 29 Jul 2020 14:42:41 -0700 Subject: [PATCH 11/11] lint fixes --- lib/run-script.js | 2 +- lib/test.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/run-script.js b/lib/run-script.js index 7cbcce2ded3e4..97fa26ecceee6 100644 --- a/lib/run-script.js +++ b/lib/run-script.js @@ -1,7 +1,7 @@ const run = require('@npmcli/run-script') const npm = require('./npm.js') const readJson = require('read-package-json-fast') -const { resolve, join } = require('path') +const { resolve } = require('path') const output = require('./utils/output.js') const log = require('npmlog') const usageUtil = require('./utils/usage') diff --git a/lib/test.js b/lib/test.js index 7ad76951a5fde..2c721575599b0 100644 --- a/lib/test.js +++ b/lib/test.js @@ -2,6 +2,7 @@ const testCmd = require('./utils/lifecycle-cmd.js')('test') const { completion, usage } = testCmd const cmd = (args, cb) => testCmd(args, er => { if (er && er.code === 'ELIFECYCLE') { + /* eslint-disable standard/no-callback-literal */ cb('Test failed. See above for more details.') } else { cb(er)