diff --git a/packages/info/src/index.ts b/packages/info/src/index.ts index 10fbafeeb6d..d62b6a3353a 100644 --- a/packages/info/src/index.ts +++ b/packages/info/src/index.ts @@ -32,7 +32,7 @@ const DEFAULT_DETAILS: Information = { export default async function info(...args): Promise { const cli = new WebpackCLI(); - const infoArgs = cli.commandLineArgs(options, { argv: args, stopAtFirstUnknown: false }); + const infoArgs = cli.argParser(options, args, true).opts; const envinfoConfig = {}; if (infoArgs._unknown && infoArgs._unknown.length > 0) { diff --git a/packages/info/src/options.ts b/packages/info/src/options.ts index 68bae2e8063..ccfe315e1b4 100644 --- a/packages/info/src/options.ts +++ b/packages/info/src/options.ts @@ -1,6 +1,7 @@ export default [ { name: 'output', + usage: '--output ', type: String, description: 'To get the output in specified format (accept json or markdown)', }, diff --git a/packages/serve/src/index.ts b/packages/serve/src/index.ts index 3c4587efb75..10d0ecfa799 100644 --- a/packages/serve/src/index.ts +++ b/packages/serve/src/index.ts @@ -1,7 +1,7 @@ import { devServer } from 'webpack-dev-server/bin/cli-flags'; import WebpackCLI from 'webpack-cli'; +import logger from 'webpack-cli/lib/utils/logger'; import startDevServer from './startDevServer'; -import argsToCamelCase from './args-to-camel-case'; /** * @@ -10,23 +10,30 @@ import argsToCamelCase from './args-to-camel-case'; * @param {String[]} args - args processed from the CLI * @returns {Function} invokes the devServer API */ -export default function serve(...args): void { +export default function serve(...args: string[]): void { const cli = new WebpackCLI(); const core = cli.getCoreFlags(); - // partial parsing usage: https://github.com/75lb/command-line-args/wiki/Partial-parsing - // since the webpack flags have the 'entry' option set as it's default option, - // we need to parse the dev server args first. Otherwise, the webpack parsing could snatch - // one of the dev server's options and set it to this 'entry' option. - // see: https://github.com/75lb/command-line-args/blob/master/doc/option-definition.md#optiondefaultoption--boolean - const devServerArgs = cli.commandLineArgs(devServer, { argv: args, partial: true }); - const webpackArgs = cli.commandLineArgs(core, { argv: devServerArgs._unknown || [], stopAtFirstUnknown: false }); - const finalArgs = argsToCamelCase(devServerArgs._all || {}); + const parsedDevServerArgs = cli.argParser(devServer, args, true); + const devServerArgs = parsedDevServerArgs.opts; + const parsedWebpackArgs = cli.argParser(core, parsedDevServerArgs.unknownArgs, true, process.title); + const webpackArgs = parsedWebpackArgs.opts; + // pass along the 'hot' argument to the dev server if it exists - if (webpackArgs && webpackArgs._all && typeof webpackArgs._all.hot !== 'undefined') { - finalArgs['hot'] = webpackArgs._all.hot; + if (webpackArgs && webpackArgs.hot !== undefined) { + devServerArgs['hot'] = webpackArgs.hot; + } + + if (parsedWebpackArgs.unknownArgs.length > 0) { + parsedWebpackArgs.unknownArgs + .filter((e) => e) + .forEach((unknown) => { + logger.warn('Unknown argument:', unknown); + }); + return; } + cli.getCompiler(webpackArgs, core).then((compiler): void => { - startDevServer(compiler, finalArgs); + startDevServer(compiler, devServerArgs); }); } diff --git a/packages/webpack-cli/__tests__/arg-parser.test.js b/packages/webpack-cli/__tests__/arg-parser.test.js new file mode 100644 index 00000000000..ab7600321d0 --- /dev/null +++ b/packages/webpack-cli/__tests__/arg-parser.test.js @@ -0,0 +1,201 @@ +const warnMock = jest.fn(); +jest.mock('../lib/utils/logger', () => { + return { + warn: warnMock, + }; +}); +jest.spyOn(process, 'exit').mockImplementation(() => {}); + +const argParser = require('../lib/utils/arg-parser'); +const { core } = require('../lib/utils/cli-flags'); + +const basicOptions = [ + { + name: 'bool-flag', + alias: 'b', + usage: '--bool-flag', + type: Boolean, + description: 'boolean flag', + }, + { + name: 'string-flag', + usage: '--string-flag ', + type: String, + description: 'string flag', + }, + { + name: 'string-flag-with-default', + usage: '--string-flag-with-default ', + type: String, + description: 'string flag', + defaultValue: 'default-value', + }, + { + name: 'custom-type-flag', + usage: '--custom-type-flag ', + type: (val) => { + return val.split(','); + }, + description: 'custom type flag', + }, +]; + +const helpAndVersionOptions = basicOptions.slice(0); +helpAndVersionOptions.push( + { + name: 'help', + usage: '--help', + type: Boolean, + description: 'help', + }, + { + name: 'version', + alias: 'v', + usage: '--version', + type: Boolean, + description: 'version', + }, +); + +describe('arg-parser', () => { + beforeEach(() => { + warnMock.mockClear(); + }); + + it('parses no flags', () => { + const res = argParser(basicOptions, [], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('parses basic flags', () => { + const res = argParser(basicOptions, ['--bool-flag', '--string-flag', 'val'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlag: 'val', + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('parses negated boolean flags', () => { + const res = argParser(basicOptions, ['--no-bool-flag'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: false, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('parses boolean flag alias', () => { + const res = argParser(basicOptions, ['-b'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('warns on usage of both flag and same negated flag, setting it to false', () => { + const res = argParser(basicOptions, ['--bool-flag', '--no-bool-flag'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: false, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(1); + expect(warnMock.mock.calls[0][0]).toContain('You provided both --bool-flag and --no-bool-flag'); + }); + + it('warns on usage of both flag and same negated flag, setting it to true', () => { + const res = argParser(basicOptions, ['--no-bool-flag', '--bool-flag'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(1); + expect(warnMock.mock.calls[0][0]).toContain('You provided both --bool-flag and --no-bool-flag'); + }); + + it('warns on usage of both flag alias and same negated flag, setting it to true', () => { + const res = argParser(basicOptions, ['--no-bool-flag', '-b'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(1); + expect(warnMock.mock.calls[0][0]).toContain('You provided both -b and --no-bool-flag'); + }); + + it('parses string flag using equals sign', () => { + const res = argParser(basicOptions, ['--string-flag=val'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + stringFlag: 'val', + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('handles additional node args from argv', () => { + const res = argParser(basicOptions, ['node', 'index.js', '--bool-flag', '--string-flag', 'val'], false); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlag: 'val', + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('handles unknown args', () => { + const res = argParser(basicOptions, ['--unknown-arg', '-b', 'no-leading-dashes'], true); + expect(res.unknownArgs).toEqual(['--unknown-arg', 'no-leading-dashes']); + expect(res.opts).toEqual({ + boolFlag: true, + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('handles custom type args', () => { + const res = argParser(basicOptions, ['--custom-type-flag', 'val1,val2,val3'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts).toEqual({ + customTypeFlag: ['val1', 'val2', 'val3'], + stringFlagWithDefault: 'default-value', + }); + expect(warnMock.mock.calls.length).toEqual(0); + }); + + it('calls help callback on --help', () => { + const helpCb = jest.fn(); + argParser(helpAndVersionOptions, ['--help'], true, '', helpCb); + expect(helpCb.mock.calls.length).toEqual(1); + expect(helpCb.mock.calls[0][0]).toEqual(['--help']); + }); + + it('calls version callback on --version', () => { + const versionCb = jest.fn(); + argParser(helpAndVersionOptions, ['--version'], true, '', () => {}, versionCb); + expect(versionCb.mock.calls.length).toEqual(1); + }); + + it('parses webpack args', () => { + const res = argParser(core, ['--entry', 'test.js', '--hot', '-o', './dist/', '--no-watch'], true); + expect(res.unknownArgs.length).toEqual(0); + expect(res.opts.entry).toEqual('test.js'); + expect(res.opts.hot).toBeTruthy(); + expect(res.opts.output).toEqual('./dist/'); + expect(res.opts.watch).toBeFalsy(); + expect(warnMock.mock.calls.length).toEqual(0); + }); +}); diff --git a/packages/webpack-cli/bin/cli.js b/packages/webpack-cli/bin/cli.js index 1bf0a1be092..34faad08445 100755 --- a/packages/webpack-cli/bin/cli.js +++ b/packages/webpack-cli/bin/cli.js @@ -3,7 +3,7 @@ 'use strict'; require('v8-compile-cache'); const importLocal = require('import-local'); -const parseArgs = require('../lib/utils/parse-args'); +const parseNodeArgs = require('../lib/utils/parse-node-args'); const runner = require('../lib/runner'); // Prefer the local installation of webpack-cli @@ -13,6 +13,6 @@ if (importLocal(__filename)) { process.title = 'webpack'; const [, , ...rawArgs] = process.argv; -const { cliArgs, nodeArgs } = parseArgs(rawArgs); +const { cliArgs, nodeArgs } = parseNodeArgs(rawArgs); runner(nodeArgs, cliArgs); diff --git a/packages/webpack-cli/lib/bootstrap.js b/packages/webpack-cli/lib/bootstrap.js index 485c129a259..860e1fbbeb8 100644 --- a/packages/webpack-cli/lib/bootstrap.js +++ b/packages/webpack-cli/lib/bootstrap.js @@ -2,12 +2,12 @@ const WebpackCLI = require('./webpack-cli'); const { core, commands } = require('./utils/cli-flags'); const logger = require('./utils/logger'); const cliExecuter = require('./utils/cli-executer'); - +const argParser = require('./utils/arg-parser'); require('./utils/process-log'); process.title = 'webpack-cli'; -const isFlagPresent = (args, flag) => args.find((arg) => [flag, `--${flag}`].includes(arg)); +// const isFlagPresent = (args, flag) => args.find((arg) => [flag, `--${flag}`].includes(arg)); const isArgCommandName = (arg, cmd) => arg === cmd.name || arg === cmd.alias; const removeCmdFromArgs = (args, cmd) => args.filter((arg) => !isArgCommandName(arg, cmd)); const normalizeFlags = (args, cmd) => { @@ -20,39 +20,21 @@ const isCommandUsed = (commands) => return process.argv.includes(cmd.name) || process.argv.includes(cmd.alias); }); -const resolveNegatedArgs = (args) => { - args._unknown.forEach((arg, idx) => { - if (arg.includes('--') || arg.includes('--no')) { - const argPair = arg.split('='); - const optName = arg.includes('--no') ? argPair[0].slice(5) : argPair[0].slice(2); - - let argValue = arg.includes('--no') ? 'false' : argPair[1]; - if (argValue === 'false') { - argValue = false; - } else if (argValue === 'true') { - argValue = true; - } - const cliFlag = core.find((opt) => opt.name === optName); - if (cliFlag) { - args[cliFlag.group][optName] = argValue; - args._all[optName] = argValue; - args._unknown[idx] = null; - } - } - }); -}; - async function runCLI(cli, commandIsUsed) { let args; - const helpFlagExists = isFlagPresent(process.argv, 'help'); - const versionFlagExists = isFlagPresent(process.argv, 'version') || isFlagPresent(process.argv, '-v'); + const runVersion = () => { + cli.runVersion(commandIsUsed); + }; + const parsedArgs = argParser(core, process.argv, false, process.title, cli.runHelp, runVersion); - if (helpFlagExists) { + if (parsedArgs.unknownArgs.includes('help')) { cli.runHelp(process.argv); - return; - } else if (versionFlagExists) { - cli.runVersion(commandIsUsed); - return; + process.exit(0); + } + + if (parsedArgs.unknownArgs.includes('version')) { + runVersion(); + process.exit(0); } if (commandIsUsed) { @@ -61,10 +43,16 @@ async function runCLI(cli, commandIsUsed) { return await cli.runCommand(commandIsUsed, ...args); } else { try { - args = cli.commandLineArgs(core, { stopAtFirstUnknown: false, partial: true }); - if (args._unknown) { - resolveNegatedArgs(args); - args._unknown + // handle the default webpack entry CLI argument, where instead + // of doing 'webpack-cli --entry ./index.js' you can simply do + // 'webpack-cli ./index.js' + // if the unknown arg starts with a '-', it will be considered + // an unknown flag rather than an entry + let entry; + if (parsedArgs.unknownArgs.length === 1 && !parsedArgs.unknownArgs[0].startsWith('-')) { + entry = parsedArgs.unknownArgs[0]; + } else if (parsedArgs.unknownArgs.length > 0) { + parsedArgs.unknownArgs .filter((e) => e) .forEach((unknown) => { logger.warn('Unknown argument:', unknown); @@ -72,7 +60,11 @@ async function runCLI(cli, commandIsUsed) { cliExecuter(); return; } - const result = await cli.run(args, core); + const parsedArgsOpts = parsedArgs.opts; + if (entry) { + parsedArgsOpts.entry = entry; + } + const result = await cli.run(parsedArgsOpts, core); if (!result) { return; } @@ -99,9 +91,8 @@ async function runCLI(cli, commandIsUsed) { const newArgKeys = Object.keys(argsMap).filter((arg) => !keysToDelete.includes(argsMap[arg].pos)); // eslint-disable-next-line require-atomic-updates process.argv = newArgKeys; - args = cli.commandLineArgs(core, { stopAtFirstUnknown: false, partial: true }); - - await cli.run(args, core); + args = argParser('', core, process.argv); + await cli.run(args.opts, core); process.stdout.write('\n'); logger.warn('Duplicate flags found, defaulting to last set value'); } else { diff --git a/packages/webpack-cli/lib/groups/AdvancedGroup.js b/packages/webpack-cli/lib/groups/AdvancedGroup.js index c3abafb23e4..dcedc86bf4c 100644 --- a/packages/webpack-cli/lib/groups/AdvancedGroup.js +++ b/packages/webpack-cli/lib/groups/AdvancedGroup.js @@ -1,5 +1,4 @@ const GroupHelper = require('../utils/GroupHelper'); -const logger = require('../utils/logger'); class AdvancedGroup extends GroupHelper { constructor(options) { @@ -77,59 +76,6 @@ class AdvancedGroup extends GroupHelper { if (args.target) { options.target = args.target; } - - if (args.global) { - const globalArrLen = args.global.length; - if (!globalArrLen) { - logger.warn('Argument to global flag is none'); - return; - } - if (globalArrLen === 1) { - logger.warn('Argument to global flag expected a key/value pair'); - return; - } - - const providePluginObject = {}; - args.global.forEach((arg, idx) => { - const isKey = idx % 2 === 0; - const isConcatArg = arg.includes('='); - if (isKey && isConcatArg) { - const splitIdx = arg.indexOf('='); - const argVal = arg.substr(splitIdx + 1); - const argKey = arg.substr(0, splitIdx); - if (!argVal.length) { - logger.warn(`Found unmatching value for global flag key '${argKey}'`); - return; - } - // eslint-disable-next-line no-prototype-builtins - if (providePluginObject.hasOwnProperty(argKey)) { - logger.warn(`Overriding key '${argKey}' for global flag`); - } - providePluginObject[argKey] = argVal; - return; - } - if (isKey) { - const nextArg = args.global[idx + 1]; - // eslint-disable-next-line no-prototype-builtins - if (providePluginObject.hasOwnProperty(arg)) { - logger.warn(`Overriding key '${arg}' for global flag`); - } - if (!nextArg) { - logger.warn(`Found unmatching value for global flag key '${arg}'`); - return; - } - providePluginObject[arg] = nextArg; - } - }); - - const { ProvidePlugin } = require('webpack'); - const globalVal = new ProvidePlugin(providePluginObject); - if (options && options.plugins) { - options.plugins.unshift(globalVal); - } else { - options.plugins = [globalVal]; - } - } } run() { this.resolveOptions(); diff --git a/packages/webpack-cli/lib/groups/OutputGroup.js b/packages/webpack-cli/lib/groups/OutputGroup.js index be24b101b1d..31de0e6dd66 100644 --- a/packages/webpack-cli/lib/groups/OutputGroup.js +++ b/packages/webpack-cli/lib/groups/OutputGroup.js @@ -1,5 +1,6 @@ const path = require('path'); const GroupHelper = require('../utils/GroupHelper'); +const logger = require('../utils/logger'); class OutputGroup extends GroupHelper { constructor(options) { @@ -27,7 +28,11 @@ class OutputGroup extends GroupHelper { const { args } = this; if (args) { const { output } = args; + if (!output) { + logger.warn( + "You provided an empty output value. Falling back to the output value of your webpack config file, or './dist/' if none was provided", + ); return; } const outputInfo = path.parse(output); diff --git a/packages/webpack-cli/lib/groups/StatsGroup.js b/packages/webpack-cli/lib/groups/StatsGroup.js index 36fc0e30ccd..b8654e8c5c5 100644 --- a/packages/webpack-cli/lib/groups/StatsGroup.js +++ b/packages/webpack-cli/lib/groups/StatsGroup.js @@ -23,6 +23,9 @@ class StatsGroup extends GroupHelper { this.opts.option.stats = { verbose: true, }; + } else if (!StatsGroup.validOptions().includes(this.args.stats)) { + logger.warn(`'${this.args.stats}' is invalid value for stats. Using 'normal' option for stats`); + this.opts.options.stats = 'normal'; } else { this.opts.options.stats = this.args.stats; } diff --git a/packages/webpack-cli/lib/groups/ZeroConfigGroup.js b/packages/webpack-cli/lib/groups/ZeroConfigGroup.js index 26c5daeed26..14f1e491d1f 100644 --- a/packages/webpack-cli/lib/groups/ZeroConfigGroup.js +++ b/packages/webpack-cli/lib/groups/ZeroConfigGroup.js @@ -20,7 +20,9 @@ class ZeroConfigGroup extends GroupHelper { if (process.env.NODE_ENV && (process.env.NODE_ENV === PRODUCTION || process.env.NODE_ENV === DEVELOPMENT)) { return process.env.NODE_ENV; } else { - if ((this.args.mode || this.args.noMode) && (this.args.dev || this.args.prod)) { + // commander sets mode to false if --no-mode is specified + const noMode = this.args.mode === false; + if ((this.args.mode || noMode) && (this.args.dev || this.args.prod)) { logger.warn( `You provided both ${this.args.mode ? 'mode' : 'no-mode'} and ${ this.args.prod ? '--prod' : '--dev' @@ -32,17 +34,19 @@ class ZeroConfigGroup extends GroupHelper { return NONE; } } - if (this.args.noMode && this.args.mode) { - logger.warn('You Provided both mode and no-mode arguments. You Should Provide just one. "mode" will be used.'); - } + if (this.args.mode) { + if (this.args.mode !== PRODUCTION && this.args.mode !== DEVELOPMENT && this.args.mode !== NONE) { + logger.warn('You provided an invalid value for "mode" option. Using "production" by default'); + return PRODUCTION; + } return this.args.mode; } if (this.args.prod) { return PRODUCTION; } else if (this.args.dev) { return DEVELOPMENT; - } else if (this.args.noMode) { + } else if (noMode) { return NONE; } return PRODUCTION; diff --git a/packages/webpack-cli/lib/utils/arg-parser.js b/packages/webpack-cli/lib/utils/arg-parser.js new file mode 100644 index 00000000000..6bdfc51f502 --- /dev/null +++ b/packages/webpack-cli/lib/utils/arg-parser.js @@ -0,0 +1,103 @@ +const commander = require('commander'); +const logger = require('./logger'); + +/** + * Creates Argument parser corresponding to the supplied options + * parse the args and return the result + * + * @param {object[]} options Array of objects with details about flags + * @param {string[]} args process.argv or it's subset + * @param {boolean} argsOnly false if all of process.argv has been provided, true if + * args is only a subset of process.argv that removes the first couple elements + */ +function argParser(options, args, argsOnly = false, name = '', helpFunction = undefined, versionFunction = undefined) { + const parser = new commander.Command(); + // Set parser name + parser.name(name); + + // Use customized version output if available + if (versionFunction) { + parser.on('option:version', () => { + versionFunction(); + process.exit(0); + }); + } + + // Use customized help output if available + if (helpFunction) { + parser.on('option:help', () => { + helpFunction(args); + process.exit(0); + }); + } + + // Allow execution if unknown arguments are present + parser.allowUnknownOption(true); + + // Register options on the parser + options.reduce((parserInstance, option) => { + const flags = option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`; + const flagsWithType = option.type !== Boolean ? flags + ' ' : flags; + if (option.type === Boolean || option.type === String) { + parserInstance.option(flagsWithType, option.description, option.defaultValue); + if (option.type === Boolean) { + // commander requires explicitly adding the negated version of boolean flags + const negatedFlag = `--no-${option.name}`; + parserInstance.option(negatedFlag, `negates ${option.name}`); + } + } else { + // in this case the type is a parsing function + parserInstance.option(flagsWithType, option.description, option.type, option.defaultValue); + } + + return parserInstance; + }, parser); + + // if we are parsing a subset of process.argv that includes + // only the arguments themselves (e.g. ['--option', 'value']) + // then we need from: 'user' passed into commander parse + // otherwise we are parsing a full process.argv + // (e.g. ['node', '/path/to/...', '--option', 'value']) + const parseOptions = argsOnly ? { from: 'user' } : {}; + + const result = parser.parse(args, parseOptions); + const opts = result.opts(); + + const unknownArgs = result.args; + + args.forEach((arg) => { + const flagName = arg.slice(5); + const option = options.find((opt) => opt.name === flagName); + const flag = `--${flagName}`; + const flagUsed = args.includes(flag) && !unknownArgs.includes(flag); + let alias = ''; + let aliasUsed = false; + if (option && option.alias) { + alias = `-${option.alias}`; + aliasUsed = args.includes(alias) && !unknownArgs.includes(alias); + } + + // this is a negated flag that is not an unknown flag, but the flag + // it is negating was also provided + if (arg.startsWith('--no-') && (flagUsed || aliasUsed) && !unknownArgs.includes(arg)) { + logger.warn( + `You provided both ${ + flagUsed ? flag : alias + } and ${arg}. We will use only the last of these flags that you provided in your CLI arguments`, + ); + } + }); + + Object.keys(opts).forEach((key) => { + if (opts[key] === undefined) { + delete opts[key]; + } + }); + + return { + unknownArgs, + opts, + }; +} + +module.exports = argParser; diff --git a/packages/webpack-cli/lib/utils/cli-flags.js b/packages/webpack-cli/lib/utils/cli-flags.js index 28b7a832f2e..33d752deb84 100644 --- a/packages/webpack-cli/lib/utils/cli-flags.js +++ b/packages/webpack-cli/lib/utils/cli-flags.js @@ -1,6 +1,3 @@ -const logger = require('../utils/logger'); -const StatsGroup = require('../groups/StatsGroup'); - const HELP_GROUP = 'help'; const CONFIG_GROUP = 'config'; const BASIC_GROUP = 'basic'; @@ -147,15 +144,6 @@ module.exports = { description: 'Load a given plugin', link: 'https://webpack.js.org/plugins/', }, - { - name: 'global', - usage: '--global myVar ./global.js', - alias: 'g', - type: String, - multiple: true, - group: ADVANCED_GROUP, - description: 'Declares and exposes a global variable', - }, { name: 'target', usage: '--target', @@ -232,14 +220,7 @@ module.exports = { { name: 'mode', usage: '--mode ', - type: (value) => { - if (value === 'development' || value === 'production' || value === 'none') { - return value; - } else { - logger.warn('You provided an invalid value for "mode" option.'); - return 'production'; - } - }, + type: String, group: ZERO_CONFIG_GROUP, description: 'Defines the mode to pass to webpack', link: 'https://webpack.js.org/concepts/#mode', @@ -271,13 +252,8 @@ module.exports = { { name: 'stats', usage: '--stats verbose', - type: (value) => { - if (StatsGroup.validOptions().includes(value)) { - return value; - } - logger.warn('No value recognised for "stats" option'); - return 'normal'; - }, + type: String, + defaultValue: 'normal', group: DISPLAY_GROUP, description: 'It instructs webpack on how to treat the stats', link: 'https://webpack.js.org/configuration/stats/#stats', diff --git a/packages/webpack-cli/lib/utils/helpers.js b/packages/webpack-cli/lib/utils/helpers.js new file mode 100644 index 00000000000..4a3fd9f3382 --- /dev/null +++ b/packages/webpack-cli/lib/utils/helpers.js @@ -0,0 +1,10 @@ +/** + * Convert camelCase to kebab-case + * @param {string} str input string in camelCase + * @returns {string} output string in kebab-case + */ +function toKebabCase(str) { + return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); +} + +module.exports = { toKebabCase }; diff --git a/packages/webpack-cli/lib/utils/parse-args.js b/packages/webpack-cli/lib/utils/parse-node-args.js similarity index 100% rename from packages/webpack-cli/lib/utils/parse-args.js rename to packages/webpack-cli/lib/utils/parse-node-args.js diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index 12e1b925229..a141e82ddbd 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -4,7 +4,8 @@ const GroupHelper = require('./utils/GroupHelper'); const { Compiler } = require('./utils/Compiler'); const { groups, core } = require('./utils/cli-flags'); const webpackMerge = require('webpack-merge'); -const commandArgs = require('command-line-args'); +const { toKebabCase } = require('./utils/helpers'); +const argParser = require('./utils/arg-parser'); const defaultCommands = { init: 'init', @@ -36,12 +37,12 @@ class WebpackCLI extends GroupHelper { this.outputConfiguration = {}; } setMappedGroups(args, inlineOptions) { - const { _all } = args; - Object.keys(_all).forEach((key) => { - this.setGroupMap(key, _all[key], inlineOptions); + Object.keys(args).forEach((key) => { + this.setGroupMap(toKebabCase(key), args[key], inlineOptions); }); } setGroupMap(key, val, inlineOptions) { + if (val === undefined) return; const opt = inlineOptions.find((opt) => opt.name === key); const groupName = opt.group; if (this.groupMap.has(groupName)) { @@ -81,14 +82,18 @@ class WebpackCLI extends GroupHelper { return options; } - // It exposes "command-line-args" function - commandLineArgs(...args) { - return commandArgs(...args); + /** + * Expose commander argParser + * @param {...any} args args for argParser + */ + argParser(...args) { + return argParser(...args); } getCoreFlags() { return core; } + /** * Based on the parsed keys, the function will import and create * a group that handles respective values diff --git a/packages/webpack-cli/package.json b/packages/webpack-cli/package.json index bf205953041..397fd074556 100644 --- a/packages/webpack-cli/package.json +++ b/packages/webpack-cli/package.json @@ -27,8 +27,8 @@ "@webpack-cli/info": "^1.0.1-alpha.4", "ansi-escapes": "^4.3.1", "chalk": "^3.0.0", - "command-line-args": "^5.1.1", "command-line-usage": "^6.1.0", + "commander": "^5.0.0", "enquirer": "^2.3.4", "execa": "^4.0.0", "import-local": "^3.0.2", diff --git a/test/config/basic/basic-config.test.js b/test/config/basic/basic-config.test.js index a98e3492652..01feb8afb94 100644 --- a/test/config/basic/basic-config.test.js +++ b/test/config/basic/basic-config.test.js @@ -5,8 +5,12 @@ const { run } = require('../../utils/test-utils'); describe('basic config file', () => { it('is able to understand and parse a very basic configuration file', (done) => { - const { stdout, stderr } = run(__dirname, ['-c', resolve(__dirname, 'webpack.config.js'), '--output', './binary/a.bundle.js']); - expect(stderr).toContain('Duplicate flags found, defaulting to last set value'); + const { stdout, stderr } = run( + __dirname, + ['-c', resolve(__dirname, 'webpack.config.js'), '--output', './binary/a.bundle.js'], + false, + ); + expect(stderr).toBeFalsy(); expect(stdout).not.toBe(undefined); stat(resolve(__dirname, './binary/a.bundle.js'), (err, stats) => { expect(err).toBe(null); diff --git a/test/defaults/output-defaults.test.js b/test/defaults/output-defaults.test.js index 947c4ba2366..8e04eb511ee 100644 --- a/test/defaults/output-defaults.test.js +++ b/test/defaults/output-defaults.test.js @@ -5,18 +5,18 @@ const { run } = require('../utils/test-utils'); describe('output flag defaults', () => { it('should create default file for a given directory', (done) => { - run(__dirname, ['--entry', './a.js', '--output', './binary'], false); - + const { stdout } = run(__dirname, ['--entry', './a.js', '--output', './binary'], false); + // Should print a warning about config fallback since we did not supply --defaults + expect(stdout).toContain('option has not been set, webpack will fallback to'); stat(resolve(__dirname, './binary/main.js'), (err, stats) => { expect(err).toBe(null); expect(stats.isFile()).toBe(true); done(); }); }); - it('set default output directory on empty flag', (done) => { - const { stdout } = run(__dirname, ['--entry', './a.js', '--output'], false); - // Should print a warning about config fallback since we did not supply --defaults - expect(stdout).toContain('option has not been set, webpack will fallback to'); + + it('set default output directory on no output flag', (done) => { + run(__dirname, ['--entry', './a.js'], false); stat(resolve(__dirname, './dist/main.js'), (err, stats) => { expect(err).toBe(null); @@ -24,9 +24,16 @@ describe('output flag defaults', () => { done(); }); }); + + it('throw error on empty output flag', () => { + const { stderr } = run(__dirname, ['--entry', './a.js', '--output'], false); + expect(stderr).toContain("error: option '-o, --output ' argument missing"); + }); + it('should not throw when --defaults flag is passed', (done) => { - const { stderr } = run(__dirname, ['--defaults'], false); + const { stdout, stderr } = run(__dirname, ['--defaults'], false); // When using --defaults it should not print warnings about config fallback + expect(stdout).not.toContain('option has not been set, webpack will fallback to'); expect(stderr).toBeFalsy(); stat(resolve(__dirname, './dist/main.js'), (err, stats) => { expect(err).toBe(null); diff --git a/test/entry/defaults-index/entry-multi-args.test.js b/test/entry/defaults-index/entry-multi-args.test.js index abc7a291a98..270e0f8a94d 100644 --- a/test/entry/defaults-index/entry-multi-args.test.js +++ b/test/entry/defaults-index/entry-multi-args.test.js @@ -19,7 +19,7 @@ describe('single entry flag index present', () => { it('finds default index file, compiles and overrides with flags successfully', (done) => { const { stderr } = run(__dirname, ['--output', 'bin/main.js']); - expect(stderr).toContain('Duplicate flags found, defaulting to last set value'); + expect(stderr).toBeFalsy(); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { expect(err).toBe(null); diff --git a/test/global/global.test.js b/test/global/global.test.js deleted file mode 100644 index 3e284d1df01..00000000000 --- a/test/global/global.test.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const path = require('path'); -const execa = require('execa'); -const { sync: spawnSync } = execa; - -const { run } = require('../utils/test-utils'); - -describe('global flag', () => { - it('warns if there are no arguments to flag', () => { - const { stderr } = run(__dirname, ['--global']); - expect(stderr).toContain('Argument to global flag is none'); - }); - - it('warns if there are no value for key', () => { - const { stderr } = run(__dirname, ['--global', 'myVar']); - expect(stderr).toContain('Argument to global flag expected a key/value pair'); - }); - - it('is able to inject one variable to global scope', () => { - const { stdout } = run(__dirname, ['--global', 'myVar', './global1.js']); - expect(stdout).toContain('option has not been set, webpack will fallback to'); - const executable = path.join(__dirname, './bin/main.js'); - const bundledScript = spawnSync('node', [executable]); - expect(bundledScript.stdout).toEqual('myVar ./global1.js'); - }); - - it('is able to inject multiple variables to global scope', () => { - const { stdout } = run(__dirname, ['--global', 'myVar', './global1.js', '--global', 'myVar2', './global2.js']); - expect(stdout).toContain('option has not been set, webpack will fallback to'); - const executable = path.join(__dirname, './bin/main.js'); - const bundledScript = spawnSync('node', [executable]); - expect(bundledScript.stdout).toEqual('myVar ./global1.js\nmyVar ./global2.js'); - }); - - it('understands = syntax', () => { - const { stdout } = run(__dirname, ['--global', 'myVar', './global1.js', '--global', 'myVar2=./global2.js']); - expect(stdout).toContain('option has not been set, webpack will fallback to'); - const executable = path.join(__dirname, './bin/main.js'); - const bundledScript = spawnSync('node', [executable]); - expect(bundledScript.stdout).toEqual('myVar ./global1.js\nmyVar ./global2.js'); - }); - - it('warns on multiple flags that are inconsistent', () => { - const result = run(__dirname, ['--global', 'myVar', './global1.js', '--global', 'myVar2']); - // eslint-disable-next-line - expect(result.stderr).toContain("Found unmatching value for global flag key 'myVar2'"); - - const result2 = run(__dirname, ['--global', 'myVar', './global1.js', '--global', 'myVar2=']); - // eslint-disable-next-line - expect(result2.stderr).toContain("Found unmatching value for global flag key 'myVar2'"); - }); -}); diff --git a/test/global/global1.js b/test/global/global1.js deleted file mode 100644 index e7d024ed70b..00000000000 --- a/test/global/global1.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'myVar ./global1.js'; diff --git a/test/global/global2.js b/test/global/global2.js deleted file mode 100644 index c80fbb2b620..00000000000 --- a/test/global/global2.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'myVar ./global2.js'; diff --git a/test/global/index.js b/test/global/index.js deleted file mode 100644 index c832cbbeaeb..00000000000 --- a/test/global/index.js +++ /dev/null @@ -1,5 +0,0 @@ -console.log(myVar); - -try { - console.log(myVar2); -} catch(e) {} diff --git a/test/init/generator/init-inquirer.test.js b/test/init/generator/init-inquirer.test.js index 0402c8edf72..2c97c9fd71b 100644 --- a/test/init/generator/init-inquirer.test.js +++ b/test/init/generator/init-inquirer.test.js @@ -22,7 +22,7 @@ describe('init', () => { }); it('should scaffold when given answers', async () => { - const stdout = await runPromptWithAnswers(genPath, ['init'], ['N', ENTER, ENTER, ENTER, ENTER, ENTER, ENTER, ENTER]); + const { stdout } = await runPromptWithAnswers(genPath, ['init'], ['N', ENTER, ENTER, ENTER, ENTER, ENTER, ENTER, ENTER]); expect(stdout).toBeTruthy(); expect(stdout).toContain(firstPrompt); diff --git a/test/loader/loader.test.js b/test/loader/loader.test.js index 0aec72825d8..49e625a9a5e 100644 --- a/test/loader/loader.test.js +++ b/test/loader/loader.test.js @@ -31,7 +31,7 @@ describe('loader command', () => { }); it('should scaffold loader template with a given name', async () => { - const stdout = await runPromptWithAnswers(__dirname, ['loader'], [loaderName, ENTER]); + const { stdout } = await runPromptWithAnswers(__dirname, ['loader'], [loaderName, ENTER]); expect(stdout).toContain(firstPrompt); diff --git a/test/mode/prod/prod.test.js b/test/mode/prod/prod.test.js index fe5a6cff333..78353586582 100644 --- a/test/mode/prod/prod.test.js +++ b/test/mode/prod/prod.test.js @@ -27,7 +27,7 @@ describe('mode flags', () => { it('should load a production config when --mode=abcd is passed', (done) => { const { stderr, stdout } = run(__dirname, ['--mode', 'abcd']); - expect(stderr).toContain('invalid value for "mode"'); + expect(stderr).toContain('invalid value for "mode" option. Using "production" by default'); expect(stdout).toBeTruthy(); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { expect(err).toBe(null); diff --git a/test/no-mode/no-mode.test.js b/test/no-mode/no-mode.test.js index 5eff202d82e..0ec137128bb 100644 --- a/test/no-mode/no-mode.test.js +++ b/test/no-mode/no-mode.test.js @@ -38,10 +38,13 @@ describe('no-mode flag', () => { }); }); - it('should load a production config when --mode=production & --no-mode are passed', (done) => { + it('should load a none config when --mode=production is passed before --no-mode', (done) => { const { stderr, stdout } = run(__dirname, ['--mode', 'production', '--no-mode']); - expect(stderr).toContain('"mode" will be used'); + expect(stderr).toContain( + 'You provided both --mode and --no-mode. We will use only the last of these flags that you provided in your CLI arguments', + ); expect(stdout).toBeTruthy(); + expect(stdout).not.toContain('main.js.map'); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { expect(err).toBe(null); @@ -50,10 +53,13 @@ describe('no-mode flag', () => { }); }); - it('should load a development config when --mode=development and --no-mode are passed', (done) => { - const { stderr, stdout } = run(__dirname, ['--mode', 'development', '--no-mode']); - expect(stderr).toContain('"mode" will be used'); + it('should load a none config when --mode=production is passed after --no-mode', (done) => { + const { stderr, stdout } = run(__dirname, ['--no-mode', '--mode', 'production']); + expect(stderr).toContain( + 'You provided both --mode and --no-mode. We will use only the last of these flags that you provided in your CLI arguments', + ); expect(stdout).toBeTruthy(); + expect(stdout).toContain('main.js.map'); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { expect(err).toBe(null); diff --git a/test/node/node.test.js b/test/node/node.test.js index 1db66c623c3..aa26dd7527e 100644 --- a/test/node/node.test.js +++ b/test/node/node.test.js @@ -2,10 +2,10 @@ const { stat } = require('fs'); const { resolve } = require('path'); const { run } = require('../utils/test-utils'); -const parseArgs = require('../../packages/webpack-cli/lib/utils/parse-args'); +const parseNodeArgs = require('../../packages/webpack-cli/lib/utils/parse-node-args'); describe('node flags', () => { - it('parseArgs helper must work correctly', () => { + it('parseNodeArgs helper must work correctly', () => { [ { rawArgs: ['--foo', '--bar', '--baz=quux'], @@ -32,7 +32,7 @@ describe('node flags', () => { expectedNodeArgs: ['--name1=value1', '--name2="value2"', '--name3', 'value3', '-k', 'v'], }, ].map(({ rawArgs, expectedNodeArgs, expectedCliArgs }) => { - const { nodeArgs, cliArgs } = parseArgs(rawArgs); + const { nodeArgs, cliArgs } = parseNodeArgs(rawArgs); expect(nodeArgs).toEqual(expectedNodeArgs); expect(cliArgs).toEqual(expectedCliArgs); }); diff --git a/test/output/named-bundles/output-named-bundles.test.js b/test/output/named-bundles/output-named-bundles.test.js index 82966106b53..7a53fb80202 100644 --- a/test/output/named-bundles/output-named-bundles.test.js +++ b/test/output/named-bundles/output-named-bundles.test.js @@ -1,59 +1,81 @@ 'use strict'; -const { stat } = require('fs'); -const { resolve } = require('path'); +const { statSync } = require('fs'); +const { join, resolve } = require('path'); +const rimraf = require('rimraf'); const { run } = require('../../utils/test-utils'); describe('output flag named bundles', () => { - it('should output file given as flag instead of in configuration', (done) => { - run(__dirname, ['-c', resolve(__dirname, 'webpack.config.js'), '--output', './binary/a.bundle.js'], false); - stat(resolve(__dirname, './binary/a.bundle.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - done(); - }); + const clean = () => { + rimraf.sync(join(__dirname, 'bin')); + rimraf.sync(join(__dirname, 'dist')); + rimraf.sync(join(__dirname, 'binary')); + }; + + beforeEach(clean); + + afterAll(clean); + + it('should output file given as flag instead of in configuration', () => { + const { stderr } = run(__dirname, ['-c', resolve(__dirname, 'webpack.config.js'), '--output', './binary/a.bundle.js'], false); + expect(stderr).toBeFalsy(); + + const stats = statSync(resolve(__dirname, './binary/a.bundle.js')); + expect(stats.isFile()).toBe(true); }); - it('should create multiple bundles with an overriding flag', (done) => { - run(__dirname, ['-c', resolve(__dirname, 'webpack.single.config.js'), '--output', './bin/[name].bundle.js'], false); - - stat(resolve(__dirname, './bin/b.bundle.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - stat(resolve(__dirname, './bin/c.bundle.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - done(); + it('should create multiple bundles with an overriding flag', () => { + const { stderr } = run( + __dirname, + ['-c', resolve(__dirname, 'webpack.single.config.js'), '--output', './bin/[name].bundle.js'], + false, + ); + expect(stderr).toBeFalsy(); + + let stats = statSync(resolve(__dirname, './bin/b.bundle.js')); + expect(stats.isFile()).toBe(true); + stats = statSync(resolve(__dirname, './bin/c.bundle.js')); + expect(stats.isFile()).toBe(true); }); - it('should not throw error on same bundle name for multiple entries with defaults', (done) => { + it('should not throw error on same bundle name for multiple entries with defaults', () => { const { stderr } = run(__dirname, ['-c', resolve(__dirname, 'webpack.defaults.config.js'), '--defaults'], false); + expect(stderr).toBeFalsy(); + + let stats = statSync(resolve(__dirname, './dist/b.main.js')); + expect(stats.isFile()).toBe(true); + stats = statSync(resolve(__dirname, './dist/c.main.js')); + expect(stats.isFile()).toBe(true); + }); + + it('should successfully compile multiple entries', () => { + const { stderr } = run(__dirname, ['-c', resolve(__dirname, 'webpack.multiple.config.js')], false); + expect(stderr).toBeFalsy(); + + let stats = statSync(resolve(__dirname, './bin/b.bundle.js')); + expect(stats.isFile()).toBe(true); + stats = statSync(resolve(__dirname, './bin/c.bundle.js')); + expect(stats.isFile()).toBe(true); + }); + + it('should output file in bin directory using default webpack config with warning for empty output value', () => { + const { stderr } = run(__dirname, ['--output='], false); + expect(stderr).toContain( + "You provided an empty output value. Falling back to the output value of your webpack config file, or './dist/' if none was provided", + ); - expect(stderr).toBe(''); - - stat(resolve(__dirname, './dist/b.main.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - stat(resolve(__dirname, './dist/c.main.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - done(); + const stats = statSync(resolve(__dirname, './bin/bundle.js')); + expect(stats.isFile()).toBe(true); }); - it('should successfully compile multiple entries', (done) => { - run(__dirname, ['-c', resolve(__dirname, 'webpack.multiple.config.js')], false); - - stat(resolve(__dirname, './bin/b.bundle.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - stat(resolve(__dirname, './bin/c.bundle.js'), (err, stats) => { - expect(err).toBe(null); - expect(stats.isFile()).toBe(true); - }); - done(); + it('should output file in dist directory using default value with warning for empty output value', () => { + const { stderr } = run(__dirname, ['-c', resolve(__dirname, 'webpack.defaults.config.js'), '--defaults', '--output='], false); + expect(stderr).toContain( + "You provided an empty output value. Falling back to the output value of your webpack config file, or './dist/' if none was provided", + ); + + let stats = statSync(resolve(__dirname, './dist/b.main.js')); + expect(stats.isFile()).toBe(true); + stats = statSync(resolve(__dirname, './dist/c.main.js')); + expect(stats.isFile()).toBe(true); }); }); diff --git a/test/serve/basic/serve-basic.test.js b/test/serve/basic/serve-basic.test.js index 8fc66398e7c..bb32dfd1846 100644 --- a/test/serve/basic/serve-basic.test.js +++ b/test/serve/basic/serve-basic.test.js @@ -48,7 +48,7 @@ describe('basic serve usage', () => { it('throws error on unknown flag', async () => { const { stdout, stderr } = await runServe(['--port', port, '--unknown-flag'], testPath); expect(stdout).toHaveLength(0); - expect(stderr).toContain('Unknown option: --unknown-flag'); + expect(stderr).toContain('Unknown argument: --unknown-flag'); }); } }); diff --git a/test/stats/stats.test.js b/test/stats/stats.test.js index 7eca9cf0e2e..656a6633825 100644 --- a/test/stats/stats.test.js +++ b/test/stats/stats.test.js @@ -48,7 +48,7 @@ describe('stats flag', () => { it('should warn when an unknown flag stats value is passed', () => { const { stderr, stdout } = run(__dirname, ['--stats', 'foo']); expect(stderr).toBeTruthy(); - expect(stderr).toContain('No value recognised for "stats" option'); + expect(stderr).toContain('invalid value for stats'); expect(stdout).toBeTruthy(); }); }); diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 78d049c9693..d107e50de74 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -67,17 +67,24 @@ function runWatch({ testCase, args = [], setOutput = true, outputKillStr = 'Time }); } -function runAndGetWatchProc(testCase, args = [], setOutput = true) { +function runAndGetWatchProc(testCase, args = [], setOutput = true, input = '', forcePipe = false) { const cwd = path.resolve(testCase); const outputPath = path.resolve(testCase, 'bin'); const argsWithOutput = setOutput ? args.concat('--output', outputPath) : args; - const webpackProc = execa(WEBPACK_PATH, argsWithOutput, { + const options = { cwd, reject: false, - stdio: ENABLE_LOG_COMPILATION ? 'inherit' : 'pipe', - }); + stdio: ENABLE_LOG_COMPILATION && !forcePipe ? 'inherit' : 'pipe', + }; + + // some tests don't work if the input option is an empty string + if (input) { + options.input = input; + } + + const webpackProc = execa(WEBPACK_PATH, argsWithOutput, options); return webpackProc; } @@ -86,8 +93,8 @@ function runAndGetWatchProc(testCase, args = [], setOutput = true) { * @param {string} location location of current working directory * @param {string[]} answers answers to be passed to stdout for inquirer question */ -const runPromptWithAnswers = async (location, args, answers) => { - const runner = runAndGetWatchProc(location, args, false); +const runPromptWithAnswers = (location, args, answers) => { + const runner = runAndGetWatchProc(location, args, false, '', true); runner.stdin.setDefaultEncoding('utf-8'); // Simulate answers by sending the answers after waiting for 2s @@ -102,14 +109,33 @@ const runPromptWithAnswers = async (location, args, answers) => { }); }, Promise.resolve()); - await simulateAnswers.then(() => { + simulateAnswers.then(() => { runner.stdin.end(); }); return new Promise((resolve) => { + const obj = {}; + let stdoutDone = false; + let stderrDone = false; runner.stdout.pipe( concat((result) => { - resolve(result.toString()); + stdoutDone = true; + obj.stdout = result.toString(); + if (stderrDone) { + runner.kill('SIGKILL'); + resolve(obj); + } + }), + ); + + runner.stderr.pipe( + concat((result) => { + stderrDone = true; + obj.stderr = result.toString(); + if (stdoutDone) { + runner.kill('SIGKILL'); + resolve(obj); + } }), ); });