diff --git a/packages/webpack-cli/src/webpack-cli.ts b/packages/webpack-cli/src/webpack-cli.ts index cf0f172159b..b2033151184 100644 --- a/packages/webpack-cli/src/webpack-cli.ts +++ b/packages/webpack-cli/src/webpack-cli.ts @@ -5,7 +5,6 @@ import { pathToFileURL } from "node:url"; import util from "node:util"; import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext"; import { - type Argument, type Command as CommanderCommand, type CommandOptions as CommanderCommandOptions, type Help, @@ -49,6 +48,9 @@ const DEFAULT_CONFIGURATION_FILES = [ ".webpack/webpackfile", ]; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type RecordAny = Record; + interface Rechoir { prepare: typeof prepare; } @@ -78,12 +80,19 @@ interface Colors extends WebpackColors { isColorSupported: boolean; } +type Context = RecordAny; + interface Command extends CommanderCommand { pkg?: string; forHelp?: boolean; + context: Context; } -interface CommandOptions extends CommanderCommandOptions { +interface CommandOptions< + A = void, + O extends CommanderArgs = CommanderArgs, + C extends Context = Context, +> extends CommanderCommandOptions { rawName: string; name: string; alias: string | string[]; @@ -92,6 +101,48 @@ interface CommandOptions extends CommanderCommandOptions { dependencies?: string[]; pkg?: string; external?: boolean; + preload?: () => Promise; + options?: + | CommandOption[] + | ((command: Command & { context: C }) => CommandOption[]) + | ((command: Command & { context: C }) => Promise); + action: A extends void + ? (options: O, cmd: Command & { context: C }) => void | Promise + : (args: A, options: O, cmd: Command & { context: C }) => void | Promise; +} + +interface WebpackContext { + webpack: typeof webpack; +} + +interface WebpackOptionsContext { + webpackOptions: CommandOption[]; +} + +interface WebpackDevServerContext { + devServer: typeof import("webpack-dev-server"); +} + +interface WebpackDevServerOptionsContext { + devServerOptions: CommandOption[]; +} + +interface KnownWebpackCLICommands { + build: CommandOptions; + serve: CommandOptions< + string[], + CommanderArgs, + WebpackContext & + WebpackOptionsContext & + WebpackDevServerContext & + WebpackDevServerOptionsContext & + Context + >; + watch: CommandOptions; + version: CommandOptions; + help: CommandOptions; + info: CommandOptions; + configtest: CommandOptions; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -131,7 +182,6 @@ interface CommandOption { defaultValue?: string; hidden?: boolean; negativeHidden?: boolean; - group?: "core"; } interface Env { @@ -141,8 +191,7 @@ interface Env { WEBPACK_SERVE?: boolean; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -interface Argv extends Record { +interface Argv extends RecordAny { env: Env; } @@ -167,6 +216,7 @@ declare interface WebpackCallback { } type ProcessedArguments = Parameters<(typeof webpack)["cli"]["processArguments"]>[2]; +type Schema = Parameters<(typeof webpack)["cli"]["getArguments"]>[0]; interface KnownOptions { config?: string[]; @@ -185,13 +235,14 @@ interface KnownOptions { configName?: string[]; disableInterpret?: boolean; extends?: string[]; + webpack: typeof webpack; } type Options = // Webpack CLI own options KnownOptions & // Webpack and webpack-dev-server options - Record; + RecordAny; const DEFAULT_WEBPACK_PACKAGES: string[] = ["webpack", "loader"]; @@ -219,10 +270,6 @@ class WebpackCLI { #isColorSupportChanged: boolean | undefined; - #builtInOptionsCache: CommandOption[] | undefined; - - webpack!: typeof webpack; - program: Command; constructor() { @@ -230,7 +277,7 @@ class WebpackCLI { this.logger = this.getLogger(); // Initialize program - this.program = program; + this.program = program as Command; this.program.name("webpack"); this.program.configureOutput({ writeErr: (str) => { @@ -267,16 +314,6 @@ class WebpackCLI { return { ...createColors({ useColor: shouldUseColor }), isColorSupported: shouldUseColor }; } - isMultipleConfiguration( - config: Configuration | MultiConfiguration, - ): config is MultiConfiguration { - return Array.isArray(config); - } - - isMultipleCompiler(compiler: Compiler | MultiCompiler): compiler is MultiCompiler { - return (compiler as MultiCompiler).compilers as unknown as boolean; - } - isPromise(value: Promise): value is Promise { return typeof (value as unknown as Promise).then === "function"; } @@ -308,61 +345,6 @@ class WebpackCLI { }; } - async isPackageInstalled(packageName: string): Promise { - if (process.versions.pnp) { - return true; - } - - try { - require.resolve(packageName); - return true; - } catch { - // Nothing - } - - // Fallback using fs - let dir = __dirname; - - do { - try { - const stats = await fs.promises.stat(path.join(dir, "node_modules", packageName)); - - if (stats.isDirectory()) { - return true; - } - } catch { - // Nothing - } - } while (dir !== (dir = path.dirname(dir))); - - // Extra fallback using fs and hidden API - // @ts-expect-error No types, private API - const { globalPaths } = await import("node:module"); - - // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274 - const results = await Promise.all( - (globalPaths as string[]).map(async (internalPath) => { - try { - const stats = await fs.promises.stat(path.join(internalPath, packageName)); - - if (stats.isDirectory()) { - return true; - } - } catch { - // Nothing - } - - return false; - }), - ); - - if (results.includes(true)) { - return true; - } - - return false; - } - async getDefaultPackageManager(): Promise { const { sync } = await import("cross-spawn"); @@ -420,6 +402,61 @@ class WebpackCLI { } } + async isPackageInstalled(packageName: string): Promise { + if (process.versions.pnp) { + return true; + } + + try { + require.resolve(packageName); + return true; + } catch { + // Nothing + } + + // Fallback using fs + let dir = __dirname; + + do { + try { + const stats = await fs.promises.stat(path.join(dir, "node_modules", packageName)); + + if (stats.isDirectory()) { + return true; + } + } catch { + // Nothing + } + } while (dir !== (dir = path.dirname(dir))); + + // Extra fallback using fs and hidden API + // @ts-expect-error No types, private API + const { globalPaths } = await import("node:module"); + + // https://github.com/nodejs/node/blob/v18.9.1/lib/internal/modules/cjs/loader.js#L1274 + const results = await Promise.all( + (globalPaths as string[]).map(async (internalPath) => { + try { + const stats = await fs.promises.stat(path.join(internalPath, packageName)); + + if (stats.isDirectory()) { + return true; + } + } catch { + // Nothing + } + + return false; + }), + ); + + if (results.includes(true)) { + return true; + } + + return false; + } + async installPackage( packageName: string, options: { preMessage?: () => void } = {}, @@ -508,46 +545,44 @@ class WebpackCLI { process.exit(2); } - async makeCommand( - commandOptions: CommandOptions, - options: CommandOption[] | (() => CommandOption[]) | (() => Promise), - action: Parameters[0], + async makeCommand( + options: CommandOptions, ): Promise { const alreadyLoaded = this.program.commands.find( - (command) => command.name() === commandOptions.rawName, + (command) => command.name() === options.rawName, ); if (alreadyLoaded) { return; } - const command = this.program.command(commandOptions.name, { - hidden: commandOptions.hidden, - isDefault: commandOptions.isDefault, - }) as Command; + const command = this.program.command(options.name, { + hidden: options.hidden, + isDefault: options.isDefault, + }) as Command & { context: C }; - if (commandOptions.description) { - command.description(commandOptions.description); + if (options.description) { + command.description(options.description); } - if (commandOptions.usage) { - command.usage(commandOptions.usage); + if (options.usage) { + command.usage(options.usage); } - if (Array.isArray(commandOptions.alias)) { - command.aliases(commandOptions.alias); + if (Array.isArray(options.alias)) { + command.aliases(options.alias); } else { - command.alias(commandOptions.alias); + command.alias(options.alias); } - command.pkg = commandOptions.pkg || "webpack-cli"; + command.pkg = options.pkg || "webpack-cli"; const { forHelp } = this.program; let allDependenciesInstalled = true; - if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { - for (const dependency of commandOptions.dependencies) { + if (options.dependencies && options.dependencies.length > 0) { + for (const dependency of options.dependencies) { if ( // Allow to use `./path/to/webpack.js` outside `node_modules` (dependency === WEBPACK_PACKAGE && WEBPACK_PACKAGE_IS_CUSTOM) || @@ -566,6 +601,13 @@ class WebpackCLI { allDependenciesInstalled = false; if (forHelp) { + command.description( + `${ + options.description + } To see all available options you need to install ${options.dependencies + .map((dependency) => `'${dependency}'`) + .join(", ")}.`, + ); continue; } @@ -573,7 +615,7 @@ class WebpackCLI { preMessage: () => { this.logger.error( `For using '${this.colors.green( - commandOptions.rawName, + options.rawName, )}' command you need to install: '${this.colors.green(dependency)}' package.`, ); }, @@ -581,33 +623,44 @@ class WebpackCLI { } } - if (options) { - if (typeof options === "function") { - if ( - forHelp && - !allDependenciesInstalled && - commandOptions.dependencies && - commandOptions.dependencies.length > 0 - ) { - command.description( - `${ - commandOptions.description - } To see all available options you need to install ${commandOptions.dependencies - .map((dependency) => `'${dependency}'`) - .join(", ")}.`, - ); - options = []; - } else { - options = await options(); + command.context = {} as C; + + if (typeof options.preload === "function") { + let data; + + try { + data = await options.preload(); + } catch (err) { + if (!forHelp) { + throw err; } } - for (const option of options) { + command.context = { ...command.context, ...data }; + } + + if (options.options) { + let commandOptions: CommandOption[]; + + if ( + forHelp && + !allDependenciesInstalled && + options.dependencies && + options.dependencies.length > 0 + ) { + commandOptions = []; + } else if (typeof options.options === "function") { + commandOptions = await options.options(command); + } else { + commandOptions = options.options; + } + + for (const option of commandOptions) { this.makeOption(command, option); } } - command.action(action); + command.action(options.action); return command; } @@ -615,7 +668,7 @@ class WebpackCLI { makeOption(command: Command, option: CommandOption) { type MainOption = Pick< CommandOption, - "valueName" | "description" | "defaultValue" | "multiple" + "valueName" | "description" | "defaultValue" | "multiple" | "configs" > & { flags: string; type: Set; @@ -697,6 +750,7 @@ class WebpackCLI { type: mainOptionType, multiple: option.multiple, defaultValue: option.defaultValue, + configs: option.configs, }; if (needNegativeOption) { @@ -770,6 +824,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } else if (mainOption.type.has(Boolean)) { const optionForCommand = new Option(mainOption.flags, mainOption.description).default( @@ -786,6 +844,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } } else if (mainOption.type.size > 1) { @@ -816,6 +878,10 @@ class WebpackCLI { optionForCommand.hidden = option.hidden || false; + if (option.configs) { + (optionForCommand as Option & { configs: ArgumentConfig[] }).configs = option.configs; + } + command.addOption(optionForCommand); } else if (mainOption.type.size === 0 && negativeOption) { const optionForCommand = new Option(mainOption.flags, mainOption.description); @@ -838,280 +904,99 @@ class WebpackCLI { } } - getBuiltInOptions(): CommandOption[] { - if (this.#builtInOptionsCache) { - return this.#builtInOptionsCache; - } - - const builtInFlags: CommandOption[] = [ - // For configs - { - name: "config", - alias: "c", - configs: [ - { - type: "string", - }, - ], - multiple: true, - valueName: "pathToConfigFile", - description: - 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".', - hidden: false, - }, - { - name: "config-name", - configs: [ - { - type: "string", - }, - ], - multiple: true, - valueName: "name", - description: - "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.", - hidden: false, - }, - { - name: "merge", - alias: "m", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Merge two or more configurations using 'webpack-merge'.", - hidden: false, - }, - // Complex configs - { - name: "env", - type: ( - value: string, - previous: Record = {}, - ): Record => { - // This ensures we're only splitting by the first `=` - const [allKeys, val] = value.split(/[=](.+)/, 2); - const splitKeys = allKeys.split(/\.(?!$)/); - - let prevRef = previous; - - for (let [index, someKey] of splitKeys.entries()) { - // https://github.com/webpack/webpack-cli/issues/3284 - if (someKey.endsWith("=")) { - // remove '=' from key - someKey = someKey.slice(0, -1); - // @ts-expect-error we explicitly want to set it to undefined - prevRef[someKey] = undefined; - continue; - } - - if (!prevRef[someKey]) { - prevRef[someKey] = {}; - } - - if (typeof prevRef[someKey] === "string") { - prevRef[someKey] = {}; - } - - if (index === splitKeys.length - 1) { - prevRef[someKey] = typeof val === "string" ? val : true; - } - - prevRef = prevRef[someKey] as Record; - } + isMultipleConfiguration( + config: Configuration | MultiConfiguration, + ): config is MultiConfiguration { + return Array.isArray(config); + } - return previous; - }, - multiple: true, - description: - 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".', - hidden: false, - }, - { - name: "config-node-env", - configs: [ - { - type: "string", - }, - ], - multiple: false, - description: - "Sets process.env.NODE_ENV to the specified value for access within the configuration.", - hidden: false, - }, + isMultipleCompiler(compiler: Compiler | MultiCompiler): compiler is MultiCompiler { + return (compiler as MultiCompiler).compilers as unknown as boolean; + } - // Adding more plugins - { - name: "analyze", - configs: [ - { - type: "enum", - values: [true], - }, - ], - multiple: false, - description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", - hidden: false, - }, - { - name: "progress", - configs: [ - { - type: "string", - }, - { - type: "enum", - values: [true], - }, - ], - description: "Print compilation progress during build.", - hidden: false, - }, + isValidationError(error: unknown): error is WebpackError { + return (error as Error).name === "ValidationError"; + } - // Output options - { - name: "json", - configs: [ - { - type: "string", - }, - { - type: "enum", - values: [true], - }, - ], - alias: "j", - valueName: "pathToJsonFile", - description: "Prints result as JSON or store it in a file.", - hidden: false, - }, - { - name: "fail-on-warnings", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.", - hidden: false, - }, - { - name: "disable-interpret", - configs: [ - { - type: "enum", - values: [true], - }, - ], - description: "Disable interpret for loading the config file.", - hidden: false, - }, - ]; - - // 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/main/test/__snapshots__/Cli.basictest.js.snap - // Fast search, `includes` is slow - const minHelpSet = new Set([ - "mode", - "watch", - "watch-options-stdin", - "stats", - "devtool", - "entry", - "target", - "name", - "output-path", - "extends", - ]); - const minimumNegativeHelpFlags = new Set(["devtool"]); - const coreArgs = this.webpack.cli.getArguments(); + schemaToOptions( + webpackMod: typeof webpack, + schema: Schema = undefined, + additionalOptions: CommandOption[] = [], + override: Partial = {}, + ): CommandOption[] { + const args = webpackMod.cli.getArguments(schema); // Take memory const options: CommandOption[] = Array.from({ - length: builtInFlags.length + Object.keys(coreArgs).length, + length: additionalOptions.length + Object.keys(args).length, }); let i = 0; // Adding own options - for (; i < builtInFlags.length; i++) options[i] = builtInFlags[i]; + for (; i < additionalOptions.length; i++) options[i] = additionalOptions[i]; // Adding core options - for (const name in coreArgs) { - const meta = coreArgs[name]; + for (const name in args) { + const meta = args[name]; options[i++] = { ...meta, name, description: meta.description, - group: "core", - hidden: !minHelpSet.has(name), - negativeHidden: !minimumNegativeHelpFlags.has(name), + hidden: !this.#minimumHelpOptions.has(name), + negativeHidden: !this.#minimumNegativeHelpOptions.has(name), + ...override, }; } - this.#builtInOptionsCache = options; - return options; } - static #commands: Record< - "build" | "watch" | "version" | "help" | "serve" | "info" | "configtest", - CommandOptions - > = { - build: { - rawName: "build", - name: "build [entries...]", - alias: ["bundle", "b"], - description: "Run webpack (default command, can be omitted).", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE], - }, - watch: { - rawName: "watch", - name: "watch [entries...]", - alias: "w", - description: "Run webpack and watch for files changes.", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE], - }, - serve: { - rawName: "serve", - name: "serve [entries...]", - alias: ["server", "s"], - description: "Run the webpack dev server and watch for source file changes while serving.", - usage: "[entries...] [options]", - dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], - }, - version: { - rawName: "version", - name: "version", - alias: "v", - usage: "[options]", - description: - "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", - }, - info: { - rawName: "info", - name: "info", - alias: "i", - usage: "[options]", - description: "Outputs information about your system.", - }, - help: { - rawName: "help", - name: "help [command] [option]", - alias: "h", - description: "Display help for commands and options.", - }, - configtest: { - rawName: "configtest", - name: "configtest [config-path]", - alias: "t", - description: "Validate a webpack configuration.", - dependencies: [WEBPACK_PACKAGE], - }, - }; + #processArguments( + webpackMod: typeof webpack, + args: Record, + configuration: RecordAny, + values: ProcessedArguments, + ) { + const problems = webpackMod.cli.processArguments(args, configuration, values); + + if (problems) { + const groupBy = >(xs: Problem[], key: K) => + xs.reduce( + (rv, problem) => { + const path = problem[key]; + + (rv[path] ||= []).push(problem); + + return rv; + }, + {} as Record, + ); + const problemsByPath = groupBy<"path">(problems, "path"); + + for (const path in problemsByPath) { + const problems = problemsByPath[path]; + + for (const problem of problems) { + this.logger.error( + `${this.capitalizeFirstLetter(problem.type.replaceAll("-", " "))}${ + problem.value ? ` '${problem.value}'` : "" + } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ + problem.index ? ` by index '${problem.index}'` : "" + }`, + ); + + if (problem.expected) { + if (problem.expected === "true | false") { + this.logger.error("Expected: without value or negative option"); + } else { + this.logger.error(`Expected: '${problem.expected}'`); + } + } + } + } + + process.exit(2); + } + } async #outputHelp( options: string[], @@ -1167,18 +1052,8 @@ class WebpackCLI { }, // Support multiple aliases subcommandTerm: (command) => { - const humanReadableArgumentName = (argument: Argument) => { - const nameOutput = argument.name() + (argument.variadic ? "..." : ""); - - return argument.required ? `<${nameOutput}>` : `[${nameOutput}]`; - }; - const args = command.registeredArguments - .map((arg) => humanReadableArgumentName(arg)) - .join(" "); - - return `${command.name()}|${command.aliases().join("|")}${args ? ` ${args}` : ""}${ - command.options.length > 0 ? " [options]" : "" - }`; + const usage = command.usage(); + return `${command.name()}|${command.aliases().join("|")}${usage.length > 0 ? ` ${usage}` : ""}`; }, visibleOptions: function visibleOptions(command) { return command.options.filter((option) => { @@ -1279,12 +1154,12 @@ class WebpackCLI { if (isGlobalHelp) { await Promise.all( - Object.values(WebpackCLI.#commands).map((knownCommand) => + Object.values(this.#commands).map((knownCommand) => this.#loadCommandByName(knownCommand.rawName), ), ); - const buildCommand = this.#findCommandByName(WebpackCLI.#commands.build.rawName); + const buildCommand = this.#findCommandByName(this.#commands.build.rawName); if (buildCommand) { this.logger.raw(buildCommand.helpInformation()); @@ -1297,7 +1172,7 @@ class WebpackCLI { const command = this.#findCommandByName(name); if (!command) { - const builtInCommandUsed = Object.values(WebpackCLI.#commands).find( + const builtInCommandUsed = Object.values(this.#commands).find( (command) => command.name.includes(name) || name === command.alias, ); if (typeof builtInCommandUsed !== "undefined") { @@ -1315,7 +1190,7 @@ class WebpackCLI { } } else if (isHelpCommandSyntax) { let isCommandSpecified = false; - let commandName = WebpackCLI.#commands.build.rawName; + let commandName = this.#commands.build.rawName; let optionName = ""; if (options.length === 1) { @@ -1379,10 +1254,10 @@ class WebpackCLI { this.logger.raw(`${bold("Default value:")} ${JSON.stringify(option.defaultValue)}`); } - const flag = this.getBuiltInOptions().find((flag) => option.long === `--${flag.name}`); + const { configs } = option as Option & { configs?: ArgumentConfig[] }; - if (flag?.configs) { - const possibleValues = flag.configs.reduce((accumulator, currentValue) => { + if (configs) { + const possibleValues = configs.reduce((accumulator, currentValue) => { if (currentValue.values) { return [...accumulator, ...currentValue.values]; } @@ -1486,29 +1361,6 @@ class WebpackCLI { return info; } - #findCommandByName(name: string) { - return this.program.commands.find( - (command) => name === command.name() || command.aliases().includes(name), - ); - } - - #isCommand(input: string, commandOptions: CommandOptions) { - const longName = commandOptions.rawName; - - if (input === longName) { - return true; - } - - if (commandOptions.alias) { - if (Array.isArray(commandOptions.alias)) { - return commandOptions.alias.includes(input); - } - return commandOptions.alias === input; - } - - return false; - } - async #loadPackage(pkg: string, isCustom: boolean): Promise { const importTarget = isCustom && /^(?:[A-Za-z]:(\\|\/)|\\\\|\/)/.test(pkg) ? pathToFileURL(pkg).toString() : pkg; @@ -1524,361 +1376,562 @@ class WebpackCLI { return this.#loadPackage(WEBPACK_DEV_SERVER_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM); } - async #loadCommandByName(commandName: string, allowToInstall = false) { - const isBuildCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.build); - const isWatchCommandUsed = this.#isCommand(commandName, WebpackCLI.#commands.watch); - - if (isBuildCommandUsed || isWatchCommandUsed) { - await this.makeCommand( - isBuildCommandUsed ? WebpackCLI.#commands.build : WebpackCLI.#commands.watch, - async () => { - this.webpack = await this.loadWebpack(); - - return this.getBuiltInOptions(); + #minimumHelpOptions = new Set([ + "mode", + "watch", + "watch-options-stdin", + "stats", + "devtool", + "entry", + "target", + "name", + "output-path", + "extends", + ]); + + #minimumNegativeHelpOptions = new Set(["devtool"]); + + #CLIOptions: CommandOption[] = [ + // For configs + { + name: "config", + alias: "c", + configs: [ + { + type: "string", }, - async (entries: string[], options: CommanderArgs) => { - if (entries.length > 0) { - options.entry = [...entries, ...(options.entry || [])]; - } - - await this.runWebpack(options, isWatchCommandUsed); + ], + multiple: true, + valueName: "pathToConfigFile", + description: + 'Provide path to one or more webpack configuration files to process, e.g. "./webpack.config.js".', + hidden: false, + }, + { + name: "config-name", + configs: [ + { + type: "string", }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.serve)) { - const loadDevServerOptions = async () => { - const devServer = await this.loadWebpackDevServer(); - - // @ts-expect-error different schema types - const options = this.webpack.cli.getArguments(devServer.schema) as unknown as Record< - string, - CommandOption - >; + ], + multiple: true, + valueName: "name", + description: + "Name(s) of particular configuration(s) to use if configuration file exports an array of multiple configurations.", + hidden: false, + }, + { + name: "merge", + alias: "m", + configs: [ + { + type: "enum", + values: [true], + }, + ], + description: "Merge two or more configurations using 'webpack-merge'.", + hidden: false, + }, + // Complex configs + { + name: "env", + type: ( + value: string, + previous: Record = {}, + ): Record => { + // This ensures we're only splitting by the first `=` + const [allKeys, val] = value.split(/[=](.+)/, 2); + const splitKeys = allKeys.split(/\.(?!$)/); + + let prevRef = previous; + + for (let [index, someKey] of splitKeys.entries()) { + // https://github.com/webpack/webpack-cli/issues/3284 + if (someKey.endsWith("=")) { + // remove '=' from key + someKey = someKey.slice(0, -1); + // @ts-expect-error we explicitly want to set it to undefined + prevRef[someKey] = undefined; + continue; + } - return Object.keys(options).map((key) => { - options[key].name = key; + if (!prevRef[someKey]) { + prevRef[someKey] = {}; + } - return options[key]; - }); - }; + if (typeof prevRef[someKey] === "string") { + prevRef[someKey] = {}; + } - await this.makeCommand( - WebpackCLI.#commands.serve, - async () => { - this.webpack = await this.loadWebpack(); + if (index === splitKeys.length - 1) { + prevRef[someKey] = typeof val === "string" ? val : true; + } - let devServerOptions = []; + prevRef = prevRef[someKey] as Record; + } - try { - devServerOptions = await loadDevServerOptions(); - } catch (error) { - this.logger.error( - `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`, - ); - process.exit(2); - } + return previous; + }, + multiple: true, + description: + 'Environment variables passed to the configuration when it is a function, e.g. "myvar" or "myvar=myval".', + hidden: false, + }, + { + name: "config-node-env", + configs: [ + { + type: "string", + }, + ], + multiple: false, + description: + "Sets process.env.NODE_ENV to the specified value for access within the configuration.", + hidden: false, + }, - const webpackOptions = this.getBuiltInOptions(); + // Adding more plugins + { + name: "analyze", + configs: [ + { + type: "enum", + values: [true], + }, + ], + multiple: false, + description: "It invokes webpack-bundle-analyzer plugin to get bundle information.", + hidden: false, + }, + { + name: "progress", + configs: [ + { + type: "string", + }, + { + type: "enum", + values: [true], + }, + ], + description: "Print compilation progress during build.", + hidden: false, + }, - return [...webpackOptions, ...devServerOptions]; + // Output options + { + name: "json", + configs: [ + { + type: "string", + }, + { + type: "enum", + values: [true], + }, + ], + alias: "j", + valueName: "pathToJsonFile", + description: "Prints result as JSON or store it in a file.", + hidden: false, + }, + { + name: "fail-on-warnings", + configs: [ + { + type: "enum", + values: [true], + }, + ], + description: "Stop webpack-cli process with non-zero exit code on warnings from webpack.", + hidden: false, + }, + { + name: "disable-interpret", + configs: [ + { + type: "enum", + values: [true], }, - async (entries: string[], options: CommanderArgs) => { - const builtInOptions = this.getBuiltInOptions(); - let devServerFlags: CommandOption[] = []; + ], + description: "Disable interpret for loading the config file.", + hidden: false, + }, + ]; - try { - devServerFlags = await loadDevServerOptions(); - } catch { - // Nothing, to prevent future updates - } + #commands: KnownWebpackCLICommands = { + build: { + rawName: "build", + name: "build [entries...]", + alias: ["bundle", "b"], + description: "Run webpack (default command, can be omitted).", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE], + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + options: async (cmd) => + this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), + action: async (entries, options, cmd) => { + const { webpack } = cmd.context; - const webpackCLIOptions: Partial = {}; - const devServerCLIOptions: CommanderArgs = {}; + if (entries.length > 0) { + options.entry = [...entries, ...(options.entry || [])]; + } - for (const optionName in options) { - const kebabedOption = this.toKebabCase(optionName); - const isBuiltInOption = builtInOptions.find( - (builtInOption) => builtInOption.name === kebabedOption, - ); + options.webpack = webpack; + + await this.runWebpack(options as Options, false); + }, + }, + watch: { + rawName: "watch", + name: "watch [entries...]", + alias: "w", + description: "Run webpack and watch for files changes.", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE], + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + options: async (cmd) => + this.schemaToOptions(cmd.context.webpack, undefined, this.#CLIOptions), + action: async (entries, options, cmd) => { + const { webpack } = cmd.context; - if (isBuiltInOption) { - webpackCLIOptions[optionName as keyof Options] = options[optionName]; - } else { - devServerCLIOptions[optionName] = options[optionName]; - } - } + if (entries.length > 0) { + options.entry = [...entries, ...(options.entry || [])]; + } - if (entries.length > 0) { - webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; - } + options.webpack = webpack; - webpackCLIOptions.argv = { - ...options, - env: { WEBPACK_SERVE: true, ...options.env }, - }; + await this.runWebpack(options as Options, true); + }, + }, + serve: { + rawName: "serve", + name: "serve [entries...]", + alias: ["server", "s"], + description: "Run the webpack dev server and watch for source file changes while serving.", + usage: "[entries...] [options]", + dependencies: [WEBPACK_PACKAGE, WEBPACK_DEV_SERVER_PACKAGE], + preload: async () => { + const webpack = await this.loadWebpack(); + const webpackOptions = this.schemaToOptions(webpack, undefined, this.#CLIOptions); + const devServer = await this.loadWebpackDevServer(); + // @ts-expect-error different versions of the `Schema` type + const devServerOptions = this.schemaToOptions(webpack, devServer.schema, undefined, { + hidden: false, + negativeHidden: false, + }); - webpackCLIOptions.isWatchingLikeCommand = true; + return { webpack, webpackOptions, devServer, devServerOptions }; + }, + options: (cmd) => { + const { webpackOptions, devServerOptions } = cmd.context; - const compiler = await this.createCompiler(webpackCLIOptions); + return [...webpackOptions, ...devServerOptions]; + }, + action: async (entries: string[], options: CommanderArgs, cmd) => { + const { webpack, webpackOptions, devServerOptions } = cmd.context; + const webpackCLIOptions: Options = { webpack, isWatchingLikeCommand: true }; + const devServerCLIOptions: CommanderArgs = {}; + + for (const optionName in options) { + const kebabedOption = this.toKebabCase(optionName); + const isBuiltInOption = webpackOptions.find( + (builtInOption) => builtInOption.name === kebabedOption, + ); - if (!compiler) { - return; + if (isBuiltInOption) { + webpackCLIOptions[optionName as keyof Options] = options[optionName]; + } else { + devServerCLIOptions[optionName] = options[optionName]; } + } - type DevServerConstructor = typeof import("webpack-dev-server"); - let DevServer: DevServerConstructor; + if (entries.length > 0) { + webpackCLIOptions.entry = [...entries, ...(options.entry || [])]; + } - try { - DevServer = await this.loadWebpackDevServer(); - } catch (err) { - this.logger.error( - `You need to install 'webpack-dev-server' for running 'webpack serve'.\n${err}`, - ); - process.exit(2); - } + webpackCLIOptions.argv = { + ...options, + env: { WEBPACK_SERVE: true, ...options.env }, + }; - const servers: InstanceType[] = []; + const compiler = await this.createCompiler(webpackCLIOptions); - if (this.needWatchStdin(compiler)) { - process.stdin.on("end", () => { - Promise.all(servers.map((server) => server.stop())).then(() => { - process.exit(0); - }); - }); - process.stdin.resume(); - } + if (!compiler) { + return; + } - const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler]; - const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer); - const compilersForDevServer = - possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; - const usedPorts: number[] = []; + type DevServerConstructor = typeof import("webpack-dev-server"); - for (const compilerForDevServer of compilersForDevServer) { - if (compilerForDevServer.options.devServer === false) { - continue; - } + const DevServer: DevServerConstructor = cmd.context.devServer; + const servers: InstanceType[] = []; - const devServerConfiguration: DevServerConfiguration = - compilerForDevServer.options.devServer || {}; + if (this.#needWatchStdin(compiler)) { + process.stdin.on("end", () => { + Promise.all(servers.map((server) => server.stop())).then(() => { + process.exit(0); + }); + }); + process.stdin.resume(); + } - const args: Record = {}; - const values: ProcessedArguments = {}; + const compilers = this.isMultipleCompiler(compiler) ? compiler.compilers : [compiler]; + const possibleCompilers = compilers.filter((compiler) => compiler.options.devServer); + const compilersForDevServer = + possibleCompilers.length > 0 ? possibleCompilers : [compilers[0]]; + const usedPorts: number[] = []; - for (const name of Object.keys(options)) { - if (name === "argv") continue; + for (const compilerForDevServer of compilersForDevServer) { + if (compilerForDevServer.options.devServer === false) { + continue; + } - const kebabName = this.toKebabCase(name); - const arg = devServerFlags.find((item) => item.name === kebabName); + const devServerConfiguration: DevServerConfiguration = + compilerForDevServer.options.devServer || {}; - if (arg) { - args[name] = arg as unknown as WebpackArgument; - // We really don't know what the value is - // eslint-disable-next-line @typescript-eslint/no-explicit-any - values[name] = options[name as keyof Options] as any; - } - } + const args: Record = {}; + const values: ProcessedArguments = {}; - if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments( - args, - devServerConfiguration, - values, - ); + for (const name of Object.keys(options)) { + if (name === "argv") continue; - if (problems) { - const groupBy = >( - xs: Problem[], - key: K, - ) => - xs.reduce( - (rv, problem) => { - const path = problem[key]; - - (rv[path] ||= []).push(problem); - - return rv; - }, - {} as Record, - ); - - const problemsByPath = groupBy<"path">(problems, "path"); - - for (const path in problemsByPath) { - const problems = problemsByPath[path]; - - for (const problem of problems) { - this.logger.error( - `${this.capitalizeFirstLetter(problem.type.replace("-", " "))}${ - problem.value ? ` '${problem.value}'` : "" - } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ - problem.index ? ` by index '${problem.index}'` : "" - }`, - ); - - if (problem.expected) { - this.logger.error(`Expected: '${problem.expected}'`); - } - } - } + const kebabName = this.toKebabCase(name); + const arg = devServerOptions.find((item) => item.name === kebabName); - process.exit(2); - } + if (arg) { + args[name] = arg as unknown as WebpackArgument; + // We really don't know what the value is + // eslint-disable-next-line @typescript-eslint/no-explicit-any + values[name] = options[name as keyof Options] as any; } + } - if (devServerConfiguration.port) { - const portNumber = Number(devServerConfiguration.port); + if (Object.keys(values).length > 0) { + this.#processArguments(webpack, args, devServerConfiguration, values); + } - if (usedPorts.includes(portNumber)) { - throw new Error( - "Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.", - ); - } + if (devServerConfiguration.port) { + const portNumber = Number(devServerConfiguration.port); - usedPorts.push(portNumber); + if (usedPorts.includes(portNumber)) { + throw new Error( + "Unique ports must be specified for each devServer option in your webpack configuration. Alternatively, run only 1 devServer config using the --config-name flag to specify your desired config.", + ); } - try { - const server = new DevServer(devServerConfiguration, compiler); + usedPorts.push(portNumber); + } - await server.start(); + try { + const server = new DevServer(devServerConfiguration, compiler); - servers.push(server as unknown as InstanceType); - } catch (error) { - if (this.isValidationError(error as Error)) { - this.logger.error((error as Error).message); - } else { - this.logger.error(error); - } + await server.start(); - process.exit(2); + servers.push(server as unknown as InstanceType); + } catch (error) { + if (this.isValidationError(error as Error)) { + this.logger.error((error as Error).message); + } else { + this.logger.error(error); } - } - if (servers.length === 0) { - this.logger.error("No dev server configurations to run"); process.exit(2); } + } + + if (servers.length === 0) { + this.logger.error("No dev server configurations to run"); + process.exit(2); + } + }, + }, + help: { + rawName: "help", + name: "help [command] [option]", + alias: "h", + description: "Display help for commands and options.", + action: () => { + // Nothing, just stub + }, + }, + version: { + rawName: "version", + name: "version", + alias: "v", + usage: "[options]", + description: + "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and other packages.", + options: [ + { + name: "output", + alias: "o", + configs: [ + { + type: "string", + }, + ], + description: "To get the output in a specified format (accept json or markdown)", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.help)) { - await this.makeCommand(WebpackCLI.#commands.help, [], () => { - // Stub for the `help` command - }); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.version)) { - await this.makeCommand( - WebpackCLI.#commands.version, - [ - { - name: "output", - alias: "o", - configs: [ - { - type: "string", - }, - ], - description: "To get the output in a specified format (accept json or markdown)", - hidden: false, + ], + action: async (options: { output?: string }) => { + let info = await this.#getInfoOutput({ + ...options, + information: { + npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`, }, - ], - async (options: { output?: string }) => { - let info = await this.#getInfoOutput({ - ...options, - information: { - npmPackages: `{${DEFAULT_WEBPACK_PACKAGES.map((item) => `*${item}*`).join(",")}}`, - }, - }); + }); - if (typeof options.output === "undefined") { - info = info.replace("Packages:", "").replaceAll(/^\s+/gm, "").trim(); - } + if (typeof options.output === "undefined") { + info = info.replace("Packages:", "").replaceAll(/^\s+/gm, "").trim(); + } - this.logger.raw(info); + this.logger.raw(info); + }, + }, + info: { + rawName: "info", + name: "info", + alias: "i", + usage: "[options]", + description: "Outputs information about your system.", + options: [ + { + name: "output", + alias: "o", + configs: [ + { + type: "string", + }, + ], + description: "To get the output in a specified format (accept json or markdown)", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.info)) { - await this.makeCommand( - WebpackCLI.#commands.info, - [ - { - name: "output", - alias: "o", - configs: [ - { - type: "string", - }, - ], - description: "To get the output in a specified format (accept json or markdown)", - hidden: false, - }, - { - name: "additional-package", - alias: "a", - configs: [{ type: "string" }], - multiple: true, - description: "Adds additional packages to the output", - hidden: false, - }, - ], - async (options: { output?: string; additionalPackage?: string[] }) => { - const info = await this.#getInfoOutput(options); - - this.logger.raw(info); + { + name: "additional-package", + alias: "a", + configs: [{ type: "string" }], + multiple: true, + description: "Adds additional packages to the output", + hidden: false, }, - ); - } else if (this.#isCommand(commandName, WebpackCLI.#commands.configtest)) { - await this.makeCommand( - WebpackCLI.#commands.configtest, - [], - async (configPath: string | undefined) => { - this.webpack = await this.loadWebpack(); - - const env: Env = {}; - const argv: Argv = { env }; - const config = await this.loadConfig( - configPath ? { config: [configPath] } : { env, argv }, - ); - const configPaths = new Set(); + ], + action: async (options: { output?: string; additionalPackage?: string[] }) => { + const info = await this.#getInfoOutput(options); - if (Array.isArray(config.options)) { - for (const options of config.options) { - const loadedConfigPaths = config.path.get(options); + this.logger.raw(info); + }, + }, + configtest: { + rawName: "configtest", + name: "configtest [config-path]", + alias: "t", + description: "Validate a webpack configuration.", + dependencies: [WEBPACK_PACKAGE], + options: [], + preload: async () => { + const webpack = await this.loadWebpack(); + return { webpack }; + }, + action: async (configPath: string | undefined, _options: CommanderArgs, cmd) => { + const { webpack } = cmd.context; + const env: Env = {}; + const argv: Argv = { env }; + const config = await this.loadConfig( + configPath ? { env, argv, webpack, config: [configPath] } : { env, argv, webpack }, + ); + const configPaths = new Set(); - if (loadedConfigPaths) { - for (const path of loadedConfigPaths) configPaths.add(path); - } - } - } else if (config.path.get(config.options)) { - const loadedConfigPaths = config.path.get(config.options); + if (Array.isArray(config.options)) { + for (const options of config.options) { + const loadedConfigPaths = config.path.get(options); if (loadedConfigPaths) { for (const path of loadedConfigPaths) configPaths.add(path); } } + } else if (config.path.get(config.options)) { + const loadedConfigPaths = config.path.get(config.options); - if (configPaths.size === 0) { - this.logger.error("No configuration found."); - process.exit(2); + if (loadedConfigPaths) { + for (const path of loadedConfigPaths) configPaths.add(path); } + } - this.logger.info(`Validate '${[...configPaths].join(" ,")}'.`); + if (configPaths.size === 0) { + this.logger.error("No configuration found."); + process.exit(2); + } - try { - this.webpack.validate(config.options); - } catch (error) { - if (this.isValidationError(error as Error)) { - this.logger.error((error as Error).message); - } else { - this.logger.error(error); - } + this.logger.info(`Validate '${[...configPaths].join(" ,")}'.`); - process.exit(2); + try { + cmd.context.webpack.validate(config.options); + } catch (error) { + if (this.isValidationError(error as Error)) { + this.logger.error((error as Error).message); + } else { + this.logger.error(error); } - this.logger.success("There are no validation errors in the given webpack configuration."); - }, - ); + process.exit(2); + } + + this.logger.success("There are no validation errors in the given webpack configuration."); + }, + }, + }; + + #isCommand( + input: string, + commandOptions: CommandOptions, + ) { + const longName = commandOptions.rawName; + + if (input === longName) { + return true; + } + + if (commandOptions.alias) { + if (Array.isArray(commandOptions.alias)) { + return commandOptions.alias.includes(input); + } + return commandOptions.alias === input; + } + + return false; + } + + #findCommandByName(name: string) { + return this.program.commands.find( + (command) => name === command.name() || command.aliases().includes(name), + ); + } + + async #loadCommandByName(commandName: string, allowToInstall = false) { + if (this.#isCommand(commandName, this.#commands.build)) { + await this.makeCommand(this.#commands.build); + } else if (this.#isCommand(commandName, this.#commands.serve)) { + await this.makeCommand(this.#commands.serve); + } else if (this.#isCommand(commandName, this.#commands.watch)) { + await this.makeCommand(this.#commands.watch); + } else if (this.#isCommand(commandName, this.#commands.help)) { + // Stub for the `help` command + await this.makeCommand(this.#commands.help); + } else if (this.#isCommand(commandName, this.#commands.version)) { + await this.makeCommand(this.#commands.version); + } else if (this.#isCommand(commandName, this.#commands.info)) { + await this.makeCommand(this.#commands.info); + } else if (this.#isCommand(commandName, this.#commands.configtest)) { + await this.makeCommand(this.#commands.configtest); } else { - const builtInExternalCommandInfo = Object.values(WebpackCLI.#commands) + const builtInExternalCommandInfo = Object.values(this.#commands) .filter((item) => item.external) .find( (externalBuiltInCommandInfo) => @@ -1962,7 +2015,7 @@ class WebpackCLI { const { operands } = this.program.parseOptions(this.program.args); const operand = - typeof operands[0] !== "undefined" ? operands[0] : WebpackCLI.#commands.build.rawName; + typeof operands[0] !== "undefined" ? operands[0] : this.#commands.build.rawName; if (operand) { const command = this.#findCommandByName(operand); @@ -2026,11 +2079,11 @@ class WebpackCLI { this.program.allowExcessArguments(true); this.program.action(async (options) => { const { operands, unknown } = this.program.parseOptions(this.program.args); - const defaultCommandNameToRun = WebpackCLI.#commands.build.rawName; + const defaultCommandNameToRun = this.#commands.build.rawName; const hasOperand = typeof operands[0] !== "undefined"; const operand = hasOperand ? operands[0] : defaultCommandNameToRun; const isHelpOption = typeof options.help !== "undefined"; - const isHelpCommandSyntax = this.#isCommand(operand, WebpackCLI.#commands.help); + const isHelpCommandSyntax = this.#isCommand(operand, this.#commands.help); if (isHelpOption || isHelpCommandSyntax) { let isVerbose = false; @@ -2081,7 +2134,7 @@ class WebpackCLI { let isKnownCommand = false; - for (const command of Object.values(WebpackCLI.#commands)) { + for (const command of Object.values(this.#commands)) { if ( command.rawName === commandNameToRun || (Array.isArray(command.alias) @@ -2113,7 +2166,7 @@ class WebpackCLI { } else { this.logger.error(`Unknown command or entry '${operand}'`); - const found = Object.values(WebpackCLI.#commands).find( + const found = Object.values(this.#commands).find( (commandOptions) => distance(operand, commandOptions.rawName) < 3, ); @@ -2142,93 +2195,84 @@ class WebpackCLI { await this.program.parseAsync(args, parseOptions); } - async #loadConfigurationFile( - configPath: string, - disableInterpret = false, - ): Promise { - let pkg: LoadableWebpackConfiguration | undefined; - - let loadingError; + async loadConfig(options: Options) { + const disableInterpret = + typeof options.disableInterpret !== "undefined" && options.disableInterpret; - try { - // eslint-disable-next-line no-eval - pkg = (await eval(`import("${pathToFileURL(configPath)}")`)).default; - } catch (err) { - if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { - throw err; - } + const loadConfigByPath = async ( + configPath: string, + argv: Argv = { env: {} }, + ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { + let options: LoadableWebpackConfiguration | undefined; - loadingError = err; - } + try { + let loadingError; - // Fallback logic when we can't use `import(...)` - if (loadingError) { - const { jsVariants, extensions } = await import("interpret"); - const ext = path.extname(configPath).toLowerCase(); + try { + // eslint-disable-next-line no-eval + options = (await eval(`import("${pathToFileURL(configPath)}")`)).default; + } catch (err) { + if (this.isValidationError(err) || process.env?.WEBPACK_CLI_FORCE_LOAD_ESM_CONFIG) { + throw err; + } - let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); + loadingError = err; + } - if (!interpreted && ext.endsWith(".cts")) { - interpreted = jsVariants[".ts"] as string; - } + // Fallback logic when we can't use `import(...)` + if (loadingError) { + const { jsVariants, extensions } = await import("interpret"); + const ext = path.extname(configPath).toLowerCase(); - if (interpreted && !disableInterpret) { - const rechoir: Rechoir = (await import("rechoir")).default; + let interpreted = Object.keys(jsVariants).find((variant) => variant === ext); - try { - rechoir.prepare(extensions, configPath); - } catch (error) { - if ((error as RechoirError)?.failures) { - this.logger.error(`Unable load '${configPath}'`); - this.logger.error((error as RechoirError).message); - for (const failure of (error as RechoirError).failures) { - this.logger.error(failure.error.message); - } - this.logger.error("Please install one of them"); - process.exit(2); + if (!interpreted && ext.endsWith(".cts")) { + interpreted = jsVariants[".ts"] as string; } - this.logger.error(error); - process.exit(2); - } - } - - try { - pkg = require(configPath); - } catch (err) { - if (this.isValidationError(err)) { - throw err; - } - throw new ConfigurationLoadingError([loadingError, err]); - } - } + if (interpreted && !disableInterpret) { + const rechoir: Rechoir = (await import("rechoir")).default; - // To handle `babel`/`module.exports.default = {};` - if (pkg && typeof pkg === "object" && "default" in pkg) { - pkg = pkg.default as LoadableWebpackConfiguration | undefined; - } + try { + rechoir.prepare(extensions, configPath); + } catch (error) { + if ((error as RechoirError)?.failures) { + this.logger.error(`Unable load '${configPath}'`); + this.logger.error((error as RechoirError).message); + for (const failure of (error as RechoirError).failures) { + this.logger.error(failure.error.message); + } + this.logger.error("Please install one of them"); + process.exit(2); + } + this.logger.error(error); + process.exit(2); + } + } - if (!pkg) { - this.logger.warn( - `Default export is missing or nullish at (from ${configPath}). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.`, - ); - } + try { + options = require(configPath); + } catch (err) { + if (this.isValidationError(err)) { + throw err; + } - return pkg || {}; - } + throw new ConfigurationLoadingError([loadingError, err]); + } + } - async loadConfig(options: Options) { - const disableInterpret = - typeof options.disableInterpret !== "undefined" && options.disableInterpret; + // To handle `babel`/`module.exports.default = {};` + if (options && typeof options === "object" && "default" in options) { + options = options.default as LoadableWebpackConfiguration | undefined; + } - const loadConfigByPath = async ( - configPath: string, - argv: Argv = { env: {} }, - ): Promise<{ options: Configuration | MultiConfiguration; path: string }> => { - let options: LoadableWebpackConfiguration | undefined; + if (!options) { + this.logger.warn( + `Default export is missing or nullish at (from ${configPath}). Webpack will run with an empty configuration. Please double-check that this is what you want. If you want to run webpack with an empty config, \`export {}\`/\`module.exports = {};\` to remove this warning.`, + ); - try { - options = await this.#loadConfigurationFile(configPath, disableInterpret); + options = {}; + } } catch (error) { if (error instanceof ConfigurationLoadingError) { this.logger.error(`Failed to load '${configPath}' config\n${error.message}`); @@ -2507,13 +2551,6 @@ class WebpackCLI { config.path.set(config.options, mergedConfigPaths); } - return config; - } - - async buildConfig( - config: ConfigurationsAndPaths, - options: Options, - ): Promise { if (options.analyze && !(await this.isPackageInstalled("webpack-bundle-analyzer"))) { await this.installPackage("webpack-bundle-analyzer", { preMessage: () => { @@ -2537,11 +2574,11 @@ class WebpackCLI { const { default: CLIPlugin } = (await import("./plugins/cli-plugin.js")).default; + const builtInOptions = this.schemaToOptions(options.webpack); const internalBuildConfig = (configuration: Configuration) => { const originalWatchValue = configuration.watch; // Apply options - const builtInOptions = this.getBuiltInOptions(); const args: Record = {}; const values: ProcessedArguments = {}; @@ -2549,7 +2586,7 @@ class WebpackCLI { if (name === "argv") continue; const kebabName = this.toKebabCase(name); - const arg = builtInOptions.find((item) => item.group === "core" && item.name === kebabName); + const arg = builtInOptions.find((item) => item.name === kebabName); if (arg) { args[name] = arg as unknown as WebpackArgument; @@ -2560,42 +2597,7 @@ class WebpackCLI { } if (Object.keys(values).length > 0) { - const problems = this.webpack.cli.processArguments(args, configuration, values); - - if (problems) { - const groupBy = >(xs: Problem[], key: K) => - xs.reduce( - (rv, problem) => { - const path = problem[key]; - - (rv[path] ||= []).push(problem); - - return rv; - }, - {} as Record, - ); - const problemsByPath = groupBy(problems, "path"); - - for (const path in problemsByPath) { - const problems = problemsByPath[path]; - - for (const problem of problems) { - this.logger.error( - `${this.capitalizeFirstLetter(problem.type.replaceAll("-", " "))}${ - problem.value ? ` '${problem.value}'` : "" - } for the '--${problem.argument.replaceAll(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}' option${ - problem.index ? ` by index '${problem.index}'` : "" - }`, - ); - - if (problem.expected) { - this.logger.error(`Expected: '${problem.expected}'`); - } - } - } - - process.exit(2); - } + this.#processArguments(options.webpack, args, configuration, values); } // Output warnings @@ -2717,28 +2719,22 @@ class WebpackCLI { return config; } - isValidationError(error: unknown): error is WebpackError { - return ( - error instanceof this.webpack.ValidationError || (error as Error).name === "ValidationError" - ); - } - async createCompiler( options: Options, callback?: WebpackCallback, ): Promise { + const { webpack } = options; + if (typeof options.configNodeEnv === "string") { process.env.NODE_ENV = options.configNodeEnv; } - let config = await this.loadConfig(options); - config = await this.buildConfig(config, options); - + const config = await this.loadConfig(options); let compiler: Compiler | MultiCompiler; try { compiler = callback - ? this.webpack(config.options, (error, stats) => { + ? webpack(config.options, (error, stats) => { if (error && this.isValidationError(error)) { this.logger.error(error.message); process.exit(2); @@ -2746,7 +2742,7 @@ class WebpackCLI { callback(error as Error | null, stats); })! - : this.webpack(config.options); + : webpack(config.options); } catch (error) { if (this.isValidationError(error)) { this.logger.error(error.message); @@ -2760,7 +2756,7 @@ class WebpackCLI { return compiler; } - needWatchStdin(compiler: Compiler | MultiCompiler): boolean { + #needWatchStdin(compiler: Compiler | MultiCompiler): boolean { if (this.isMultipleCompiler(compiler)) { return Boolean( compiler.compilers.some((compiler: Compiler) => compiler.options.watchOptions?.stdin), @@ -2894,7 +2890,7 @@ class WebpackCLI { process.on(signal, listener); } - if (this.needWatchStdin(compiler)) { + if (this.#needWatchStdin(compiler)) { process.stdin.on("end", () => { process.exit(0); }); diff --git a/test/api/CLI.test.js b/test/api/CLI.test.js index 30e5f946c20..2dd11521a72 100644 --- a/test/api/CLI.test.js +++ b/test/api/CLI.test.js @@ -19,8 +19,12 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand({ name: "command" }, [], (options) => { - expect(options).toEqual({}); + const command = await cli.makeCommand({ + name: "command", + options: [], + action: (options) => { + expect(options).toEqual({}); + }, }); command.parseAsync([], { from: "user" }); @@ -31,20 +35,18 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -54,21 +56,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -78,11 +78,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -90,10 +88,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--no-boolean"], { from: "user" }); }); @@ -103,11 +101,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-boolean", configs: [ @@ -118,10 +114,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsBoolean: false }); }, - ); + }); command.parseAsync(["--no-configs-boolean"], { from: "user" }); }); @@ -131,11 +127,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-number", configs: [ @@ -146,10 +140,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsNumber: 42 }); }, - ); + }); command.parseAsync(["--configs-number", "42"], { from: "user" }); }); @@ -159,11 +153,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-string", configs: [ @@ -174,10 +166,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsString: "foo" }); }, - ); + }); command.parseAsync(["--configs-string", "foo"], { from: "user" }); }); @@ -187,11 +179,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-path", configs: [ @@ -202,10 +192,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsPath: "/root/foo" }); }, - ); + }); command.parseAsync(["--configs-path", "/root/foo"], { from: "user", @@ -217,11 +207,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "configs-regexp", configs: [ @@ -232,10 +220,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ configsRegexp: "\\w+" }); }, - ); + }); command.parseAsync(["--configs-regexp", "\\w+"], { from: "user" }); }); @@ -245,11 +233,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-string", configs: [ @@ -261,10 +247,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumString: "foo" }); }, - ); + }); command.parseAsync(["--enum-string", "foo"], { from: "user" }); }); @@ -274,11 +260,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-number", configs: [ @@ -290,10 +274,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumNumber: 42 }); }, - ); + }); command.parseAsync(["--enum-number", "42"], { from: "user" }); }); @@ -303,11 +287,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "enum-boolean", configs: [ @@ -319,10 +301,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ enumBoolean: false }); }, - ); + }); command.parseAsync(["--no-enum-boolean"], { from: "user" }); }); @@ -332,11 +314,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -344,10 +324,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--boolean", "--no-boolean"], { from: "user" }); }); @@ -357,11 +337,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -369,10 +347,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--no-boolean", "--boolean"], { from: "user" }); }); @@ -382,11 +360,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -394,10 +370,10 @@ describe("CLI API", () => { defaultValue: false, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -407,21 +383,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "bar" }); }, - ); + }); command.parseAsync(["--string", "bar"], { from: "user" }); }); @@ -431,11 +405,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", alias: "s", @@ -443,10 +415,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "foo" }); }, - ); + }); command.parseAsync(["-s", "foo"], { from: "user" }); }); @@ -456,11 +428,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, @@ -468,10 +438,10 @@ describe("CLI API", () => { defaultValue: "default-value", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "default-value" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -481,11 +451,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, @@ -493,10 +461,10 @@ describe("CLI API", () => { defaultValue: "default-value", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "foo" }); }, - ); + }); command.parseAsync(["--string", "foo"], { from: "user" }); }); @@ -506,21 +474,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", type: String, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "bar" }); }, - ); + }); command.parseAsync(["--string=bar"], { from: "user" }); }); @@ -530,11 +496,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -542,10 +506,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "bar"], { from: "user" }); }); @@ -555,11 +519,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -568,10 +530,10 @@ describe("CLI API", () => { defaultValue: "string", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: "string" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -581,11 +543,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -594,10 +554,10 @@ describe("CLI API", () => { defaultValue: "string", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "--string", "bar"], { from: "user", @@ -609,11 +569,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "string", multiple: true, @@ -621,10 +579,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ string: ["foo", "bar"] }); }, - ); + }); command.parseAsync(["--string", "foo", "--string", "bar"], { from: "user", @@ -636,21 +594,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", type: Number, description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 12 }); }, - ); + }); command.parseAsync(["--number", "12"], { from: "user" }); }); @@ -660,11 +616,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", type: Number, @@ -672,10 +626,10 @@ describe("CLI API", () => { defaultValue: 20, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 20 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -685,11 +639,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", multiple: true, @@ -697,10 +649,10 @@ describe("CLI API", () => { description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: [1, 2] }); }, - ); + }); command.parseAsync(["--number", "1", "--number", "2"], { from: "user", @@ -712,11 +664,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", multiple: true, @@ -725,10 +675,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: [1, 2] }); }, - ); + }); command.parseAsync(["--number", "1", "--number", "2"], { from: "user", @@ -740,11 +690,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "number", multiple: true, @@ -753,10 +701,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ number: 50 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -766,21 +714,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: () => "function", description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: "function" }); }, - ); + }); command.parseAsync(["--custom", "value"], { from: "user" }); }); @@ -790,11 +736,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: () => "function", @@ -802,10 +746,10 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: "default" }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -815,11 +759,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => [...previous, value], @@ -827,10 +769,10 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: ["value", "other"] }); }, - ); + }); command.parseAsync(["--custom", "value", "--custom", "other"], { from: "user", @@ -842,11 +784,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => [...previous, value], @@ -855,10 +795,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: 50 }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -870,11 +810,9 @@ describe("CLI API", () => { let skipDefault = true; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "custom", type: (value, previous = []) => { @@ -890,10 +828,10 @@ describe("CLI API", () => { defaultValue: 50, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ custom: ["foo"] }); }, - ); + }); command.parseAsync(["--custom", "foo"], { from: "user" }); }); @@ -903,21 +841,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -927,21 +863,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: "value" }); }, - ); + }); command.parseAsync(["--boolean-and-string", "value"], { from: "user", @@ -953,11 +887,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -965,10 +897,10 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -978,11 +910,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -990,12 +920,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: ["bar", "baz"], }); }, - ); + }); command.parseAsync(["--boolean-and-string", "bar", "--boolean-and-string", "baz"], { from: "user", @@ -1007,11 +937,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1019,10 +947,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: true }); }, - ); + }); command.parseAsync(["--boolean-and-string"], { from: "user" }); }); @@ -1032,11 +960,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1044,10 +970,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: "foo" }); }, - ); + }); command.parseAsync(["--boolean-and-string", "foo"], { from: "user", @@ -1059,11 +985,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-string", type: [Boolean, String], @@ -1071,10 +995,10 @@ describe("CLI API", () => { negative: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndString: false }); }, - ); + }); command.parseAsync(["--no-boolean-and-string"], { from: "user" }); }); @@ -1084,21 +1008,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number", type: [Boolean, Number], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumber: true }); }, - ); + }); command.parseAsync(["--boolean-and-number"], { from: "user" }); }); @@ -1108,21 +1030,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number", type: [Boolean, Number], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumber: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number", "12"], { from: "user", @@ -1134,21 +1054,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: [Boolean], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); }); @@ -1158,23 +1076,21 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1186,21 +1102,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1212,23 +1126,21 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "bar", }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "bar"], { from: "user", @@ -1240,11 +1152,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1252,12 +1162,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "default", }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -1267,11 +1177,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1279,12 +1187,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "foo", }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1296,11 +1204,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1308,10 +1214,10 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: 12 }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1323,11 +1229,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1335,12 +1239,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1352,11 +1256,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1364,12 +1266,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: true, }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string"], { from: "user", @@ -1381,11 +1283,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1393,12 +1293,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo"], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1410,11 +1310,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1422,12 +1320,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: [12], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1439,11 +1337,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1451,12 +1347,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", "bar"], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "bar"], @@ -1469,11 +1365,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1481,12 +1375,12 @@ describe("CLI API", () => { multiple: true, }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", 12], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "12"], @@ -1499,11 +1393,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1512,12 +1404,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: "default", }); }, - ); + }); command.parseAsync([], { from: "user" }); }); @@ -1527,11 +1419,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1540,12 +1430,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo"], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "foo"], { from: "user", @@ -1557,11 +1447,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1570,12 +1458,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: [12], }); }, - ); + }); command.parseAsync(["--boolean-and-number-and-string", "12"], { from: "user", @@ -1587,11 +1475,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean-and-number-and-string", type: [Boolean, Number, String], @@ -1600,12 +1486,12 @@ describe("CLI API", () => { defaultValue: "default", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ booleanAndNumberAndString: ["foo", 12], }); }, - ); + }); command.parseAsync( ["--boolean-and-number-and-string", "foo", "--boolean-and-number-and-string", "12"], @@ -1618,21 +1504,19 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "unknown", type: [Boolean, Symbol], description: "description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ unknown: "foo" }); }, - ); + }); command.parseAsync(["--unknown", "foo"], { from: "user" }); }); @@ -1642,11 +1526,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -1654,10 +1536,10 @@ describe("CLI API", () => { negatedDescription: "Negated description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: true }); }, - ); + }); command.parseAsync(["--boolean"], { from: "user" }); @@ -1669,11 +1551,9 @@ describe("CLI API", () => { cli.program.commands = []; - const command = await cli.makeCommand( - { - name: "command", - }, - [ + const command = await cli.makeCommand({ + name: "command", + options: [ { name: "boolean", type: Boolean, @@ -1682,10 +1562,10 @@ describe("CLI API", () => { negatedDescription: "Negated description", }, ], - (options) => { + action: (options) => { expect(options).toEqual({ boolean: false }); }, - ); + }); command.parseAsync(["--no-boolean"], { from: "user" }); diff --git a/test/api/resolve-config/resolve-config.test.js b/test/api/resolve-config/resolve-config.test.js deleted file mode 100644 index 1d3fce10fdf..00000000000 --- a/test/api/resolve-config/resolve-config.test.js +++ /dev/null @@ -1,83 +0,0 @@ -const { resolve } = require("node:path"); -const WebpackCLI = require("../../../packages/webpack-cli/lib/webpack-cli").default; -const arrayConfig = require("./webpack.config.cjs"); -const config1 = require("./webpack.config1.cjs"); -const config2 = require("./webpack.config2.cjs"); -const promiseConfig = require("./webpack.promise.config.cjs"); - -const cli = new WebpackCLI(); - -describe("resolveConfig", () => { - it("should handle merge properly", async () => { - const result = await cli.loadConfig({ - merge: true, - config: [resolve(__dirname, "./webpack.config.cjs")], - }); - - const expectedOptions = { - output: { - filename: "./dist-commonjs.js", - libraryTarget: "commonjs", - }, - entry: "./a.js", - name: "amd", - mode: "production", - devtool: "eval-cheap-module-source-map", - target: "node", - }; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should return array for multiple config", async () => { - const result = await cli.loadConfig({ - config: [ - resolve(__dirname, "./webpack.config1.cjs"), - resolve(__dirname, "./webpack.config2.cjs"), - ], - }); - const expectedOptions = [config1, config2]; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should return config object for single config", async () => { - const result = await cli.loadConfig({ - config: [resolve(__dirname, "./webpack.config1.cjs")], - }); - - expect(result.options).toEqual(config1); - }); - - it("should return resolved config object for promise config", async () => { - const result = await cli.loadConfig({ - config: [resolve(__dirname, "./webpack.promise.config.cjs")], - }); - const expectedOptions = await promiseConfig(); - - expect(result.options).toEqual(expectedOptions); - }); - - it("should handle configs returning different types", async () => { - const result = await cli.loadConfig({ - config: [ - resolve(__dirname, "./webpack.promise.config.cjs"), - resolve(__dirname, "./webpack.config.cjs"), - ], - }); - const resolvedPromiseConfig = await promiseConfig(); - const expectedOptions = [resolvedPromiseConfig, ...arrayConfig]; - - expect(result.options).toEqual(expectedOptions); - }); - - it("should handle different env formats", async () => { - const result = await cli.loadConfig({ - argv: { env: { test: true, name: "Hisoka" } }, - config: [resolve(__dirname, "./env.webpack.config.cjs")], - }); - const expectedOptions = { mode: "staging", name: "Hisoka" }; - - expect(result.options).toEqual(expectedOptions); - }); -}); diff --git a/test/api/resolve-config/webpack.config.cjs b/test/api/resolve-config/webpack.config.cjs deleted file mode 100644 index 3d7d3ef7ce6..00000000000 --- a/test/api/resolve-config/webpack.config.cjs +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = [ - { - output: { - filename: "./dist-amd.js", - libraryTarget: "amd", - }, - entry: "./a.js", - name: "amd", - mode: "development", - devtool: "eval-cheap-module-source-map", - }, - { - output: { - filename: "./dist-commonjs.js", - libraryTarget: "commonjs", - }, - entry: "./a.js", - mode: "production", - target: "node", - }, -]; diff --git a/test/api/resolve-config/webpack.config1.cjs b/test/api/resolve-config/webpack.config1.cjs deleted file mode 100644 index c4b7df891f7..00000000000 --- a/test/api/resolve-config/webpack.config1.cjs +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - output: { - libraryTarget: "amd", - }, - entry: "./a.js", - name: "amd", -}; diff --git a/test/api/resolve-config/webpack.config2.cjs b/test/api/resolve-config/webpack.config2.cjs deleted file mode 100644 index 854b414229c..00000000000 --- a/test/api/resolve-config/webpack.config2.cjs +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - output: { - libraryTarget: "commonjs", - }, - entry: "./a.js", - mode: "production", - target: "node", -}; diff --git a/test/api/resolve-config/webpack.promise.config.cjs b/test/api/resolve-config/webpack.promise.config.cjs deleted file mode 100644 index f1083b0702e..00000000000 --- a/test/api/resolve-config/webpack.promise.config.cjs +++ /dev/null @@ -1,14 +0,0 @@ -const path = require("node:path"); - -module.exports = () => - new Promise((resolve) => { - setTimeout(() => { - resolve({ - entry: "./a", - output: { - path: path.resolve(__dirname, "./binary"), - filename: "promise.js", - }, - }); - }, 500); - }); diff --git a/test/build/config/function/async-env.config.js b/test/build/config/function/async-env.config.js new file mode 100644 index 00000000000..4b6bb6b47e6 --- /dev/null +++ b/test/build/config/function/async-env.config.js @@ -0,0 +1,10 @@ +module.exports = function configuration(env) { + const configName = env.name; + return { + name: configName, + mode: "development", + output: { + filename: `./async-${configName}-single.js`, + }, + }; +}; diff --git a/test/build/config/function/async-multi-webpack.config.js b/test/build/config/function/async-multi-webpack.config.js new file mode 100644 index 00000000000..3a20f17aff1 --- /dev/null +++ b/test/build/config/function/async-multi-webpack.config.js @@ -0,0 +1,20 @@ +module.exports = async () => [ + { + output: { + filename: "./multi-async-first.js", + }, + name: "first", + entry: "./src/first.js", + mode: "development", + stats: "minimal", + }, + { + output: { + filename: "./multi-async-second.js", + }, + name: "second", + entry: "./src/second.js", + mode: "development", + stats: "minimal", + }, +]; diff --git a/test/build/config/function/async-single-webpack.config.js b/test/build/config/function/async-single-webpack.config.js new file mode 100644 index 00000000000..353b4d20640 --- /dev/null +++ b/test/build/config/function/async-single-webpack.config.js @@ -0,0 +1,7 @@ +module.exports = async () => ({ + output: { + filename: "./async-single.js", + }, + name: "single", + mode: "development", +}); diff --git a/test/api/resolve-config/env.webpack.config.cjs b/test/build/config/function/env.config.js similarity index 56% rename from test/api/resolve-config/env.webpack.config.cjs rename to test/build/config/function/env.config.js index 4707f9e6c86..04fa62be056 100644 --- a/test/api/resolve-config/env.webpack.config.cjs +++ b/test/build/config/function/env.config.js @@ -2,6 +2,9 @@ module.exports = function configuration(env) { const configName = env.name; return { name: configName, - mode: env.test ? "staging" : "production", + mode: "development", + output: { + filename: `./${configName}-single.js`, + }, }; }; diff --git a/test/build/config/function/function-each-multi-webpack.config.js b/test/build/config/function/function-each-multi-webpack.config.js new file mode 100644 index 00000000000..86c3df51b3b --- /dev/null +++ b/test/build/config/function/function-each-multi-webpack.config.js @@ -0,0 +1,20 @@ +module.exports = [ + () => ({ + output: { + filename: "./function-each-first.js", + }, + name: "first", + entry: "./src/first.js", + mode: "development", + stats: "minimal", + }), + async () => ({ + output: { + filename: "./function-each-second.js", + }, + name: "second", + entry: "./src/second.js", + mode: "development", + stats: "minimal", + }), +]; diff --git a/test/build/config/function/functional-config.test.js b/test/build/config/function/functional-config.test.js index 2408e1e1300..db00488d1b0 100644 --- a/test/build/config/function/functional-config.test.js +++ b/test/build/config/function/functional-config.test.js @@ -14,7 +14,19 @@ describe("functional config", () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toContain("./src/index.js"); - expect(existsSync(resolve(__dirname, "./dist/dist-single.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/single.js"))).toBeTruthy(); + }); + + it("should build and not throw error with async single configuration", async () => { + const { stderr, stdout, exitCode } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-single-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/async-single.js"))).toBeTruthy(); }); it("should build and not throw errors with multiple configurations", async () => { @@ -27,7 +39,73 @@ describe("functional config", () => { expect(stderr).toBeFalsy(); expect(stdout).toContain("first"); expect(stdout).toContain("second"); - expect(existsSync(resolve(__dirname, "./dist/dist-first.js"))).toBeTruthy(); - expect(existsSync(resolve(__dirname, "./dist/dist-second.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-second.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with async multiple configurations", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-multi-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("first"); + expect(stdout).toContain("second"); + expect(existsSync(resolve(__dirname, "./dist/multi-async-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/multi-async-second.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with promise configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "promise.webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/promise-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with env configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "env.config.js"), + "--env=name=env", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/env-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with async env configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "async-env.config.js"), + "--env=name=env", + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("./src/index.js"); + expect(existsSync(resolve(__dirname, "./dist/async-env-single.js"))).toBeTruthy(); + }); + + it("should build and not throw errors with function each multiple configurations", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "--config", + resolve(__dirname, "function-each-multi-webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain("first"); + expect(stdout).toContain("second"); + expect(existsSync(resolve(__dirname, "./dist/function-each-first.js"))).toBeTruthy(); + expect(existsSync(resolve(__dirname, "./dist/function-each-second.js"))).toBeTruthy(); }); }); diff --git a/test/build/config/function/multi-webpack.config.js b/test/build/config/function/multi-webpack.config.js index f82f79ada0a..73de2b98411 100644 --- a/test/build/config/function/multi-webpack.config.js +++ b/test/build/config/function/multi-webpack.config.js @@ -1,7 +1,7 @@ module.exports = () => [ { output: { - filename: "./dist-first.js", + filename: "./multi-first.js", }, name: "first", entry: "./src/first.js", @@ -10,7 +10,7 @@ module.exports = () => [ }, { output: { - filename: "./dist-second.js", + filename: "./multi-second.js", }, name: "second", entry: "./src/second.js", diff --git a/test/build/config/function/promise.webpack.config.js b/test/build/config/function/promise.webpack.config.js new file mode 100644 index 00000000000..4f55e4152ab --- /dev/null +++ b/test/build/config/function/promise.webpack.config.js @@ -0,0 +1,9 @@ +module.exports = new Promise((resolve) => { + resolve({ + output: { + filename: "./promise-single.js", + }, + name: "promise-single", + mode: "development", + }); +}); diff --git a/test/build/config/function/single-webpack.config.js b/test/build/config/function/single-webpack.config.js index 0c93cbb0193..4df8fa9ad26 100644 --- a/test/build/config/function/single-webpack.config.js +++ b/test/build/config/function/single-webpack.config.js @@ -1,6 +1,6 @@ module.exports = () => ({ output: { - filename: "./dist-single.js", + filename: "./single.js", }, name: "single", mode: "development", diff --git a/test/build/config/top-multi-compilers-options/index.js b/test/build/config/multi-compiler-options/index.js similarity index 100% rename from test/build/config/top-multi-compilers-options/index.js rename to test/build/config/multi-compiler-options/index.js diff --git a/test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js b/test/build/config/multi-compiler-options/multi-compiler-options.test.js similarity index 100% rename from test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js rename to test/build/config/multi-compiler-options/multi-compiler-options.test.js diff --git a/test/build/config/top-multi-compilers-options/webpack.config.js b/test/build/config/multi-compiler-options/webpack.config.js similarity index 100% rename from test/build/config/top-multi-compilers-options/webpack.config.js rename to test/build/config/multi-compiler-options/webpack.config.js diff --git a/test/build/config/multi-compiler/a.js b/test/build/config/multi-compiler/a.js new file mode 100644 index 00000000000..0e9a8dc5145 --- /dev/null +++ b/test/build/config/multi-compiler/a.js @@ -0,0 +1 @@ +module.exports = "a.js"; diff --git a/test/build/config/multi-compiler/b.js b/test/build/config/multi-compiler/b.js new file mode 100644 index 00000000000..e3ec26f3f74 --- /dev/null +++ b/test/build/config/multi-compiler/b.js @@ -0,0 +1 @@ +module.exports = "b.js"; diff --git a/test/build/config/multi-compiler/multi-compiler.test.js b/test/build/config/multi-compiler/multi-compiler.test.js new file mode 100644 index 00000000000..88e1d57fe54 --- /dev/null +++ b/test/build/config/multi-compiler/multi-compiler.test.js @@ -0,0 +1,16 @@ +"use strict"; + +const { resolve } = require("node:path"); +const { run } = require("../../../utils/test-utils"); + +describe("basic config file", () => { + it("should build and not throw error with a basic configuration file", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + resolve(__dirname, "webpack.config.js"), + ]); + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); +}); diff --git a/test/build/config/multi-compiler/webpack.config.js b/test/build/config/multi-compiler/webpack.config.js new file mode 100644 index 00000000000..cc56b56a848 --- /dev/null +++ b/test/build/config/multi-compiler/webpack.config.js @@ -0,0 +1,18 @@ +const { resolve } = require("node:path"); + +module.exports = [ + { + entry: "./a.js", + output: { + path: resolve(__dirname, "binary"), + filename: "a.bundle.js", + }, + }, + { + entry: "./b.js", + output: { + path: resolve(__dirname, "binary"), + filename: "b.bundle.js", + }, + }, +]; diff --git a/test/build/custom-webpack/custom-webpack.test.js b/test/build/custom-webpack/custom-webpack.test.js index a8764c4c1f8..94b2c54486e 100644 --- a/test/build/custom-webpack/custom-webpack.test.js +++ b/test/build/custom-webpack/custom-webpack.test.js @@ -19,9 +19,6 @@ describe("custom-webpack", () => { env: { WEBPACK_PACKAGE: resolve(__dirname, "./custom-webpack.js") }, }); - console.log(stderr); - console.log(stdout); - expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); expect(stdout).toContain("main.js"); diff --git a/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 b/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 index ecc4b892b59..157da2650f0 100644 --- a/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 +++ b/test/build/stats/flags/__snapshots__/stats.test.js.snap.webpack5 @@ -4,7 +4,7 @@ exports[`stats flag should log error when an unknown flag stats value is passed: "[webpack-cli] Invalid value 'foo' for the '--stats' option [webpack-cli] Expected: 'none | summary | errors-only | errors-warnings | minimal | normal | detailed | verbose' [webpack-cli] Invalid value 'foo' for the '--stats' option -[webpack-cli] Expected: 'true | false'" +[webpack-cli] Expected: without value or negative option" `; exports[`stats flag should log error when an unknown flag stats value is passed: stdout 1`] = `""`; diff --git a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 index 9b5adec60c9..17e22931ffc 100644 --- a/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 +++ b/test/help/__snapshots__/help.test.js.snap.devServer5.webpack5 @@ -2469,6 +2469,20 @@ CLI documentation: https://webpack.js.org/api/cli/. Made with ♥ by the webpack team." `; +exports[`help should show help information using the "help --cache-type" option when using serve: stderr 1`] = `""`; + +exports[`help should show help information using the "help --cache-type" option when using serve: stdout 1`] = ` +"Usage: webpack serve --cache-type +Description: In memory caching. Filesystem caching. +Possible values: 'memory' | 'filesystem' + +To see list of all supported commands and options run 'webpack --help=verbose'. + +Webpack documentation: https://webpack.js.org/. +CLI documentation: https://webpack.js.org/api/cli/. +Made with ♥ by the webpack team." +`; + exports[`help should show help information using the "help --cache-type" option: stderr 1`] = `""`; exports[`help should show help information using the "help --cache-type" option: stdout 1`] = ` @@ -2550,6 +2564,20 @@ CLI documentation: https://webpack.js.org/api/cli/. Made with ♥ by the webpack team." `; +exports[`help should show help information using the "help --server-type" option when using serve: stderr 1`] = `""`; + +exports[`help should show help information using the "help --server-type" option when using serve: stdout 1`] = ` +"Usage: webpack serve --server-type +Description: Allows to set server and options (by default 'http'). +Possible values: 'http' | 'https' | 'spdy' | 'http2' + +To see list of all supported commands and options run 'webpack --help=verbose'. + +Webpack documentation: https://webpack.js.org/. +CLI documentation: https://webpack.js.org/api/cli/. +Made with ♥ by the webpack team." +`; + exports[`help should show help information using the "help --stats" option: stderr 1`] = `""`; exports[`help should show help information using the "help --stats" option: stdout 1`] = ` diff --git a/test/help/help.test.js b/test/help/help.test.js index b8d844a7c1d..56a32fe918e 100644 --- a/test/help/help.test.js +++ b/test/help/help.test.js @@ -245,6 +245,22 @@ describe("help", () => { expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); }); + it('should show help information using the "help --cache-type" option when using serve', async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["help", "serve", "--cache-type"]); + + expect(exitCode).toBe(0); + expect(normalizeStderr(stderr)).toMatchSnapshot("stderr"); + expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); + }); + + it('should show help information using the "help --server-type" option when using serve', async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ["help", "serve", "--server-type"]); + + expect(exitCode).toBe(0); + expect(normalizeStderr(stderr)).toMatchSnapshot("stderr"); + expect(normalizeStdout(stdout)).toMatchSnapshot("stdout"); + }); + it('should show help information using the "help --output-chunk-format" option', async () => { const { exitCode, stderr, stdout } = await run(__dirname, ["help", "--output-chunk-format"]);