diff --git a/packages/webpack-cli/__tests__/resolveAdvanced.test.js b/packages/webpack-cli/__tests__/resolveAdvanced.test.js deleted file mode 100644 index 390f22bc995..00000000000 --- a/packages/webpack-cli/__tests__/resolveAdvanced.test.js +++ /dev/null @@ -1,37 +0,0 @@ -const resolveAdvanced = require('../lib/groups/resolveAdvanced'); - -const targetValues = ['web', 'webworker', 'node', 'async-node', 'node-webkit', 'electron-main', 'electron-renderer', 'electron-preload']; - -describe('advanced options', function () { - it('should load the HMR plugin', async () => { - const result = await resolveAdvanced({ - hot: true, - }); - expect(result.options.plugins[0].constructor.name).toEqual('HotModuleReplacementPlugin'); - }); - - it('should load the prefetch plugin', async () => { - const result = await resolveAdvanced({ - prefetch: 'url', - }); - expect(result.options.plugins[0].constructor.name).toEqual('PrefetchPlugin'); - }); - - it('should load the webpack-bundle-analyzer plugin', async () => { - const result = await resolveAdvanced({ - analyze: true, - }); - expect(result.options.plugins[0].constructor.name).toEqual('BundleAnalyzerPlugin'); - }); - - { - targetValues.map((option) => { - it(`should handle ${option} option`, async () => { - const result = await resolveAdvanced({ - target: option, - }); - expect(result.options.target).toEqual(option); - }); - }); - } -}); diff --git a/packages/webpack-cli/__tests__/resolveArgs.test.js b/packages/webpack-cli/__tests__/resolveArgs.test.js new file mode 100644 index 00000000000..d5347ba23fd --- /dev/null +++ b/packages/webpack-cli/__tests__/resolveArgs.test.js @@ -0,0 +1,142 @@ +const { resolve } = require('path'); +const webpackCLI = require('../lib/webpack-cli'); + +const targetValues = ['web', 'webworker', 'node', 'async-node', 'node-webkit', 'electron-main', 'electron-renderer', 'electron-preload']; + +const basicResolver = new webpackCLI().resolveArgs; + +describe('BasicResolver', () => { + it('should handle the output option', async () => { + const result = await basicResolver({ + outputPath: './bundle', + }); + expect(result.options.output.path).toEqual(resolve('bundle')); + }); + + it('should handle the mode option [production]', async () => { + const result = await basicResolver( + { + mode: 'production', + }, + {}, + ); + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'production' }); + expect(result.options.mode).toEqual('production'); + }); + + it('should handle the mode option [development]', async () => { + const result = await basicResolver( + { + mode: 'development', + }, + {}, + ); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'development' }); + expect(result.options.mode).toEqual('development'); + }); + + it('should handle the mode option [none]', async () => { + const result = await basicResolver( + { + mode: 'none', + }, + {}, + ); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'none' }); + expect(result.options.mode).toEqual('none'); + }); + + it('should prefer supplied move flag over NODE_ENV', async () => { + process.env.NODE_ENV = 'production'; + const result = await basicResolver( + { + mode: 'development', + }, + {}, + ); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'development' }); + }); + + it('should prefer supplied move flag over mode from config', async () => { + const result = await basicResolver( + { + mode: 'development', + }, + { mode: 'production' }, + ); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'development' }); + }); + + it('should prefer mode form config over NODE_ENV', async () => { + process.env.NODE_ENV = 'development'; + const result = await basicResolver({}, { mode: 'production' }); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'production' }); + }); + + it('should prefer mode form flag over NODE_ENV and config', async () => { + process.env.NODE_ENV = 'development'; + const result = await basicResolver({ mode: 'none' }, { mode: 'production' }); + + // ensure no other properties are added + expect(result.options).toMatchObject({ mode: 'none' }); + }); + + it('should assign json correctly', async () => { + const result = await basicResolver({ + json: true, + }); + expect(result.options.stats).toBeFalsy(); + expect(result.outputOptions.json).toBeTruthy(); + }); + + it('should assign stats correctly', async () => { + const result = await basicResolver({ + stats: 'warning', + }); + expect(result.options.stats).toEqual('warning'); + expect(result.outputOptions.json).toBeFalsy(); + }); + + it('should load the HMR plugin', async () => { + const result = await basicResolver({ + hot: true, + }); + expect(result.options.plugins[0].constructor.name).toEqual('HotModuleReplacementPlugin'); + }); + + it('should load the prefetch plugin', async () => { + const result = await basicResolver({ + prefetch: 'url', + }); + expect(result.options.plugins[0].constructor.name).toEqual('PrefetchPlugin'); + }); + + it('should load the webpack-bundle-analyzer plugin', async () => { + const result = await basicResolver({ + analyze: true, + }); + expect(result.options.plugins[0].constructor.name).toEqual('BundleAnalyzerPlugin'); + }); + + { + targetValues.map((option) => { + it(`should handle ${option} option`, async () => { + const result = await basicResolver({ + target: option, + }); + expect(result.options.target).toEqual(option); + }); + }); + } +}); diff --git a/packages/webpack-cli/__tests__/resolveMode.test.js b/packages/webpack-cli/__tests__/resolveMode.test.js deleted file mode 100644 index 3db6f889273..00000000000 --- a/packages/webpack-cli/__tests__/resolveMode.test.js +++ /dev/null @@ -1,82 +0,0 @@ -const resolveMode = require('../lib/groups/resolveMode'); - -describe('resolveMode', function () { - it('should handle the mode option [production]', () => { - const result = resolveMode( - { - mode: 'production', - }, - {}, - ); - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'production' }); - expect(result.options.mode).toEqual('production'); - }); - - it('should handle the mode option [development]', () => { - const result = resolveMode( - { - mode: 'development', - }, - {}, - ); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'development' }); - expect(result.options.mode).toEqual('development'); - }); - - it('should handle the mode option [none]', () => { - const result = resolveMode( - { - mode: 'none', - }, - {}, - ); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'none' }); - expect(result.options.mode).toEqual('none'); - }); - - it('should prefer supplied move flag over NODE_ENV', () => { - process.env.NODE_ENV = 'production'; - const result = resolveMode( - { - mode: 'development', - }, - {}, - ); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'development' }); - }); - - it('should prefer supplied move flag over mode from config', () => { - const result = resolveMode( - { - mode: 'development', - }, - { mode: 'production' }, - ); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'development' }); - }); - - it('should prefer mode form config over NODE_ENV', () => { - process.env.NODE_ENV = 'development'; - const result = resolveMode({}, { mode: 'production' }); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'production' }); - }); - - it('should prefer mode form flag over NODE_ENV and config', () => { - process.env.NODE_ENV = 'development'; - const result = resolveMode({ mode: 'none' }, { mode: 'production' }); - - // ensure no other properties are added - expect(result.options).toMatchObject({ mode: 'none' }); - }); -}); diff --git a/packages/webpack-cli/__tests__/resolveOutput.test.js b/packages/webpack-cli/__tests__/resolveOutput.test.js deleted file mode 100644 index 3522d5b1068..00000000000 --- a/packages/webpack-cli/__tests__/resolveOutput.test.js +++ /dev/null @@ -1,11 +0,0 @@ -const { resolve } = require('path'); -const resolveOutput = require('../lib/groups/resolveOutput'); - -describe('OutputGroup', function () { - it('should handle the output option', () => { - const result = resolveOutput({ - outputPath: './bundle', - }); - expect(result.options.output.path).toEqual(resolve('bundle')); - }); -}); diff --git a/packages/webpack-cli/__tests__/resolveStats.js b/packages/webpack-cli/__tests__/resolveStats.js deleted file mode 100644 index 2b71a292960..00000000000 --- a/packages/webpack-cli/__tests__/resolveStats.js +++ /dev/null @@ -1,19 +0,0 @@ -const resolveStats = require('../lib/groups/resolveStats'); - -describe('StatsGroup', function () { - it('should assign json correctly', () => { - const result = resolveStats({ - json: true, - }); - expect(result.options.stats).toBeFalsy(); - expect(result.outputOptions.json).toBeTruthy(); - }); - - it('should assign stats correctly', () => { - const result = resolveStats({ - stats: 'warning', - }); - expect(result.options.stats).toEqual('warning'); - expect(result.outputOptions.json).toBeFalsy(); - }); -}); diff --git a/packages/webpack-cli/lib/groups/basicResolver.js b/packages/webpack-cli/lib/groups/basicResolver.js deleted file mode 100644 index f4e548f7ae6..00000000000 --- a/packages/webpack-cli/lib/groups/basicResolver.js +++ /dev/null @@ -1,40 +0,0 @@ -const { core, groups } = require('../utils/cli-flags'); - -const WEBPACK_OPTION_FLAGS = core - .filter((coreFlag) => { - return coreFlag.group === groups.BASIC_GROUP; - }) - .reduce((result, flagObject) => { - result.push(flagObject.name); - if (flagObject.alias) { - result.push(flagObject.alias); - } - return result; - }, []); - -function resolveArgs(args) { - const finalOptions = { - options: {}, - outputOptions: {}, - }; - Object.keys(args).forEach((arg) => { - if (WEBPACK_OPTION_FLAGS.includes(arg)) { - finalOptions.outputOptions[arg] = args[arg]; - } - if (arg === 'devtool') { - finalOptions.options.devtool = args[arg]; - } - if (arg === 'name') { - finalOptions.options.name = args[arg]; - } - if (arg === 'watch') { - finalOptions.options.watch = true; - } - if (arg === 'entry') { - finalOptions.options[arg] = args[arg]; - } - }); - return finalOptions; -} - -module.exports = resolveArgs; diff --git a/packages/webpack-cli/lib/groups/resolveMode.js b/packages/webpack-cli/lib/groups/resolveMode.js deleted file mode 100644 index 2e3a8a15acd..00000000000 --- a/packages/webpack-cli/lib/groups/resolveMode.js +++ /dev/null @@ -1,49 +0,0 @@ -const PRODUCTION = 'production'; -const DEVELOPMENT = 'development'; - -/* -Mode priority: - - Mode flag - - Mode from config - - Mode form NODE_ENV -*/ - -/** - * - * @param {string} mode - mode flag value - * @param {Object} configObject - contains relevant loaded config - */ -const assignMode = (mode, configObject) => { - const { - env: { NODE_ENV }, - } = process; - const { mode: configMode } = configObject; - let finalMode; - if (mode) { - finalMode = mode; - } else if (configMode) { - finalMode = configMode; - } else if (NODE_ENV && (NODE_ENV === PRODUCTION || NODE_ENV === DEVELOPMENT)) { - finalMode = NODE_ENV; - } else { - finalMode = PRODUCTION; - } - return { mode: finalMode }; -}; - -/** - * - * @param {Object} args - parsedArgs from the CLI - * @param {Object | Array} configOptions - Contains loaded config or array of configs - */ -const resolveMode = (args, configOptions) => { - const { mode } = args; - let resolvedMode; - if (Array.isArray(configOptions)) { - resolvedMode = configOptions.map((configObject) => assignMode(mode, configObject)); - } else resolvedMode = assignMode(mode, configOptions); - - return { options: resolvedMode }; -}; - -module.exports = resolveMode; diff --git a/packages/webpack-cli/lib/groups/resolveOutput.js b/packages/webpack-cli/lib/groups/resolveOutput.js deleted file mode 100644 index b658a9e8a55..00000000000 --- a/packages/webpack-cli/lib/groups/resolveOutput.js +++ /dev/null @@ -1,19 +0,0 @@ -const path = require('path'); - -/** - * Resolves the output flag - * @param {args} args - Parsed arguments passed to the CLI - */ -const resolveOutput = (args) => { - const { outputPath } = args; - const finalOptions = { - options: { output: {} }, - outputOptions: {}, - }; - if (outputPath) { - finalOptions.options.output.path = path.resolve(outputPath); - } - return finalOptions; -}; - -module.exports = resolveOutput; diff --git a/packages/webpack-cli/lib/groups/resolveStats.js b/packages/webpack-cli/lib/groups/resolveStats.js deleted file mode 100644 index be538f0352b..00000000000 --- a/packages/webpack-cli/lib/groups/resolveStats.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Resolve flags which deal with compilation stats - * @param {args} args - Parsed args passed to CLI - */ -const resolveStats = (args) => { - const { stats, json } = args; - - const finalOptions = { - options: {}, - outputOptions: {}, - }; - - if (stats !== undefined) { - finalOptions.options.stats = stats; - } - if (json) { - finalOptions.outputOptions.json = json; - } - return finalOptions; -}; - -module.exports = resolveStats; diff --git a/packages/webpack-cli/lib/utils/arg-parser.js b/packages/webpack-cli/lib/utils/arg-parser.js index dd239e6e2b2..73dbfb350c0 100644 --- a/packages/webpack-cli/lib/utils/arg-parser.js +++ b/packages/webpack-cli/lib/utils/arg-parser.js @@ -16,7 +16,6 @@ const { defaultCommands } = require('./cli-flags'); */ const argParser = (options, args, argsOnly = false, name = '') => { const parser = new commander.Command(); - // Set parser name parser.name(name); parser.storeOptionsAsProperties(false); diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index cb6f15999ae..d661a7cb865 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -1,21 +1,19 @@ +const path = require('path'); const packageExists = require('./utils/package-exists'); const webpack = packageExists('webpack') ? require('webpack') : undefined; -const logger = require('./utils/logger'); const webpackMerge = require('webpack-merge'); -const { core, coreFlagMap } = require('./utils/cli-flags'); +const { writeFileSync } = require('fs'); +const { options: coloretteOptions, yellow } = require('colorette'); + +const logger = require('./utils/logger'); +const { core, groups, coreFlagMap } = require('./utils/cli-flags'); const argParser = require('./utils/arg-parser'); const assignFlagDefaults = require('./utils/flag-defaults'); -const { writeFileSync } = require('fs'); -const { options: coloretteOptions } = require('colorette'); const WebpackCLIPlugin = require('./plugins/WebpackCLIPlugin'); +const promptInstallation = require('./utils/prompt-installation'); // CLI arg resolvers const handleConfigResolution = require('./groups/resolveConfig'); -const resolveMode = require('./groups/resolveMode'); -const resolveStats = require('./groups/resolveStats'); -const resolveOutput = require('./groups/resolveOutput'); -const basicResolver = require('./groups/basicResolver'); -const resolveAdvanced = require('./groups/resolveAdvanced'); const toKebabCase = require('./utils/to-kebab-case'); class WebpackCLI { @@ -48,6 +46,145 @@ class WebpackCLI { this._mergeOptionsToConfiguration(configWithDefaults); } + async resolveArgs(args, configOptions = {}) { + // Since color flag has a default value, when there are no other args then exit + // eslint-disable-next-line no-prototype-builtins + if (Object.keys(args).length === 1 && args.hasOwnProperty('color') && !process.env.NODE_ENV) return {}; + + const { outputPath, stats, json, mode, target, prefetch, hot, analyze } = args; + const finalOptions = { + options: {}, + outputOptions: {}, + }; + + const WEBPACK_OPTION_FLAGS = core + .filter((coreFlag) => { + return coreFlag.group === groups.BASIC_GROUP; + }) + .reduce((result, flagObject) => { + result.push(flagObject.name); + if (flagObject.alias) { + result.push(flagObject.alias); + } + return result; + }, []); + + const PRODUCTION = 'production'; + const DEVELOPMENT = 'development'; + + /* + Mode priority: + - Mode flag + - Mode from config + - Mode form NODE_ENV + */ + + /** + * + * @param {string} mode - mode flag value + * @param {Object} configObject - contains relevant loaded config + */ + const assignMode = (mode, configObject) => { + const { + env: { NODE_ENV }, + } = process; + const { mode: configMode } = configObject; + let finalMode; + if (mode) { + finalMode = mode; + } else if (configMode) { + finalMode = configMode; + } else if (NODE_ENV && (NODE_ENV === PRODUCTION || NODE_ENV === DEVELOPMENT)) { + finalMode = NODE_ENV; + } else { + finalMode = PRODUCTION; + } + return finalMode; + }; + + Object.keys(args).forEach((arg) => { + if (WEBPACK_OPTION_FLAGS.includes(arg)) { + finalOptions.outputOptions[arg] = args[arg]; + } + if (arg === 'devtool') { + finalOptions.options.devtool = args[arg]; + } + if (arg === 'name') { + finalOptions.options.name = args[arg]; + } + if (arg === 'watch') { + finalOptions.options.watch = true; + } + if (arg === 'entry') { + finalOptions.options[arg] = args[arg]; + } + }); + if (outputPath) { + finalOptions.options.output = { path: path.resolve(outputPath) }; + } + + if (stats !== undefined) { + finalOptions.options.stats = stats; + } + if (json) { + finalOptions.outputOptions.json = json; + } + + if (hot) { + const { HotModuleReplacementPlugin } = require('webpack'); + const hotModuleVal = new HotModuleReplacementPlugin(); + if (finalOptions.options && finalOptions.options.plugins) { + finalOptions.options.plugins.unshift(hotModuleVal); + } else { + finalOptions.options.plugins = [hotModuleVal]; + } + } + if (prefetch) { + const { PrefetchPlugin } = require('webpack'); + const prefetchVal = new PrefetchPlugin(null, args.prefetch); + if (finalOptions.options && finalOptions.options.plugins) { + finalOptions.options.plugins.unshift(prefetchVal); + } else { + finalOptions.options.plugins = [prefetchVal]; + } + } + if (analyze) { + if (packageExists('webpack-bundle-analyzer')) { + // eslint-disable-next-line node/no-extraneous-require + const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); + const bundleAnalyzerVal = new BundleAnalyzerPlugin(); + if (finalOptions.options && finalOptions.options.plugins) { + finalOptions.options.plugins.unshift(bundleAnalyzerVal); + } else { + finalOptions.options.plugins = [bundleAnalyzerVal]; + } + } else { + await promptInstallation('webpack-bundle-analyzer', () => { + logger.error(`It looks like ${yellow('webpack-bundle-analyzer')} is not installed.`); + }) + .then(() => logger.success(`${yellow('webpack-bundle-analyzer')} was installed sucessfully.`)) + .catch(() => { + logger.error(`Action Interrupted, Please try once again or install ${yellow('webpack-bundle-analyzer')} manually.`); + process.exit(2); + }); + } + } + if (target) { + finalOptions.options.target = args.target; + } + + if (Array.isArray(configOptions)) { + // Todo - handle multi config for all flags + finalOptions.options = configOptions.map(() => ({ ...finalOptions.options })); + configOptions.forEach((configObject, index) => { + finalOptions.options[index].mode = assignMode(mode, configObject); + }); + } else { + finalOptions.options.mode = assignMode(mode, configOptions); + } + return finalOptions; + } + async _baseResolver(cb, parsedArgs, strategy) { const resolvedConfig = await cb(parsedArgs, this.compilerConfiguration); this._mergeOptionsToConfiguration(resolvedConfig.options, strategy); @@ -146,12 +283,8 @@ class WebpackCLI { async runOptionGroups(parsedArgs) { await Promise.resolve() .then(() => this._baseResolver(handleConfigResolution, parsedArgs)) - .then(() => this._baseResolver(resolveMode, parsedArgs)) - .then(() => this._baseResolver(resolveOutput, parsedArgs)) .then(() => this._handleCoreFlags(parsedArgs)) - .then(() => this._baseResolver(basicResolver, parsedArgs)) - .then(() => this._baseResolver(resolveAdvanced, parsedArgs)) - .then(() => this._baseResolver(resolveStats, parsedArgs)); + .then(() => this._baseResolver(this.resolveArgs, parsedArgs)); } handleError(error) { diff --git a/test/mode/mode-single-arg/mode-single-arg.test.js b/test/mode/mode-single-arg/mode-single-arg.test.js index 03a3be80584..c3577ac844c 100644 --- a/test/mode/mode-single-arg/mode-single-arg.test.js +++ b/test/mode/mode-single-arg/mode-single-arg.test.js @@ -36,7 +36,6 @@ describe('mode flags', () => { it('should pick mode form NODE_ENV', () => { const { stderr, stdout, exitCode } = run(__dirname, [], false, [], { NODE_ENV: 'development' }); - expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toContain(`mode: 'development'`);