diff --git a/packages/serve/src/index.ts b/packages/serve/src/index.ts index f9d6c6eef00..5242d35b985 100644 --- a/packages/serve/src/index.ts +++ b/packages/serve/src/index.ts @@ -1,5 +1,5 @@ import { devServer } from "webpack-dev-server/bin/cli-flags"; -import WebpackCLI from "webpack-cli"; +import WebpackCLI from "../../webpack-cli"; import startDevServer from "./startDevServer"; import argsToCamelCase from "./args-to-camel-case"; @@ -10,23 +10,33 @@ 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(): 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 || {}); - // 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; - } - cli.getCompiler(webpackArgs, core).then((compiler): void => { + const filteredArgs = process.argv.filter(arg => (arg != "serve")); + const devServerArgs = cli.argParser(devServer, filteredArgs); + const webpackArgs = cli.argParser(core, filteredArgs, process.title, cli.runHelp, cli.runVersion); + const finalArgs = argsToCamelCase(devServerArgs.opts() || {}); + + + // pass along the 'hot' argument to the dev server if it exists + if (webpackArgs && webpackArgs.opts() && webpackArgs.opts().hot !== undefined) { + finalArgs['hot'] = webpackArgs.opts().hot; + } + + Object.keys(finalArgs).forEach(arg => { + if (finalArgs[arg] === undefined) { + delete finalArgs[arg]; + } + }); + + if (webpackArgs.args.length > 0) { + process.stderr.write(`Unknown option: ${webpackArgs.args}`); + return; + } + + cli.getCompiler(webpackArgs.opts(), core).then((compiler): void => { startDevServer(compiler, finalArgs); }); } diff --git a/packages/webpack-cli/lib/bootstrap.js b/packages/webpack-cli/lib/bootstrap.js index f014721148f..116951f2b3c 100644 --- a/packages/webpack-cli/lib/bootstrap.js +++ b/packages/webpack-cli/lib/bootstrap.js @@ -2,7 +2,7 @@ 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'; @@ -21,7 +21,7 @@ const isCommandUsed = commands => }); const resolveNegatedArgs = args => { - args._unknown.forEach((arg, idx) => { + args.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); @@ -36,23 +36,23 @@ const resolveNegatedArgs = args => { if (cliFlag) { args[cliFlag.group][optName] = argValue; args._all[optName] = argValue; - args._unknown[idx] = null; + args.args[idx] = null; } } }); }; async function runCLI(cli, commandIsUsed) { - let args; - const helpFlagExists = isFlagPresent(process.argv, 'help'); - const versionFlagExists = isFlagPresent(process.argv, 'version'); + const parsedArgs = argParser(core, process.argv, process.title, cli.runHelp, cli.runVersion); - if (helpFlagExists) { + if (parsedArgs.args.includes('help')) { cli.runHelp(process.argv); - return; - } else if (versionFlagExists) { + process.exit(0); + } + + if (parsedArgs.args.includes('version')) { cli.runVersion(); - return; + process.exit(0); } if (commandIsUsed) { @@ -61,10 +61,9 @@ 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 + if (parsedArgs.args.length > 0) { + resolveNegatedArgs(parsedArgs.args); + parsedArgs.args .filter(e => e) .forEach(unknown => { logger.warn('Unknown argument:', unknown); @@ -72,7 +71,7 @@ async function runCLI(cli, commandIsUsed) { cliExecuter(); return; } - const result = await cli.run(args, core); + const result = await cli.run(parsedArgs.opts(), core); if (!result) { return; } @@ -99,9 +98,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..98dea3e4e4b 100644 --- a/packages/webpack-cli/lib/groups/AdvancedGroup.js +++ b/packages/webpack-cli/lib/groups/AdvancedGroup.js @@ -77,59 +77,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..97e3ba0d2af 100644 --- a/packages/webpack-cli/lib/groups/OutputGroup.js +++ b/packages/webpack-cli/lib/groups/OutputGroup.js @@ -27,7 +27,10 @@ class OutputGroup extends GroupHelper { const { args } = this; if (args) { const { output } = args; - if (!output) { + // TODO: Remove comment before merge + // We need to show warning when empty output flag is supplied + // which is set to boolean true by commander + if (!output || output === true) { 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 1091dd2d288..a3cd98e52ff 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 1d0e36f2c65..55033fb4f6e 100644 --- a/packages/webpack-cli/lib/groups/ZeroConfigGroup.js +++ b/packages/webpack-cli/lib/groups/ZeroConfigGroup.js @@ -20,7 +20,7 @@ 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)) { + if ((this.args.mode !== undefined ) && (this.args.dev || this.args.prod)) { logger.warn( `You provided both ${this.args.mode ? 'mode' : 'no-mode'} and ${ this.args.prod ? '--prod' : '--dev' @@ -32,12 +32,12 @@ 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.'); + return PRODUCTION; + } return this.args.mode; } if (this.args.prod) { 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..4fa97f4133a --- /dev/null +++ b/packages/webpack-cli/lib/utils/arg-parser.js @@ -0,0 +1,45 @@ +const commander = require("commander"); + +/** + * 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 + */ +function argParser(options, args, 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(args); + process.exit(0); + }); + } + + // Use customised help output is avaliable + 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 + ' [type]' : flags; + parserInstance.option(flagsWithType, option.description, option.defaultValue); + return parserInstance; + }, parser); + + return parser.parse(args); +} + +module.exports = argParser; diff --git a/packages/webpack-cli/lib/utils/cli-flags.js b/packages/webpack-cli/lib/utils/cli-flags.js index dc90ea2873c..176aa153211 100644 --- a/packages/webpack-cli/lib/utils/cli-flags.js +++ b/packages/webpack-cli/lib/utils/cli-flags.js @@ -197,15 +197,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', alias: 't', @@ -281,14 +272,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' diff --git a/packages/webpack-cli/lib/utils/helpers.js b/packages/webpack-cli/lib/utils/helpers.js new file mode 100644 index 00000000000..34fdd0a55d4 --- /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/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index 9c6fa362b14..fcf0adafd09 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)) { @@ -82,15 +83,19 @@ class WebpackCLI extends GroupHelper { } /** - * It exposes "command-line-args" function + * Expose commander argParser + * @param {...any} args args for argParser */ - commandLineArgs(...args) { - return commandArgs(...args); + 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 45cbb4edd17..1431bc18506 100644 --- a/packages/webpack-cli/package.json +++ b/packages/webpack-cli/package.json @@ -27,8 +27,8 @@ "@webpack-cli/package-utils": "^1.0.1-alpha.4", "ansi-escapes": "^4.2.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": "^3.2.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 aeebd213a63..6eba772f6b0 100644 --- a/test/config/basic/basic-config.test.js +++ b/test/config/basic/basic-config.test.js @@ -6,7 +6,7 @@ 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'); + 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/entry/defaults-index/entry-multi-args.test.js b/test/entry/defaults-index/entry-multi-args.test.js index e8462a4f75a..862d33965d9 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/no-mode/no-mode.test.js b/test/no-mode/no-mode.test.js index c043e9f4740..762716c633f 100644 --- a/test/no-mode/no-mode.test.js +++ b/test/no-mode/no-mode.test.js @@ -40,7 +40,7 @@ describe('no-mode flag', () => { it('should load a production config when --mode=production & --no-mode are passed', done => { const { stderr, stdout } = run(__dirname, ['--mode', 'production', '--no-mode']); - expect(stderr).toContain('"mode" will be used'); + expect(stderr).toBeFalsy(); expect(stdout).toBeTruthy(); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { @@ -52,7 +52,7 @@ 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'); + expect(stderr).toBeFalsy(); expect(stdout).toBeTruthy(); stat(resolve(__dirname, './bin/main.js'), (err, stats) => { diff --git a/test/stats/stats.test.js b/test/stats/stats.test.js index b0b27c278ad..fb69293d7ff 100644 --- a/test/stats/stats.test.js +++ b/test/stats/stats.test.js @@ -44,7 +44,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(); }); });