const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const Module = require('module');

const { program, Option } = require('commander');
const utils = require('./utils');

class WebpackCLI {
    constructor() {
        // Global
        this.webpack = require(process.env.WEBPACK_PACKAGE || 'webpack');
        this.logger = utils.logger;
        this.utils = utils;

        // Initialize program
        this.program = program;
        this.program.name('webpack');
        this.program.configureOutput({
            writeErr: this.logger.error,
            outputError: (str, write) => write(`Error: ${this.utils.capitalizeFirstLetter(str.replace(/^error:/, '').trim())}`),
        });
    }

    async makeCommand(commandOptions, options, action) {
        const alreadyLoaded = this.program.commands.find(
            (command) => command.name() === commandOptions.name.split(' ')[0] || command.aliases().includes(commandOptions.alias),
        );

        if (alreadyLoaded) {
            return;
        }

        const command = this.program.command(commandOptions.name, {
            noHelp: commandOptions.noHelp,
            hidden: commandOptions.hidden,
            isDefault: commandOptions.isDefault,
        });

        if (commandOptions.description) {
            command.description(commandOptions.description, commandOptions.argsDescription);
        }

        if (commandOptions.usage) {
            command.usage(commandOptions.usage);
        }

        if (Array.isArray(commandOptions.alias)) {
            command.aliases(commandOptions.alias);
        } else {
            command.alias(commandOptions.alias);
        }

        if (commandOptions.pkg) {
            command.pkg = commandOptions.pkg;
        } else {
            command.pkg = 'webpack-cli';
        }

        const { forHelp } = this.program;

        let allDependenciesInstalled = true;

        if (commandOptions.dependencies && commandOptions.dependencies.length > 0) {
            for (const dependency of commandOptions.dependencies) {
                const { packageExists } = this.utils;
                const isPkgExist = packageExists(dependency);

                if (isPkgExist) {
                    continue;
                } else if (!isPkgExist && forHelp) {
                    allDependenciesInstalled = false;
                    continue;
                }

                const { promptInstallation, colors } = this.utils;

                await promptInstallation(dependency, () => {
                    this.logger.error(
                        `For using '${colors.green(commandOptions.name.split(' ')[0])}' command you need to install: '${colors.green(
                            dependency,
                        )}' package`,
                    );
                });
            }
        }

        if (options) {
            if (typeof options === 'function') {
                if (forHelp && !allDependenciesInstalled) {
                    command.description(
                        `${commandOptions.description} To see all available options you need to install ${commandOptions.dependencies
                            .map((dependency) => `'${dependency}'`)
                            .join(',')}.`,
                    );
                    options = [];
                } else {
                    options = options();
                }
            }

            options.forEach((optionForCommand) => {
                this.makeOption(command, optionForCommand);
            });
        }

        command.action(action);

        return command;
    }

