From 15d7333f832e3d68ae16895569f27a27ef86573e Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 19 Nov 2020 11:30:40 -0800 Subject: [PATCH] Use @npmcli/run-script for exec, explore; add interactive exec This removes all other arg/shell escaping mechanisms, as they are no longer needed, and will be un-done by puka in @npmcli/run-script anyway. Adds an interactive shell mode when `npm exec` is run without any arguments, allowing users to interactively test out commands in an npm script environment. Previously, this would do nothing, and just exit. Prevent weird behavior from `npm explore ../blah`. `explore` now can _only_ be used to explore packages that are actually found in the relevant `node_modules` folder. --- docs/content/commands/npm-exec.md | 7 + lib/exec.js | 41 +++-- lib/explore.js | 76 ++++---- lib/utils/escape-arg.js | 18 -- lib/utils/escape-exec-path.js | 21 --- test/lib/exec.js | 35 +++- test/lib/explore.js | 285 ++++++++++++++++++----------- test/lib/utils/escape-arg.js | 15 -- test/lib/utils/escape-exec-path.js | 15 -- 9 files changed, 293 insertions(+), 220 deletions(-) delete mode 100644 lib/utils/escape-arg.js delete mode 100644 lib/utils/escape-exec-path.js delete mode 100644 test/lib/utils/escape-arg.js delete mode 100644 test/lib/utils/escape-exec-path.js diff --git a/docs/content/commands/npm-exec.md b/docs/content/commands/npm-exec.md index c9de9933be3a5..9f4abb02d4031 100644 --- a/docs/content/commands/npm-exec.md +++ b/docs/content/commands/npm-exec.md @@ -16,9 +16,12 @@ npx [@] [args...] npx -p [@] [args...] npx -c ' [args...]' npx -p [@] -c ' [args...]' +Run without --call or positional args to open interactive subshell + alias: npm x, npx +common options: --package= (may be specified multiple times) -p is a shorthand for --package only when using npx executable -c --call= (may not be mixed with positional arguments) @@ -30,6 +33,10 @@ 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. + 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 diff --git a/lib/exec.js b/lib/exec.js index 6bcaf838ed327..e7af55ec68422 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -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' + @@ -13,7 +12,9 @@ const usage = usageUtil('exec', 'npx [@] [args...]\n' + 'npx -p [@] [args...]\n' + 'npx -c \' [args...]\'\n' + - 'npx -p [@] -c \' [args...]\'', + 'npx -p [@] -c \' [args...]\'' + + '\n' + + 'Run without --call or positional args to open interactive subshell\n', '\n--package= (may be specified multiple times)\n' + '-p is a shorthand for --package only when using npx executable\n' + @@ -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 @@ -84,6 +84,7 @@ const run = async ({ args, call, pathArr }) => { npm.log.disableProgress() try { return await runScript({ + ...npm.flatOptions, pkg, banner: false, // we always run in cwd, not --prefix @@ -101,13 +102,24 @@ 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) { + output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`) + 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 @@ -126,8 +138,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, }) } @@ -187,9 +200,13 @@ const exec = async args => { 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 (!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${ @@ -205,7 +222,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) => { diff --git a/lib/explore.js b/lib/explore.js index 96221cb4973d7..e9b09707ec63b 100644 --- a/lib/explore.js +++ b/lib/explore.js @@ -4,59 +4,65 @@ const usageUtil = require('./utils/usage.js') const completion = require('./utils/completion/installed-shallow.js') const usage = usageUtil('explore', 'npm explore [ -- ]') +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() } } diff --git a/lib/utils/escape-arg.js b/lib/utils/escape-arg.js deleted file mode 100644 index 135d380fc2d5a..0000000000000 --- a/lib/utils/escape-arg.js +++ /dev/null @@ -1,18 +0,0 @@ -const { normalize } = require('path') -const isWindows = require('./is-windows.js') - -/* -Escape the name of an executable suitable for passing to the system shell. - -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. 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 = str => isWindows ? '"' + normalize(str) + '"' - : /[^-_.~/\w]/.test(str) ? "'" + str.replace(/'/g, '\'"\'"\'') + "'" - : str diff --git a/lib/utils/escape-exec-path.js b/lib/utils/escape-exec-path.js deleted file mode 100644 index 83c70680b4da2..0000000000000 --- a/lib/utils/escape-exec-path.js +++ /dev/null @@ -1,21 +0,0 @@ -const { normalize } = require('path') -const isWindows = require('./is-windows.js') - -/* -Escape the name of an executable suitable for passing to the system shell. - -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. 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'`. -*/ - -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, '\'"\'"\'') + "'" - : str diff --git a/test/lib/exec.js b/test/lib/exec.js index c65f916428d96..8a0b38f3b5139 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -1,6 +1,8 @@ const t = require('tap') const requireInject = require('require-inject') const { resolve, delimiter } = require('path') +const OUTPUT = [] +const output = (...msg) => OUTPUT.push(msg) const ARB_CTOR = [] const ARB_ACTUAL_TREE = {} @@ -29,6 +31,7 @@ const npm = { call: '', package: [], legacyPeerDeps: false, + shell: 'shell-cmd', }, localPrefix: 'local-prefix', localBin: 'local-bin', @@ -91,6 +94,7 @@ const mocks = { pacote, read, 'mkdirp-infer-owner': mkdirp, + '../../lib/utils/output.js': output, } const exec = requireInject('../../lib/exec.js', mocks) @@ -123,7 +127,7 @@ t.test('npx foo, bin already exists locally', async t => { await exec(['foo'], er => { t.ifError(er, 'npm exec') }) - t.strictSame(RUN_SCRIPTS, [{ + t.match(RUN_SCRIPTS, [{ pkg: { scripts: { npx: 'foo' }}, banner: false, path: process.cwd(), @@ -147,7 +151,7 @@ t.test('npx foo, bin already exists globally', async t => { await exec(['foo'], er => { t.ifError(er, 'npm exec') }) - t.strictSame(RUN_SCRIPTS, [{ + t.match(RUN_SCRIPTS, [{ pkg: { scripts: { npx: 'foo' }}, banner: false, path: process.cwd(), @@ -193,6 +197,33 @@ t.test('npm exec foo, already present locally', async t => { }]) }) +t.test('npm exec , run interactive shell', async t => { + ARB_CTOR.length = 0 + MKDIRPS.length = 0 + ARB_REIFY.length = 0 + OUTPUT.length = 0 + await exec([], er => { + if (er) + throw er + }) + t.strictSame(OUTPUT, [ + ['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'], + ], 'printed message about interactive shell') + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist') + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'shell-cmd' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) +}) + t.test('npm exec foo, not present locally or in central loc', async t => { const path = t.testdir() const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') diff --git a/test/lib/explore.js b/test/lib/explore.js index 64c70bcce7ef6..23eab1172a05e 100644 --- a/test/lib/explore.js +++ b/test/lib/explore.js @@ -1,53 +1,70 @@ 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 RPJ_ERROR = null +let RPJ_CALLED = '' +const mockRPJ = async path => { + if (RPJ_ERROR) { + try { + throw RPJ_ERROR + } finally { + RPJ_ERROR = null + } + } + RPJ_CALLED = path + return {some: 'package'} } -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') +let RUN_SCRIPT_ERROR = null +let RUN_SCRIPT_EXIT_CODE = 0 +let RUN_SCRIPT_SIGNAL = null +let RUN_SCRIPT_EXEC = null +const mockRunScript = ({ pkg, banner, path, event, stdio }) => { + if (event !== '_explore') + throw new Error('got wrong event name') - if (SPAWN_ERROR) - return Promise.reject(SPAWN_ERROR) + RUN_SCRIPT_EXEC = pkg.scripts._explore - SPAWN_SHELL_EXEC = sh - SPAWN_SHELL_ARGS = shellArgs - return Promise.resolve({ code: SPAWN_EXIT_CODE }) + if (RUN_SCRIPT_ERROR) { + try { + return Promise.reject(RUN_SCRIPT_ERROR) + } finally { + RUN_SCRIPT_ERROR = null + } + } + + if (RUN_SCRIPT_EXIT_CODE || RUN_SCRIPT_SIGNAL) { + return Promise.reject(Object.assign(new Error('command failed'), { + code: RUN_SCRIPT_EXIT_CODE, + signal: RUN_SCRIPT_SIGNAL, + })) + } + + return Promise.resolve({ code: 0, signal: null }) } const output = [] let ERROR_HANDLER_CALLED = null +const logs = [] 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, - }, + 'read-package-json-fast': mockRPJ, '../../lib/npm.js': { dir: windows ? 'c:\\npm\\dir' : '/npm/dir', + log: { + error: (...msg) => logs.push(msg), + disableProgress: () => {}, + enableProgress: () => {}, + }, flatOptions: { shell: 'shell-command', }, }, - '@npmcli/promise-spawn': mockSpawn, + '@npmcli/run-script': mockRunScript, '../../lib/utils/output.js': out => { output.push(out) }, @@ -68,14 +85,12 @@ t.test('basic interactive', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: 'c:\\npm\\dir\\pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: [], + RPJ_CALLED: 'c:\\npm\\dir\\pkg\\package.json', + RUN_SCRIPT_EXEC: 'shell-command', }) t.strictSame(output, [ "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n", @@ -88,14 +103,12 @@ t.test('basic interactive', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: '/npm/dir/pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: [], + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'shell-command', }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", @@ -109,11 +122,11 @@ t.test('interactive tracks exit code', t => { const { exitCode } = process t.beforeEach((cb) => { process.exitCode = exitCode - SPAWN_EXIT_CODE = 99 + RUN_SCRIPT_EXIT_CODE = 99 cb() }) t.afterEach((cb) => { - SPAWN_EXIT_CODE = 0 + RUN_SCRIPT_EXIT_CODE = 0 output.length = 0 process.exitCode = exitCode cb() @@ -125,14 +138,12 @@ t.test('interactive tracks exit code', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: 'c:\\npm\\dir\\pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: [], + RPJ_CALLED: 'c:\\npm\\dir\\pkg\\package.json', + RUN_SCRIPT_EXEC: 'shell-command', }) t.strictSame(output, [ "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n", @@ -146,14 +157,12 @@ t.test('interactive tracks exit code', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: '/npm/dir/pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: [], + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'shell-command', }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", @@ -162,16 +171,11 @@ t.test('interactive tracks exit code', t => { })) t.test('posix spawn fail', t => { - t.teardown(() => { - SPAWN_ERROR = null - }) - SPAWN_ERROR = Object.assign(new Error('glorb'), { + RUN_SCRIPT_ERROR = Object.assign(new Error('glorb'), { code: 33, }) return posixExplore(['pkg'], er => { - if (er) - throw er - + t.match(er, { message: 'glorb', code: 33 }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) @@ -179,6 +183,32 @@ t.test('interactive tracks exit code', t => { }) }) + t.test('posix spawn fail, 0 exit code', t => { + RUN_SCRIPT_ERROR = Object.assign(new Error('glorb'), { + code: 0, + }) + return posixExplore(['pkg'], er => { + t.match(er, { message: 'glorb', code: 0 }) + t.strictSame(output, [ + "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", + ]) + t.equal(process.exitCode, 1) + }) + }) + + t.test('posix spawn fail, no exit code', t => { + RUN_SCRIPT_ERROR = Object.assign(new Error('command failed'), { + code: 'EPROBLEM', + }) + return posixExplore(['pkg'], er => { + t.match(er, { message: 'command failed', code: 'EPROBLEM' }) + t.strictSame(output, [ + "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", + ]) + t.equal(process.exitCode, 1) + }) + }) + t.end() }) @@ -194,19 +224,12 @@ t.test('basic non-interactive', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: 'c:\\npm\\dir\\pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: [ - '/d', - '/s', - '/c', - '"ls"', - ], + RPJ_CALLED: 'c:\\npm\\dir\\pkg\\package.json', + RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) })) @@ -217,14 +240,12 @@ t.test('basic non-interactive', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: '/npm/dir/pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: ['-c', 'ls'], + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) })) @@ -232,33 +253,93 @@ t.test('basic non-interactive', t => { t.end() }) -t.test('usage if no pkg provided', t => { - t.teardown(() => { +t.test('signal fails non-interactive', t => { + const { exitCode } = process + t.afterEach((cb) => { output.length = 0 - ERROR_HANDLER_CALLED = null + logs.length = 0 + cb() }) - t.plan(1) - posixExplore([], er => { - if (er) - throw er + + t.beforeEach(cb => { + RUN_SCRIPT_SIGNAL = 'SIGPROBLEM' + RUN_SCRIPT_EXIT_CODE = null + process.exitCode = exitCode + cb() + }) + t.afterEach(cb => { + process.exitCode = exitCode + cb() + }) + + t.test('windows', t => windowsExplore(['pkg', 'ls'], er => { + t.match(er, { + message: 'command failed', + signal: 'SIGPROBLEM', + }) t.strictSame({ - ERROR_HANDLER_CALLED: null, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { - ERROR_HANDLER_CALLED: null, - STAT_CALLED: '/npm/dir/pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: ['-c', 'ls'], + RPJ_CALLED: 'c:\\npm\\dir\\pkg\\package.json', + RUN_SCRIPT_EXEC: 'ls', + }) + t.strictSame(output, []) + })) + + t.test('posix', t => posixExplore(['pkg', 'ls'], er => { + t.match(er, { + message: 'command failed', + signal: 'SIGPROBLEM', }) - }).catch(er => t.equal(er, 'npm explore [ -- ]')) + + t.strictSame({ + RPJ_CALLED, + RUN_SCRIPT_EXEC, + }, { + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'ls', + }) + t.strictSame(output, []) + })) + + t.end() +}) + +t.test('usage if no pkg provided', t => { + t.teardown(() => { + output.length = 0 + ERROR_HANDLER_CALLED = null + }) + const noPkg = [ + [], + ['foo/../..'], + ['asdf/..'], + ['.'], + ['..'], + ['../..'], + ] + t.plan(noPkg.length) + for (const args of noPkg) { + t.test(JSON.stringify(args), t => posixExplore(args, er => { + t.equal(er, 'npm explore [ -- ]') + t.strictSame({ + ERROR_HANDLER_CALLED: null, + RPJ_CALLED, + RUN_SCRIPT_EXEC, + }, { + ERROR_HANDLER_CALLED: null, + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'ls', + }) + })) + } }) t.test('pkg not installed', t => { - STAT_ERROR = new Error('plurple') - t.plan(1) + RPJ_ERROR = new Error('plurple') + t.plan(2) posixExplore(['pkg', 'ls'], er => { if (er) @@ -266,17 +347,17 @@ t.test('pkg not installed', t => { t.strictSame({ ERROR_HANDLER_CALLED, - STAT_CALLED, - SPAWN_SHELL_EXEC, - SPAWN_SHELL_ARGS, + RPJ_CALLED, + RUN_SCRIPT_EXEC, }, { ERROR_HANDLER_CALLED: null, - STAT_CALLED: '/npm/dir/pkg', - SPAWN_SHELL_EXEC: 'shell-command', - SPAWN_SHELL_ARGS: ['-c', 'ls'], + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) }).catch(er => { - t.match(er, { message: `It doesn't look like pkg is installed.` }) + t.match(er, { message: 'plurple' }) + t.match(logs, [['explore', `It doesn't look like pkg is installed.`]]) + logs.length = 0 }) }) diff --git a/test/lib/utils/escape-arg.js b/test/lib/utils/escape-arg.js deleted file mode 100644 index b80a63f0b877b..0000000000000 --- a/test/lib/utils/escape-arg.js +++ /dev/null @@ -1,15 +0,0 @@ -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'`) diff --git a/test/lib/utils/escape-exec-path.js b/test/lib/utils/escape-exec-path.js deleted file mode 100644 index f16c576ec5550..0000000000000 --- a/test/lib/utils/escape-exec-path.js +++ /dev/null @@ -1,15 +0,0 @@ -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'`)