    makeOption(command, option) {
        let mainOption;
        let negativeOption;

        if (option.configs) {
            let needNegativeOption = false;
            let mainOptionType = new Set();

            option.configs.forEach((config) => {
                // Possible value: "enum" | "string" | "path" | "number" | "boolean" | "RegExp" | "reset"
                switch (config.type) {
                    case 'reset':
                        mainOptionType.add(Boolean);
                        break;
                    case 'boolean':
                        if (!needNegativeOption) {
                            needNegativeOption = true;
                        }

                        mainOptionType.add(Boolean);
                        break;
                    case 'number':
                        mainOptionType.add(Number);
                        break;
                    case 'string':
                    case 'path':
                    case 'RegExp':
                        mainOptionType.add(String);
                        break;
                    case 'enum': {
                        let hasFalseEnum = false;

                        const enumTypes = config.values.map((value) => {
                            switch (typeof value) {
                                case 'string':
                                    mainOptionType.add(String);
                                    break;
                                case 'number':
                                    mainOptionType.add(Number);
                                    break;
                                case 'boolean':
                                    if (!hasFalseEnum && value === false) {
                                        hasFalseEnum = true;
                                        break;
                                    }

                                    mainOptionType.add(Boolean);
                                    break;
                            }
                        });

                        if (!needNegativeOption) {
                            needNegativeOption = hasFalseEnum;
                        }

                        return enumTypes;
                    }
                }
            });

            mainOption = {
                flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`,
                description: option.description || '',
                type: mainOptionType,
                multiple: option.multiple,
                defaultValue: option.defaultValue,
            };

            if (needNegativeOption) {
                negativeOption = {
                    flags: `--no-${option.name}`,
                    description: option.negatedDescription ? option.negatedDescription : `Negative '${option.name}' option.`,
                };
            }
        } else {
            mainOption = {
                flags: option.alias ? `-${option.alias}, --${option.name}` : `--${option.name}`,
                // TODO `describe` used by `webpack-dev-server@3`
                description: option.description || option.describe || '',
                type: option.type ? new Set(Array.isArray(option.type) ? option.type : [option.type]) : new Set([Boolean]),
                multiple: option.multiple,
                defaultValue: option.defaultValue,
            };

            if (option.negative) {
                negativeOption = {
                    flags: `--no-${option.name}`,
                    description: option.negatedDescription ? option.negatedDescription : `Negative '${option.name}' option.`,
                };
            }
        }

        if (mainOption.type.size > 1 && mainOption.type.has(Boolean)) {
            mainOption.flags = `${mainOption.flags} [value${mainOption.multiple ? '...' : ''}]`;
        } else if (mainOption.type.size > 0 && !mainOption.type.has(Boolean)) {
            mainOption.flags = `${mainOption.flags} <value${mainOption.multiple ? '...' : ''}>`;
        }

        if (mainOption.type.size === 1) {
            if (mainOption.type.has(Number)) {
                let skipDefault = true;

                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser((value, prev = []) => {
                        if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                            prev = [];
                            skipDefault = false;
                        }

                        return mainOption.multiple ? [].concat(prev).concat(Number(value)) : Number(value);
                    })
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else if (mainOption.type.has(String)) {
                let skipDefault = true;

                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser((value, prev = []) => {
                        if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                            prev = [];
                            skipDefault = false;
                        }

                        return mainOption.multiple ? [].concat(prev).concat(value) : value;
                    })
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else if (mainOption.type.has(Boolean)) {
                const optionForCommand = new Option(mainOption.flags, mainOption.description).default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            } else {
                const optionForCommand = new Option(mainOption.flags, mainOption.description)
                    .argParser(Array.from(mainOption.type)[0])
                    .default(mainOption.defaultValue);

                optionForCommand.helpLevel = option.helpLevel;

                command.addOption(optionForCommand);
            }
        } else if (mainOption.type.size > 1) {
            let skipDefault = true;

            const optionForCommand = new Option(mainOption.flags, mainOption.description, mainOption.defaultValue)
                .argParser((value, prev = []) => {
                    if (mainOption.defaultValue && mainOption.multiple && skipDefault) {
                        prev = [];
                        skipDefault = false;
                    }

                    if (mainOption.type.has(Number)) {
                        const numberValue = Number(value);

                        if (!isNaN(numberValue)) {
                            return mainOption.multiple ? [].concat(prev).concat(numberValue) : numberValue;
                        }
                    }

                    if (mainOption.type.has(String)) {
                        return mainOption.multiple ? [].concat(prev).concat(value) : value;
                    }

                    return value;
                })
                .default(mainOption.defaultValue);

            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        } else if (mainOption.type.size === 0 && negativeOption) {
            const optionForCommand = new Option(mainOption.flags, mainOption.description);

            // Hide stub option
            optionForCommand.hideHelp();
            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        }

        if (negativeOption) {
            const optionForCommand = new Option(negativeOption.flags, negativeOption.description);

            optionForCommand.helpLevel = option.helpLevel;

            command.addOption(optionForCommand);
        }
    }

    getBuiltInOptions() {
        if (this.builtInOptionsCache) {
            return this.builtInOptionsCache;
        }

        const minimumHelpFlags = [
            'config',
            'config-name',
            'merge',
            'env',
            'mode',
            'watch',
            'watch-options-stdin',
            'stats',
            'devtool',
            'entry',
            'target',
            'progress',
            'json',
            'name',
            'output-path',
            'node-env',
        ];

        const builtInFlags = [
            // For configs
            {
                name: 'config',
                alias: 'c',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                multiple: true,
                description: 'Provide path to a webpack configuration file e.g. ./webpack.config.js.',
            },
            {
                name: 'config-name',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                multiple: true,
                description: 'Name of the configuration to use.',
            },
            {
                name: 'merge',
                alias: 'm',
                configs: [
                    {
                        type: 'enum',
                        values: [true],
                    },
                ],
                description: "Merge two or more configurations using 'webpack-merge'.",
            },
            // Complex configs
            {
                name: 'env',
                type: (value, previous = {}) => {
                    // for https://github.com/webpack/webpack-cli/issues/2642
                    if (value.endsWith('=')) {
                        value.concat('""');
                    }

                    // This ensures we're only splitting by the first `=`
                    const [allKeys, val] = value.split(/=(.+)/, 2);
                    const splitKeys = allKeys.split(/\.(?!$)/);

                    let prevRef = previous;

                    splitKeys.forEach((someKey, index) => {
                        if (!prevRef[someKey]) {
                            prevRef[someKey] = {};
                        }

                        if (typeof prevRef[someKey] === 'string') {
                            prevRef[someKey] = {};
                        }

                        if (index === splitKeys.length - 1) {
                            if (typeof val === 'string') {
                                prevRef[someKey] = val;
                            } else {
                                prevRef[someKey] = true;
                            }
                        }

                        prevRef = prevRef[someKey];
                    });

                    return previous;
                },
                multiple: true,
                description: 'Environment passed to the configuration when it is a function.',
            },
            {
                name: 'node-env',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                multiple: false,
                description: 'Sets process.env.NODE_ENV to the specified value.',
            },

            // Adding more plugins
            {
                name: 'hot',
                alias: 'h',
                configs: [
                    {
                        type: 'string',
                    },
                    {
                        type: 'boolean',
                    },
                ],
                negative: true,
                description: 'Enables Hot Module Replacement',
                negatedDescription: 'Disables Hot Module Replacement.',
            },
            {
                name: 'analyze',
                configs: [
                    {
                        type: 'enum',
                        values: [true],
                    },
                ],
                multiple: false,
                description: 'It invokes webpack-bundle-analyzer plugin to get bundle information.',
            },
            {
                name: 'progress',
                configs: [
                    {
                        type: 'string',
                    },
                    {
                        type: 'enum',
                        values: [true],
                    },
                ],
                description: 'Print compilation progress during build.',
            },
            {
                name: 'prefetch',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                description: 'Prefetch this request.',
            },

            // Output options
            {
                name: 'json',
                configs: [
                    {
                        type: 'string',
                    },
                    {
                        type: 'enum',
                        values: [true],
                    },
                ],
                alias: 'j',
                description: 'Prints result as JSON or store it in a file.',
            },

            // For webpack@4
            {
                name: 'entry',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                multiple: true,
                description: 'The entry point(s) of your application e.g. ./src/main.js.',
            },
            {
                name: 'output-path',
                alias: 'o',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                description: 'Output location of the file generated by webpack e.g. ./dist/.',
            },
            {
                name: 'target',
                alias: 't',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                multiple: this.webpack.cli !== undefined,
                description: 'Sets the build target e.g. node.',
            },
            {
                name: 'devtool',
                configs: [
                    {
                        type: 'string',
                    },
                    {
                        type: 'enum',
                        values: [false],
                    },
                ],
                negative: true,
                alias: 'd',
                description: 'Determine source maps to use.',
                negatedDescription: 'Do not generate source maps.',
            },
            {
                name: 'mode',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                description: 'Defines the mode to pass to webpack.',
            },
            {
                name: 'name',
                configs: [
                    {
                        type: 'string',
                    },
                ],
                description: 'Name of the configuration. Used when loading multiple configurations.',
            },
            {
                name: 'stats',
                configs: [
                    {
                        type: 'string',
                    },
                    {
                        type: 'boolean',
                    },
                ],
                negative: true,
                description: 'It instructs webpack on how to treat the stats e.g. verbose.',
                negatedDescription: 'Disable stats output.',
            },
            {
                name: 'watch',
                configs: [
                    {
                        type: 'boolean',
                    },
                ],
                negative: true,
                alias: 'w',
                description: 'Watch for files changes.',
                negatedDescription: 'Do not watch for file changes.',
            },
            {
                name: 'watch-options-stdin',
                configs: [
                    {
                        type: 'boolean',
                    },
                ],
                negative: true,
                description: 'Stop watching when stdin stream has ended.',
                negatedDescription: 'Do not stop watching when stdin stream has ended.',
            },
        ];

        // Extract all the flags being exported from core.
        // A list of cli flags generated by core can be found here https://github.com/webpack/webpack/blob/master/test/__snapshots__/Cli.test.js.snap
        const coreFlags = this.webpack.cli
            ? Object.entries(this.webpack.cli.getArguments()).map(([flag, meta]) => {
                  const inBuiltIn = builtInFlags.find((builtInFlag) => builtInFlag.name === flag);

                  if (inBuiltIn) {
                      return { ...meta, name: flag, group: 'core', ...inBuiltIn, configs: meta.configs || [] };
                  }

                  return { ...meta, name: flag, group: 'core' };
              })
            : [];

        const options = []
            .concat(builtInFlags.filter((builtInFlag) => !coreFlags.find((coreFlag) => builtInFlag.name === coreFlag.name)))
            .concat(coreFlags)
            .map((option) => {
                option.helpLevel = minimumHelpFlags.includes(option.name) ? 'minimum' : 'verbose';

                return option;
            });

        this.builtInOptionsCache = options;

        return options;
    }

    applyNodeEnv(options) {
        if (typeof options.nodeEnv === 'string') {
            process.env.NODE_ENV = options.nodeEnv;
        }
    }

    async run(args, parseOptions) {
        // Built-in internal commands
        const buildCommandOptions = {
            name: 'build [entries...]',
            alias: ['bundle', 'b'],
            description: 'Run webpack (default command, can be omitted).',
            usage: '[entries...] [options]',
        };
        const watchCommandOptions = {
            name: 'watch [entries...]',
            alias: 'w',
            description: 'Run webpack and watch for files changes.',
            usage: '[entries...] [options]',
        };
        const versionCommandOptions = {
            name: 'version [commands...]',
            alias: 'v',
            description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
        };
        const helpCommandOptions = {
            name: 'help [command] [option]',
            alias: 'h',
            description: 'Display help for commands and options.',
        };
        // Built-in external commands
        const externalBuiltInCommandsInfo = [
            {
                name: 'serve [entries...]',
                alias: ['server', 's'],
                pkg: '@webpack-cli/serve',
            },
            {
                name: 'info',
                alias: 'i',
                pkg: '@webpack-cli/info',
            },
            {
                name: 'init',
                alias: ['create', 'new', 'c', 'n'],
                pkg: '@webpack-cli/generators',
            },
            {
                name: 'loader',
                alias: 'l',
                pkg: '@webpack-cli/generators',
            },
            {
                name: 'plugin',
                alias: 'p',
                pkg: '@webpack-cli/generators',
            },
            {
                name: 'migrate',
                alias: 'm',
                pkg: '@webpack-cli/migrate',
            },
            {
                name: 'configtest [config-path]',
                alias: 't',
                pkg: '@webpack-cli/configtest',
            },
        ];

        const knownCommands = [
            buildCommandOptions,
            watchCommandOptions,
            versionCommandOptions,
            helpCommandOptions,
            ...externalBuiltInCommandsInfo,
        ];
        const getCommandName = (name) => name.split(' ')[0];
        const isKnownCommand = (name) =>
            knownCommands.find(
                (command) =>
                    getCommandName(command.name) === name ||
                    (Array.isArray(command.alias) ? command.alias.includes(name) : command.alias === name),
            );
        const isCommand = (input, commandOptions) => {
            const longName = getCommandName(commandOptions.name);

            if (input === longName) {
                return true;
            }

            if (commandOptions.alias) {
                if (Array.isArray(commandOptions.alias)) {
                    return commandOptions.alias.includes(input);
                } else {
                    return commandOptions.alias === input;
                }
            }

            return false;
        };
        const findCommandByName = (name) =>
            this.program.commands.find((command) => name === command.name() || command.aliases().includes(name));
        const isOption = (value) => value.startsWith('-');
        const isGlobalOption = (value) =>
            value === '--color' ||
            value === '--no-color' ||
            value === '-v' ||
            value === '--version' ||
            value === '-h' ||
            value === '--help';

        const loadCommandByName = async (commandName, allowToInstall = false) => {
            const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
            const isWatchCommandUsed = isCommand(commandName, watchCommandOptions);

            if (isBuildCommandUsed || isWatchCommandUsed) {
                const options = this.getBuiltInOptions();

                await this.makeCommand(
                    isBuildCommandUsed ? buildCommandOptions : watchCommandOptions,
                    isWatchCommandUsed ? options.filter((option) => option.name !== 'watch') : options,
                    async (entries, options) => {
                        if (entries.length > 0) {
                            options.entry = [...entries, ...(options.entry || [])];
                        }

                        await this.buildCommand(options, isWatchCommandUsed);
                    },
                );
            } else if (isCommand(commandName, helpCommandOptions)) {
                // Stub for the `help` command
                this.makeCommand(helpCommandOptions, [], () => {});
            } else if (isCommand(commandName, versionCommandOptions)) {
                // Stub for the `help` command
                this.makeCommand(versionCommandOptions, [], () => {});
            } else {
                const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find(
                    (externalBuiltInCommandInfo) =>
                        getCommandName(externalBuiltInCommandInfo.name) === commandName ||
                        (Array.isArray(externalBuiltInCommandInfo.alias)
                            ? externalBuiltInCommandInfo.alias.includes(commandName)
                            : externalBuiltInCommandInfo.alias === commandName),
                );

                let pkg;

                if (builtInExternalCommandInfo) {
                    ({ pkg } = builtInExternalCommandInfo);
                } else {
                    pkg = commandName;
                }

                if (pkg !== 'webpack-cli' && !this.utils.packageExists(pkg)) {
                    if (!allowToInstall) {
                        return;
                    }

                    const { promptInstallation, colors } = this.utils;

                    pkg = await promptInstallation(pkg, () => {
                        this.logger.error(`For using this command you need to install: '${colors.green(pkg)}' package`);
                    });
                }

                let loadedCommand;

                try {
                    loadedCommand = require(pkg);
                } catch (error) {
                    // Ignore, command is not installed

                    return;
                }

                if (loadedCommand.default) {
                    loadedCommand = loadedCommand.default;
                }

                let command;

                try {
                    command = new loadedCommand();

                    await command.apply(this);
                } catch (error) {
                    this.logger.error(`Unable to load '${pkg}' command`);
                    this.logger.error(error);
                    process.exit(2);
                }
            }
        };

        // Register own exit
        this.program.exitOverride(async (error) => {
            if (error.exitCode === 0) {
                process.exit(0);
            }

            if (error.code === 'executeSubCommandAsync') {
                process.exit(2);
            }

            if (error.code === 'commander.help') {
                process.exit(0);
            }

            if (error.code === 'commander.unknownOption') {
                let name = error.message.match(/'(.+)'/);

                if (name) {
                    name = name[1].substr(2);

                    if (name.includes('=')) {
                        name = name.split('=')[0];
                    }

                    const { operands } = this.program.parseOptions(this.program.args);
                    const operand = typeof operands[0] !== 'undefined' ? operands[0] : getCommandName(buildCommandOptions.name);

                    if (operand) {
                        const command = findCommandByName(operand);

                        if (!command) {
                            this.logger.error(`Can't find and load command '${operand}'`);
                            this.logger.error("Run 'webpack --help' to see available commands and options");
                            process.exit(2);
                        }

                        command.options.forEach((option) => {
                            if (!option.hidden && this.utils.levenshtein.distance(name, option.long.slice(2)) < 3) {
                                this.logger.error(`Did you mean '--${option.name()}'?`);
                            }
                        });
                    }
                }
            }

            // Codes:
            // - commander.unknownCommand
            // - commander.missingArgument
            // - commander.missingMandatoryOptionValue
            // - commander.optionMissingArgument

            this.logger.error("Run 'webpack --help' to see available commands and options");
            process.exit(2);
        });

        // Default `--color` and `--no-color` options
        const cli = this;
        this.program.option('--color', 'Enable colors on console.');
        this.program.on('option:color', function () {
            const { color } = this.opts();

            cli.utils.colors.options.changed = true;
            cli.utils.colors.options.enabled = color;
        });
        this.program.option('--no-color', 'Disable colors on console.');
        this.program.on('option:no-color', function () {
            const { color } = this.opts();

            cli.utils.colors.options.changed = true;
            cli.utils.colors.options.enabled = color;
        });

        // Make `-v, --version` options
        // Make `version|v [commands...]` command
        const outputVersion = async (options) => {
            // Filter `bundle`, `watch`, `version` and `help` commands
            const possibleCommandNames = options.filter(
                (option) =>
                    !isCommand(option, buildCommandOptions) &&
                    !isCommand(option, watchCommandOptions) &&
                    !isCommand(option, versionCommandOptions) &&
                    !isCommand(option, helpCommandOptions),
            );

            possibleCommandNames.forEach((possibleCommandName) => {
                if (!isOption(possibleCommandName)) {
                    return;
                }

                this.logger.error(`Unknown option '${possibleCommandName}'`);
                this.logger.error("Run 'webpack --help' to see available commands and options");
                process.exit(2);
            });

            if (possibleCommandNames.length > 0) {
                await Promise.all(possibleCommandNames.map((possibleCommand) => loadCommandByName(possibleCommand)));

                for (const possibleCommandName of possibleCommandNames) {
                    const foundCommand = findCommandByName(possibleCommandName);

                    if (!foundCommand) {
                        this.logger.error(`Unknown command '${possibleCommandName}'`);
                        this.logger.error("Run 'webpack --help' to see available commands and options");
                        process.exit(2);
                    }

                    try {
                        const { name, version } = require(`${foundCommand.pkg}/package.json`);

                        this.logger.raw(`${name} ${version}`);
                    } catch (e) {
                        this.logger.error(`Error: External package '${foundCommand.pkg}' not found`);
                        process.exit(2);
                    }
                }
            }

            const pkgJSON = require('../package.json');

            this.logger.raw(`webpack ${this.webpack.version}`);
            this.logger.raw(`webpack-cli ${pkgJSON.version}`);

            if (this.utils.packageExists('webpack-dev-server')) {
                // eslint-disable-next-line
                const { version } = require('webpack-dev-server/package.json');

                this.logger.raw(`webpack-dev-server ${version}`);
            }

            process.exit(0);
        };
        this.program.option(
            '-v, --version',
            "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
        );

        const outputHelp = async (options, isVerbose, isHelpCommandSyntax, program) => {
            const { bold } = this.utils.colors;

            const outputIncorrectUsageOfHelp = () => {
                this.logger.error('Incorrect use of help');
                this.logger.error("Please use: 'webpack help [command] [option]' | 'webpack [command] --help'");
                this.logger.error("Run 'webpack --help' to see available commands and options");
                process.exit(2);
            };

            const isGlobalHelp = options.length === 0;
            const isCommandHelp = options.length === 1 && !isOption(options[0]);

            if (isGlobalHelp || isCommandHelp) {
                program.configureHelp({
                    sortSubcommands: true,
                    // Support multiple aliases
                    commandUsage: (command) => {
                        let parentCmdNames = '';

                        for (let parentCmd = command.parent; parentCmd; parentCmd = parentCmd.parent) {
                            parentCmdNames = `${parentCmd.name()} ${parentCmdNames}`;
                        }

                        if (isGlobalHelp) {
                            return `${parentCmdNames}${command.usage()}\n${this.utils.colors.bold(
                                'Alternative usage to run commands:',
                            )} ${parentCmdNames}[command] [options]`;
                        }

                        return `${parentCmdNames}${command.name()}|${command.aliases().join('|')} ${command.usage()}`;
                    },
                    // Support multiple aliases
                    subcommandTerm: (command) => {
                        const humanReadableArgumentName = (argument) => {
                            const nameOutput = argument.name + (argument.variadic === true ? '...' : '');

                            return argument.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']';
                        };
                        const args = command._args.map((arg) => humanReadableArgumentName(arg)).join(' ');

                        return `${command.name()}|${command.aliases().join('|')}${args ? ` ${args}` : ''}${
                            command.options.length > 0 ? ' [options]' : ''
                        }`;
                    },
                    visibleOptions: function visibleOptions(command) {
                        return command.options.filter((option) => {
                            if (option.hidden) {
                                return false;
                            }

                            switch (option.helpLevel) {
                                case 'verbose':
                                    return isVerbose;
                                case 'minimum':
                                default:
                                    return true;
                            }
                        });
                    },
                    padWidth(command, helper) {
                        return Math.max(
                            helper.longestArgumentTermLength(command, helper),
                            helper.longestOptionTermLength(command, helper),
                            // For global options
                            helper.longestOptionTermLength(program, helper),
                            helper.longestSubcommandTermLength(isGlobalHelp ? program : command, helper),
                        );
                    },
                    formatHelp: (command, helper) => {
                        const termWidth = helper.padWidth(command, helper);
                        const helpWidth = helper.helpWidth || process.env.WEBPACK_CLI_HELP_WIDTH || 80;
                        const itemIndentWidth = 2;
                        const itemSeparatorWidth = 2; // between term and description

                        const formatItem = (term, description) => {
                            if (description) {
                                const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;

                                return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
                            }

                            return term;
                        };

                        const formatList = (textArray) => textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));

                        // Usage
                        let output = [`${bold('Usage:')} ${helper.commandUsage(command)}`, ''];

                        // Description
                        const commandDescription = isGlobalHelp
                            ? 'The build tool for modern web applications.'
                            : helper.commandDescription(command);

                        if (commandDescription.length > 0) {
                            output = output.concat([commandDescription, '']);
                        }

                        // Arguments
                        const argumentList = helper
                            .visibleArguments(command)
                            .map((argument) => formatItem(argument.term, argument.description));

                        if (argumentList.length > 0) {
                            output = output.concat([bold('Arguments:'), formatList(argumentList), '']);
                        }

                        // Options
                        const optionList = helper
                            .visibleOptions(command)
                            .map((option) => formatItem(helper.optionTerm(option), helper.optionDescription(option)));

                        if (optionList.length > 0) {
                            output = output.concat([bold('Options:'), formatList(optionList), '']);
                        }

                        // Global options
                        const globalOptionList = program.options.map((option) =>
                            formatItem(helper.optionTerm(option), helper.optionDescription(option)),
                        );

                        if (globalOptionList.length > 0) {
                            output = output.concat([bold('Global options:'), formatList(globalOptionList), '']);
                        }

                        // Commands
                        const commandList = helper
                            .visibleCommands(isGlobalHelp ? program : command)
                            .map((command) => formatItem(helper.subcommandTerm(command), helper.subcommandDescription(command)));

                        if (commandList.length > 0) {
                            output = output.concat([bold('Commands:'), formatList(commandList), '']);
                        }

                        return output.join('\n');
                    },
                });

                if (isGlobalHelp) {
                    await Promise.all(
                        knownCommands.map((knownCommand) => {
                            return loadCommandByName(getCommandName(knownCommand.name));
                        }),
                    );

                    const buildCommand = findCommandByName(getCommandName(buildCommandOptions.name));

                    this.logger.raw(buildCommand.helpInformation());
                } else {
                    const name = options[0];

                    await loadCommandByName(name);

                    const command = findCommandByName(name);

                    if (!command) {
                        const builtInCommandUsed = externalBuiltInCommandsInfo.find(
                            (command) => command.name.includes(name) || name === command.alias,
                        );
                        if (typeof builtInCommandUsed !== 'undefined') {
                            this.logger.error(`For using '${name}' command you need to install '${builtInCommandUsed.pkg}' package`);
                        } else {
                            this.logger.error(`Can't find and load command '${name}'`);
                            this.logger.error("Run 'webpack --help' to see available commands and options");
                        }
                        process.exit(2);
                    }

                    this.logger.raw(command.helpInformation());
                }
            } else if (isHelpCommandSyntax) {
                let isCommandSpecified = false;
                let commandName = getCommandName(buildCommandOptions.name);
                let optionName;

                if (options.length === 1) {
                    optionName = options[0];
                } else if (options.length === 2) {
                    isCommandSpecified = true;
                    commandName = options[0];
                    optionName = options[1];

                    if (isOption(commandName)) {
                        outputIncorrectUsageOfHelp();
                    }
                } else {
                    outputIncorrectUsageOfHelp();
                }

                await loadCommandByName(commandName);

                const command = isGlobalOption(optionName) ? program : findCommandByName(commandName);

                if (!command) {
                    this.logger.error(`Can't find and load command '${commandName}'`);
                    this.logger.error("Run 'webpack --help' to see available commands and options");
                    process.exit(2);
                }

                const option = command.options.find((option) => option.short === optionName || option.long === optionName);

                if (!option) {
                    this.logger.error(`Unknown option '${optionName}'`);
                    this.logger.error("Run 'webpack --help' to see available commands and options");
                    process.exit(2);
                }

                const nameOutput =
                    option.flags.replace(/^.+[[<]/, '').replace(/(\.\.\.)?[\]>].*$/, '') + (option.variadic === true ? '...' : '');
                const value = option.required ? '<' + nameOutput + '>' : option.optional ? '[' + nameOutput + ']' : '';

                this.logger.raw(
                    `${bold('Usage')}: webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.long}${value ? ` ${value}` : ''}`,
                );

                if (option.short) {
                    this.logger.raw(
                        `${bold('Short:')} webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.short}${
                            value ? ` ${value}` : ''
                        }`,
                    );
                }

                if (option.description) {
                    this.logger.raw(`${bold('Description:')} ${option.description}`);
                }

                if (!option.negate && options.defaultValue) {
                    this.logger.raw(`${bold('Default value:')} ${JSON.stringify(option.defaultValue)}`);
                }

                this.logger.raw('');

                // TODO implement this after refactor cli arguments
                // logger.raw('Possible values: foo | bar');
                // logger.raw('Documentation: https://webpack.js.org/option/name/');
            } else {
                outputIncorrectUsageOfHelp();
            }

            this.logger.raw("To see list of all supported commands and options run 'webpack --help=verbose'.\n");
            this.logger.raw(`${bold('Webpack documentation:')} https://webpack.js.org/.`);
            this.logger.raw(`${bold('CLI documentation:')} https://webpack.js.org/api/cli/.`);
            this.logger.raw(`${bold('Made with ♥ by the webpack team')}.`);
            process.exit(0);
        };
        this.program.helpOption(false);
        this.program.addHelpCommand(false);
        this.program.option('-h, --help [verbose]', 'Display help for commands and options.');

        let isInternalActionCalled = false;

        // Default action
        this.program.usage('[options]');
        this.program.allowUnknownOption(true);
        this.program.action(async (options, program) => {
            if (!isInternalActionCalled) {
                isInternalActionCalled = true;
            } else {
                this.logger.error('No commands found to run');
                process.exit(2);
            }

            // Command and options
            const { operands, unknown } = this.program.parseOptions(program.args);
            const defaultCommandToRun = getCommandName(buildCommandOptions.name);
            const hasOperand = typeof operands[0] !== 'undefined';
            const operand = hasOperand ? operands[0] : defaultCommandToRun;
            const isHelpOption = typeof options.help !== 'undefined';
            const isHelpCommandSyntax = isCommand(operand, helpCommandOptions);

            if (isHelpOption || isHelpCommandSyntax) {
                let isVerbose = false;

                if (isHelpOption) {
                    if (typeof options.help === 'string') {
                        if (options.help !== 'verbose') {
                            this.logger.error("Unknown value for '--help' option, please use '--help=verbose'");
                            process.exit(2);
                        }

                        isVerbose = true;
                    }
                }

                this.program.forHelp = true;

                const optionsForHelp = []
                    .concat(isHelpOption && hasOperand ? [operand] : [])
                    // Syntax `webpack help [command]`
                    .concat(operands.slice(1))
                    // Syntax `webpack help [option]`
                    .concat(unknown)
                    .concat(isHelpCommandSyntax && typeof options.color !== 'undefined' ? [options.color ? '--color' : '--no-color'] : [])
                    .concat(isHelpCommandSyntax && typeof options.version !== 'undefined' ? ['--version'] : []);

                await outputHelp(optionsForHelp, isVerbose, isHelpCommandSyntax, program);
            }

            const isVersionOption = typeof options.version !== 'undefined';
            const isVersionCommandSyntax = isCommand(operand, versionCommandOptions);

            if (isVersionOption || isVersionCommandSyntax) {
                const optionsForVersion = []
                    .concat(isVersionOption ? [operand] : [])
                    .concat(operands.slice(1))
                    .concat(unknown);

                await outputVersion(optionsForVersion, program);
            }

            let commandToRun = operand;
            let commandOperands = operands.slice(1);

            if (isKnownCommand(commandToRun)) {
                await loadCommandByName(commandToRun, true);
            } else {
                let isEntrySyntax = fs.existsSync(operand);

                if (isEntrySyntax) {
                    commandToRun = defaultCommandToRun;
                    commandOperands = operands;

                    await loadCommandByName(commandToRun);
                } else {
                    this.logger.error(`Unknown command or entry '${operand}'`);

                    const found = knownCommands.find(
                        (commandOptions) => this.utils.levenshtein.distance(operand, getCommandName(commandOptions.name)) < 3,
                    );

                    if (found) {
                        this.logger.error(
                            `Did you mean '${getCommandName(found.name)}' (alias '${
                                Array.isArray(found.alias) ? found.alias.join(', ') : found.alias
                            }')?`,
                        );
                    }

                    this.logger.error("Run 'webpack --help' to see available commands and options");
                    process.exit(2);
                }
            }

            await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], { from: 'user' });
        });

        await this.program.parseAsync(args, parseOptions);
    }

    async resolveConfig(options) {
        const loadConfig = async (configPath) => {
            const { interpret } = this.utils;
            const ext = path.extname(configPath);
            const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext);

            if (interpreted) {
                const { rechoir } = this.utils;

                try {
                    rechoir.prepare(interpret.extensions, configPath);
                } catch (error) {
                    if (error.failures) {
                        this.logger.error(`Unable load '${configPath}'`);
                        this.logger.error(error.message);

                        error.failures.forEach((failure) => {
                            this.logger.error(failure.error.message);
                        });
                        this.logger.error('Please install one of them');
                        process.exit(2);
                    }

                    this.logger.error(error);
                    process.exit(2);
                }
            }

            let options;

            try {
                try {
                    options = require(configPath);
                } catch (error) {
                    let previousModuleCompile;

                    // TODO Workaround https://github.com/zertosh/v8-compile-cache/issues/30
                    if (this._originalModuleCompile) {
                        previousModuleCompile = Module.prototype._compile;

                        Module.prototype._compile = this._originalModuleCompile;
                    }

                    const dynamicImportLoader = this.utils.dynamicImportLoader();

                    if (this._originalModuleCompile) {
                        Module.prototype._compile = previousModuleCompile;
                    }

                    if (
                        (error.code === 'ERR_REQUIRE_ESM' || process.env.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) &&
                        pathToFileURL &&
                        dynamicImportLoader
                    ) {
                        const urlForConfig = pathToFileURL(configPath);

                        options = await dynamicImportLoader(urlForConfig);
                        options = options.default;

                        return { options, path: configPath };
                    }

                    throw error;
                }
            } catch (error) {
                this.logger.error(`Failed to load '${configPath}' config`);

                if (this.isValidationError(error)) {
                    this.logger.error(error.message);
                } else {
                    this.logger.error(error);
                }

                process.exit(2);
            }

            if (options.default) {
                options = options.default;
            }

            return { options, path: configPath };
        };

        const evaluateConfig = async (loadedConfig, argv) => {
            const isMultiCompiler = Array.isArray(loadedConfig.options);
            const config = isMultiCompiler ? loadedConfig.options : [loadedConfig.options];

            let evaluatedConfig = await Promise.all(
                config.map(async (rawConfig) => {
                    if (typeof rawConfig.then === 'function') {
                        rawConfig = await rawConfig;
                    }

                    // `Promise` may return `Function`
                    if (typeof rawConfig === 'function') {
                        // when config is a function, pass the env from args to the config function
                        rawConfig = await rawConfig(argv.env, argv);
                    }

                    return rawConfig;
                }),
            );

            loadedConfig.options = isMultiCompiler ? evaluatedConfig : evaluatedConfig[0];

            const isObject = (value) => typeof value === 'object' && value !== null;

            if (!isObject(loadedConfig.options) && !Array.isArray(loadedConfig.options)) {
                this.logger.error(`Invalid configuration in '${loadedConfig.path}'`);
                process.exit(2);
            }

            return loadedConfig;
        };

        let config = { options: {}, path: new WeakMap() };

        if (options.config && options.config.length > 0) {
            const evaluatedConfigs = await Promise.all(
                options.config.map(async (value) => evaluateConfig(await loadConfig(path.resolve(value)), options.argv || {})),
            );

            config.options = [];

            evaluatedConfigs.forEach((evaluatedConfig) => {
                if (Array.isArray(evaluatedConfig.options)) {
                    evaluatedConfig.options.forEach((options) => {
                        config.options.push(options);
                        config.path.set(options, evaluatedConfig.path);
                    });
                } else {
                    config.options.push(evaluatedConfig.options);
                    config.path.set(evaluatedConfig.options, evaluatedConfig.path);
                }
            });

            config.options = config.options.length === 1 ? config.options[0] : config.options;
        } else {
            const { interpret } = this.utils;

            // Order defines the priority, in decreasing order
            const defaultConfigFiles = ['webpack.config', '.webpack/webpack.config', '.webpack/webpackfile']
                .map((filename) =>
                    // Since .cjs is not available on interpret side add it manually to default config extension list
                    [...Object.keys(interpret.extensions), '.cjs'].map((ext) => ({
                        path: path.resolve(filename + ext),
                        ext: ext,
                        module: interpret.extensions[ext],
                    })),
                )
                .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);

            let foundDefaultConfigFile;

            for (const defaultConfigFile of defaultConfigFiles) {
                if (!fs.existsSync(defaultConfigFile.path)) {
                    continue;
                }

                foundDefaultConfigFile = defaultConfigFile;
                break;
            }

            if (foundDefaultConfigFile) {
                const loadedConfig = await loadConfig(foundDefaultConfigFile.path);
                const evaluatedConfig = await evaluateConfig(loadedConfig, options.argv || {});

                config.options = evaluatedConfig.options;

                if (Array.isArray(config.options)) {
                    config.options.forEach((options) => {
                        config.path.set(options, evaluatedConfig.path);
                    });
                } else {
                    config.path.set(evaluatedConfig.options, evaluatedConfig.path);
                }
            }
        }

        if (options.configName) {
            const notfoundConfigNames = [];

            config.options = options.configName.map((configName) => {
                let found;

                if (Array.isArray(config.options)) {
                    found = config.options.find((options) => options.name === configName);
                } else {
                    found = config.options.name === configName ? config.options : undefined;
                }

                if (!found) {
                    notfoundConfigNames.push(configName);
                }

                return found;
            });

            if (notfoundConfigNames.length > 0) {
                this.logger.error(
                    notfoundConfigNames.map((configName) => `Configuration with the name "${configName}" was not found.`).join(' '),
                );
                process.exit(2);
            }
        }

        if (options.merge) {
            const { merge } = require('webpack-merge');

            // we can only merge when there are multiple configurations
            // either by passing multiple configs by flags or passing a
            // single config exporting an array
            if (!Array.isArray(config.options) || config.options.length <= 1) {
                this.logger.error('At least two configurations are required for merge.');
                process.exit(2);
            }

            const mergedConfigPaths = [];

            config.options = config.options.reduce((accumulator, options) => {
                const configPath = config.path.get(options);
                const mergedOptions = merge(accumulator, options);

                mergedConfigPaths.push(configPath);

                return mergedOptions;
            }, {});
            config.path.set(config.options, mergedConfigPaths);
        }

        return config;
    }

    // TODO refactor
    async applyOptions(config, options) {
        if (options.analyze) {
            if (!this.utils.packageExists('webpack-bundle-analyzer')) {
                const { promptInstallation, colors } = this.utils;

                await promptInstallation('webpack-bundle-analyzer', () => {
                    this.logger.error(`It looks like ${colors.yellow('webpack-bundle-analyzer')} is not installed.`);
                });

                this.logger.success(`${colors.yellow('webpack-bundle-analyzer')} was installed successfully.`);
            }
        }

        if (typeof options.progress === 'string' && options.progress !== 'profile') {
            this.logger.error(`'${options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`);
            process.exit(2);
        }

        if (typeof options.hot === 'string' && options.hot !== 'only') {
            this.logger.error(`'${options.hot}' is an invalid value for the --hot option. Use 'only' instead.`);
            process.exit(2);
        }

        const outputHints = (configOptions) => {
            if (
                configOptions.watch &&
                options.argv &&
                options.argv.env &&
                (options.argv.env['WEBPACK_WATCH'] || options.argv.env['WEBPACK_SERVE'])
            ) {
                this.logger.warn(
                    `No need to use the '${
                        options.argv.env['WEBPACK_WATCH'] ? 'watch' : 'serve'
                    }' command together with '{ watch: true }' configuration, it does not make sense.`,
                );

                if (options.argv.env['WEBPACK_SERVE']) {
                    configOptions.watch = false;
                }
            }

            return configOptions;
        };

        config.options = Array.isArray(config.options)
            ? config.options.map((options) => outputHints(options))
            : outputHints(config.options);

        if (this.webpack.cli) {
            const processArguments = (configOptions) => {
                const args = this.getBuiltInOptions()
                    .filter((flag) => flag.group === 'core')
                    .reduce((accumulator, flag) => {
                        accumulator[flag.name] = flag;

                        return accumulator;
                    }, {});

                const values = Object.keys(options).reduce((accumulator, name) => {
                    if (name === 'argv') {
                        return accumulator;
                    }

                    const kebabName = this.utils.toKebabCase(name);

                    if (args[kebabName]) {
                        accumulator[kebabName] = options[name];
                    }

                    return accumulator;
                }, {});

                const problems = this.webpack.cli.processArguments(args, configOptions, values);

                if (problems) {
                    const groupBy = (xs, key) => {
                        return xs.reduce((rv, x) => {
                            (rv[x[key]] = rv[x[key]] || []).push(x);

                            return rv;
                        }, {});
                    };
                    const problemsByPath = groupBy(problems, 'path');

                    for (const path in problemsByPath) {
                        const problems = problemsByPath[path];

                        problems.forEach((problem) => {
                            this.logger.error(
                                `${this.utils.capitalizeFirstLetter(problem.type.replace(/-/g, ' '))}${
                                    problem.value ? ` '${problem.value}'` : ''
                                } for the '--${problem.argument}' option${problem.index ? ` by index '${problem.index}'` : ''}`,
                            );

                            if (problem.expected) {
                                this.logger.error(`Expected: '${problem.expected}'`);
                            }
                        });
                    }

                    process.exit(2);
                }

                return configOptions;
            };

            config.options = Array.isArray(config.options)
                ? config.options.map((options) => processArguments(options))
                : processArguments(config.options);

            const setupDefaultOptions = (configOptions) => {
                // No need to run for webpack@4
                if (configOptions.cache && configOptions.cache.type === 'filesystem') {
                    const configPath = config.path.get(configOptions);

                    if (configPath) {
                        if (!configOptions.cache.buildDependencies) {
                            configOptions.cache.buildDependencies = {};
                        }

                        if (!configOptions.cache.buildDependencies.defaultConfig) {
                            configOptions.cache.buildDependencies.defaultConfig = [];
                        }

                        if (Array.isArray(configPath)) {
                            configPath.forEach((item) => {
                                configOptions.cache.buildDependencies.defaultConfig.push(item);
                            });
                        } else {
                            configOptions.cache.buildDependencies.defaultConfig.push(configPath);
                        }
                    }
                }

                return configOptions;
            };

            config.options = Array.isArray(config.options)
                ? config.options.map((options) => setupDefaultOptions(options))
                : setupDefaultOptions(config.options);
        }

        // Logic for webpack@4
        // TODO remove after drop webpack@4
        const processLegacyArguments = (configOptions) => {
            if (options.entry) {
                configOptions.entry = options.entry;
            }

            if (options.outputPath) {
                configOptions.output = {
                    ...configOptions.output,
                    ...{ path: path.resolve(options.outputPath) },
                };
            }

            if (options.target) {
                configOptions.target = options.target;
            }

            if (typeof options.devtool !== 'undefined') {
                configOptions.devtool = options.devtool;
            }

            if (options.mode) {
                configOptions.mode = options.mode;
            } else if (
                !configOptions.mode &&
                process.env &&
                process.env.NODE_ENV &&
                (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'none')
            ) {
                configOptions.mode = process.env.NODE_ENV;
            }

            if (options.name) {
                configOptions.name = options.name;
            }

            if (typeof options.stats !== 'undefined') {
                configOptions.stats = options.stats;
            }

            if (typeof options.watch !== 'undefined') {
                configOptions.watch = options.watch;
            }

            if (typeof options.watchOptionsStdin !== 'undefined') {
                configOptions.watchOptions = {
                    ...configOptions.watchOptions,
                    ...{ stdin: options.watchOptionsStdin },
                };
            }

            return configOptions;
        };

        config.options = Array.isArray(config.options)
            ? config.options.map((options) => processLegacyArguments(options))
            : processLegacyArguments(config.options);

        // Apply `stats` and `stats.colors` options
        const applyStatsColors = (configOptions) => {
            // TODO remove after drop webpack@4
            const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions;

            if (statsForWebpack4) {
                if (typeof configOptions.stats === 'undefined') {
                    configOptions.stats = {};
                } else if (typeof configOptions.stats === 'boolean' || typeof configOptions.stats === 'string') {
                    if (
                        typeof configOptions.stats === 'string' &&
                        configOptions.stats !== 'none' &&
                        configOptions.stats !== 'verbose' &&
                        configOptions.stats !== 'detailed' &&
                        configOptions.stats !== 'minimal' &&
                        configOptions.stats !== 'errors-only' &&
                        configOptions.stats !== 'errors-warnings'
                    ) {
                        return configOptions;
                    }

                    configOptions.stats = this.webpack.Stats.presetToOptions(configOptions.stats);
                }
            } else {
                if (typeof configOptions.stats === 'undefined') {
                    configOptions.stats = { preset: 'normal' };
                } else if (typeof configOptions.stats === 'boolean') {
                    configOptions.stats = configOptions.stats ? { preset: 'normal' } : { preset: 'none' };
                } else if (typeof configOptions.stats === 'string') {
                    configOptions.stats = { preset: configOptions.stats };
                }
            }

            let colors;

            // From arguments
            if (typeof this.utils.colors.options.changed !== 'undefined') {
                colors = Boolean(this.utils.colors.options.enabled);
            }
            // From stats
            else if (typeof configOptions.stats.colors !== 'undefined') {
                colors = configOptions.stats.colors;
            }
            // Default
            else {
                colors = Boolean(this.utils.colors.options.enabled);
            }

            configOptions.stats.colors = colors;

            return configOptions;
        };

        config.options = Array.isArray(config.options)
            ? config.options.map((options) => applyStatsColors(options))
            : applyStatsColors(config.options);

        return config;
    }

    async applyCLIPlugin(config, cliOptions) {
        const addCLIPlugin = (configOptions) => {
            if (!configOptions.plugins) {
                configOptions.plugins = [];
            }

            const CLIPlugin = require('./plugins/CLIPlugin');

            configOptions.plugins.unshift(
                new CLIPlugin({
                    configPath: config.path.get(configOptions),
                    helpfulOutput: !cliOptions.json,
                    hot: cliOptions.hot,
                    progress: cliOptions.progress,
                    prefetch: cliOptions.prefetch,
                    analyze: cliOptions.analyze,
                }),
            );

            return configOptions;
        };
        config.options = Array.isArray(config.options)
            ? config.options.map((options) => addCLIPlugin(options))
            : addCLIPlugin(config.options);

        return config;
    }

    needWatchStdin(compiler) {
        if (compiler.compilers) {
            return compiler.compilers.some((compiler) => compiler.options.watchOptions && compiler.options.watchOptions.stdin);
        }

        return compiler.options.watchOptions && compiler.options.watchOptions.stdin;
    }

    isValidationError(error) {
        // https://github.com/webpack/webpack/blob/master/lib/index.js#L267
        // https://github.com/webpack/webpack/blob/v4.44.2/lib/webpack.js#L90
        const ValidationError = this.webpack.ValidationError || this.webpack.WebpackOptionsValidationError;

        return error instanceof ValidationError || error.name === 'ValidationError';
    }

    async createCompiler(options, callback) {
        this.applyNodeEnv(options);

        let config = await this.resolveConfig(options);

        config = await this.applyOptions(config, options);
        config = await this.applyCLIPlugin(config, options);

        let compiler;

        try {
            compiler = this.webpack(
                config.options,
                callback
                    ? (error, stats) => {
                          if (error && this.isValidationError(error)) {
                              this.logger.error(error.message);
                              process.exit(2);
                          }

                          callback(error, stats);
                      }
                    : callback,
            );
        } catch (error) {
            if (this.isValidationError(error)) {
                this.logger.error(error.message);
            } else {
                this.logger.error(error);
            }

            process.exit(2);
        }

        // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
        if (compiler && compiler.compiler) {
            compiler = compiler.compiler;
        }

        return compiler;
    }

    async buildCommand(options, isWatchCommand) {
        let compiler;

        const callback = (error, stats) => {
            if (error) {
                this.logger.error(error);
                process.exit(2);
            }

            if (stats.hasErrors()) {
                process.exitCode = 1;
            }

            if (!compiler) {
                return;
            }

            const statsOptions = compiler.compilers
                ? { children: compiler.compilers.map((compiler) => (compiler.options ? compiler.options.stats : undefined)) }
                : compiler.options
                ? compiler.options.stats
                : undefined;

            // TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats
            const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions;

            if (compiler.compilers && statsForWebpack4) {
                statsOptions.colors = statsOptions.children.some((child) => child.colors);
            }

            if (options.json) {
                const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext');
                const handleWriteError = (error) => {
                    this.logger.error(error);
                    process.exit(2);
                };

                if (options.json === true) {
                    createJsonStringifyStream(stats.toJson(statsOptions))
                        .on('error', handleWriteError)
                        .pipe(process.stdout)
                        .on('error', handleWriteError)
                        .on('close', () => process.stdout.write('\n'));
                } else {
                    createJsonStringifyStream(stats.toJson(statsOptions))
                        .on('error', handleWriteError)
                        .pipe(fs.createWriteStream(options.json))
                        .on('error', handleWriteError)
                        // Use stderr to logging
                        .on('close', () =>
                            process.stderr.write(
                                `[webpack-cli] ${this.utils.colors.green(`stats are successfully stored as json to ${options.json}`)}\n`,
                            ),
                        );
                }
            } else {
                const printedStats = stats.toString(statsOptions);

                // Avoid extra empty line when `stats: 'none'`
                if (printedStats) {
                    this.logger.raw(printedStats);
                }
            }
        };

        const env =
            isWatchCommand || options.watch
                ? { WEBPACK_WATCH: true, ...options.env }
                : { WEBPACK_BUNDLE: true, WEBPACK_BUILD: true, ...options.env };

        options.argv = { ...options, env };

        if (isWatchCommand) {
            options.watch = true;
        }

        compiler = await this.createCompiler(options, callback);

        if (!compiler) {
            return;
        }

        const isWatch = (compiler) =>
            compiler.compilers ? compiler.compilers.some((compiler) => compiler.options.watch) : compiler.options.watch;

        if (isWatch(compiler) && this.needWatchStdin(compiler)) {
            process.stdin.on('end', () => {
                process.exit(0);
            });
            process.stdin.resume();
        }
    }
}

module.exports = WebpackCLI;