From 82b1fb7441f04595ac90626235d506f29e5bb107 Mon Sep 17 00:00:00 2001 From: Alexander Akait <4567934+alexander-akait@users.noreply.github.com> Date: Thu, 5 Aug 2021 19:57:37 +0300 Subject: [PATCH] fix: support top multi compiler options (#2874) --- packages/webpack-cli/lib/webpack-cli.js | 227 ++++++++++-------- .../top-multi-compilers-options/index.js | 6 + .../top-multi-compilers-options.test.js | 35 +++ .../webpack.config.js | 43 ++++ 4 files changed, 207 insertions(+), 104 deletions(-) create mode 100644 test/build/config/top-multi-compilers-options/index.js create mode 100644 test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js create mode 100644 test/build/config/top-multi-compilers-options/webpack.config.js diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index 14d2294638e..168d75d45da 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -847,7 +847,7 @@ class WebpackCLI { options.entry = [...entries, ...(options.entry || [])]; } - await this.buildCommand(options, isWatchCommandUsed); + await this.runWebpack(options, isWatchCommandUsed); }, ); } else if (isCommand(commandName, helpCommandOptions)) { @@ -1521,107 +1521,124 @@ class WebpackCLI { await this.program.parseAsync(args, parseOptions); } - async resolveConfig(options) { - const loadConfig = async (configPath) => { - const { interpret } = this.utils; - const ext = path.extname(configPath); - const interpreted = Object.keys(interpret.jsVariants).find( - (variant) => variant === ext, - ); + async loadConfig(configPath, argv = {}) { + const { interpret } = this.utils; + const ext = path.extname(configPath); + const interpreted = Object.keys(interpret.jsVariants).find((variant) => variant === ext); - if (interpreted) { - const { rechoir } = this.utils; - - try { - rechoir.prepare(interpret.extensions, configPath); - } catch (error) { - if (error.failures) { - this.logger.error(`Unable load '${configPath}'`); - this.logger.error(error.message); - - error.failures.forEach((failure) => { - this.logger.error(failure.error.message); - }); - this.logger.error("Please install one of them"); - process.exit(2); - } - - this.logger.error(error); - process.exit(2); - } - } - - let options; + if (interpreted) { + const { rechoir } = this.utils; try { - options = await this.tryRequireThenImport(configPath, false); + rechoir.prepare(interpret.extensions, configPath); } catch (error) { - this.logger.error(`Failed to load '${configPath}' config`); - - if (this.isValidationError(error)) { + if (error.failures) { + this.logger.error(`Unable load '${configPath}'`); this.logger.error(error.message); - } else { - this.logger.error(error); + + error.failures.forEach((failure) => { + this.logger.error(failure.error.message); + }); + this.logger.error("Please install one of them"); + process.exit(2); } + this.logger.error(error); process.exit(2); } + } - return { options, path: configPath }; - }; + let options; - const evaluateConfig = async (loadedConfig, argv) => { - const isMultiCompiler = Array.isArray(loadedConfig.options); - const config = isMultiCompiler ? loadedConfig.options : [loadedConfig.options]; + try { + options = await this.tryRequireThenImport(configPath, false); + } catch (error) { + this.logger.error(`Failed to load '${configPath}' config`); + + if (this.isValidationError(error)) { + this.logger.error(error.message); + } else { + this.logger.error(error); + } - const evaluatedConfig = await Promise.all( - config.map(async (rawConfig) => { - if (typeof rawConfig.then === "function") { - rawConfig = await rawConfig; + process.exit(2); + } + + if (Array.isArray(options)) { + await Promise.all( + options.map(async (_, i) => { + if (typeof options[i].then === "function") { + options[i] = await options[i]; } // `Promise` may return `Function` - if (typeof rawConfig === "function") { + if (typeof options[i] === "function") { // when config is a function, pass the env from args to the config function - rawConfig = await rawConfig(argv.env, argv); + options[i] = await options[i](argv.env, argv); } - - return rawConfig; }), ); + } else { + if (typeof options.then === "function") { + options = await options; + } - loadedConfig.options = isMultiCompiler ? evaluatedConfig : evaluatedConfig[0]; + // `Promise` may return `Function` + if (typeof options === "function") { + // when config is a function, pass the env from args to the config function + options = await options(argv.env, argv); + } + } - const isObject = (value) => typeof value === "object" && value !== null; + const isObject = (value) => typeof value === "object" && value !== null; - if (!isObject(loadedConfig.options) && !Array.isArray(loadedConfig.options)) { - this.logger.error(`Invalid configuration in '${loadedConfig.path}'`); - process.exit(2); - } + if (!isObject(options) && !Array.isArray(options)) { + this.logger.error(`Invalid configuration in '${configPath}'`); - return loadedConfig; - }; + process.exit(2); + } + return { options, path: configPath }; + } + + async resolveConfig(options) { const config = { options: {}, path: new WeakMap() }; if (options.config && options.config.length > 0) { - const evaluatedConfigs = await Promise.all( - options.config.map(async (value) => - evaluateConfig(await loadConfig(path.resolve(value)), options.argv || {}), + const loadedConfigs = await Promise.all( + options.config.map((configPath) => + this.loadConfig(path.resolve(configPath), options.argv), ), ); config.options = []; - evaluatedConfigs.forEach((evaluatedConfig) => { - if (Array.isArray(evaluatedConfig.options)) { - evaluatedConfig.options.forEach((options) => { - config.options.push(options); - config.path.set(options, evaluatedConfig.path); + loadedConfigs.forEach((loadedConfig) => { + const isArray = Array.isArray(loadedConfig.options); + + // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release + if (config.options.length === 0) { + config.options = loadedConfig.options; + } else { + if (!Array.isArray(config.options)) { + config.options = [config.options]; + } + + if (isArray) { + loadedConfig.options.forEach((item) => { + config.options.push(item); + }); + } else { + config.options.push(loadedConfig.options); + } + } + + if (isArray) { + loadedConfig.options.forEach((options) => { + config.path.set(options, loadedConfig.path); }); } else { - config.options.push(evaluatedConfig.options); - config.path.set(evaluatedConfig.options, evaluatedConfig.path); + config.path.set(loadedConfig.options, loadedConfig.path); } }); @@ -1657,23 +1674,25 @@ class WebpackCLI { } if (foundDefaultConfigFile) { - const loadedConfig = await loadConfig(foundDefaultConfigFile.path); - const evaluatedConfig = await evaluateConfig(loadedConfig, options.argv || {}); + const loadedConfig = await this.loadConfig( + foundDefaultConfigFile.path, + options.argv, + ); - config.options = evaluatedConfig.options; + config.options = loadedConfig.options; if (Array.isArray(config.options)) { - config.options.forEach((options) => { - config.path.set(options, evaluatedConfig.path); + config.options.forEach((item) => { + config.path.set(item, loadedConfig.path); }); } else { - config.path.set(evaluatedConfig.options, evaluatedConfig.path); + config.path.set(loadedConfig.options, loadedConfig.path); } } } if (options.configName) { - const notfoundConfigNames = []; + const notFoundConfigNames = []; config.options = options.configName.map((configName) => { let found; @@ -1685,15 +1704,15 @@ class WebpackCLI { } if (!found) { - notfoundConfigNames.push(configName); + notFoundConfigNames.push(configName); } return found; }); - if (notfoundConfigNames.length > 0) { + if (notFoundConfigNames.length > 0) { this.logger.error( - notfoundConfigNames + notFoundConfigNames .map( (configName) => `Configuration with the name "${configName}" was not found.`, @@ -1731,6 +1750,18 @@ class WebpackCLI { return config; } + runFunctionOnOptions(options, fn) { + if (Array.isArray(options)) { + for (let item of options) { + item = fn(item); + } + } else { + options = fn(options); + } + + return options; + } + // TODO refactor async applyOptions(config, options) { if (options.analyze) { @@ -1786,9 +1817,7 @@ class WebpackCLI { return configOptions; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => outputHints(options)) - : outputHints(config.options); + this.runFunctionOnOptions(config.options, outputHints); if (this.webpack.cli) { const processArguments = (configOptions) => { @@ -1850,9 +1879,7 @@ class WebpackCLI { return configOptions; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => processArguments(options)) - : processArguments(config.options); + this.runFunctionOnOptions(config.options, processArguments); const setupDefaultOptions = (configOptions) => { // No need to run for webpack@4 @@ -1881,9 +1908,7 @@ class WebpackCLI { return configOptions; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => setupDefaultOptions(options)) - : setupDefaultOptions(config.options); + this.runFunctionOnOptions(config.options, setupDefaultOptions); } // Logic for webpack@4 @@ -1943,12 +1968,10 @@ class WebpackCLI { return configOptions; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => processLegacyArguments(options)) - : processLegacyArguments(config.options); + this.runFunctionOnOptions(config.options, processLegacyArguments); // Apply `stats` and `stats.colors` options - const applyStatsColors = (configOptions) => { + const applyStatsOption = (configOptions) => { // TODO remove after drop webpack@4 const statsForWebpack4 = this.webpack.Stats && this.webpack.Stats.presetToOptions; @@ -2005,9 +2028,7 @@ class WebpackCLI { return configOptions; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => applyStatsColors(options)) - : applyStatsColors(config.options); + this.runFunctionOnOptions(config.options, applyStatsOption); return config; } @@ -2015,14 +2036,14 @@ class WebpackCLI { async applyCLIPlugin(config, cliOptions) { const CLIPlugin = await this.tryRequireThenImport("./plugins/CLIPlugin"); - const addCLIPlugin = (configOptions) => { - if (!configOptions.plugins) { - configOptions.plugins = []; + const addCLIPlugin = (options) => { + if (!options.plugins) { + options.plugins = []; } - configOptions.plugins.unshift( + options.plugins.unshift( new CLIPlugin({ - configPath: config.path.get(configOptions), + configPath: config.path.get(options), helpfulOutput: !cliOptions.json, hot: cliOptions.hot, progress: cliOptions.progress, @@ -2031,12 +2052,10 @@ class WebpackCLI { }), ); - return configOptions; + return options; }; - config.options = Array.isArray(config.options) - ? config.options.map((options) => addCLIPlugin(options)) - : addCLIPlugin(config.options); + this.runFunctionOnOptions(config.options, addCLIPlugin); return config; } @@ -2102,7 +2121,7 @@ class WebpackCLI { return compiler; } - async buildCommand(options, isWatchCommand) { + async runWebpack(options, isWatchCommand) { // eslint-disable-next-line prefer-const let compiler; let createJsonStringifyStream; diff --git a/test/build/config/top-multi-compilers-options/index.js b/test/build/config/top-multi-compilers-options/index.js new file mode 100644 index 00000000000..97f0cb45e3a --- /dev/null +++ b/test/build/config/top-multi-compilers-options/index.js @@ -0,0 +1,6 @@ +console.log(`test ${Math.random()}`); +console.log(`test ${Math.random()}`); +console.log(`test ${Math.random()}`); +console.log(`test ${Math.random()}`); +console.log(`test ${Math.random()}`); +console.log(`test ${Math.random()}`); diff --git a/test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js b/test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js new file mode 100644 index 00000000000..607a82e3e3c --- /dev/null +++ b/test/build/config/top-multi-compilers-options/top-multi-compilers-options.test.js @@ -0,0 +1,35 @@ +"use strict"; + +const { resolve } = require("path"); +const { run, isWebpack5 } = require("../../../utils/test-utils"); + +describe("top multi compiler options", () => { + it("should work without provided configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + if (isWebpack5) { + expect(stdout).toContain("Done build0\nDone build1\nDone build2\nDone build3"); + } else { + expect(stdout).toBeTruthy(); + } + }); + + it("should work with provided configuration", async () => { + const { exitCode, stderr, stdout } = await run(__dirname, [ + "-c", + resolve(__dirname, "webpack.config.js"), + ]); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + + if (isWebpack5) { + expect(stdout).toContain("Done build0\nDone build1\nDone build2\nDone build3"); + } else { + expect(stdout).toBeTruthy(); + } + }); +}); diff --git a/test/build/config/top-multi-compilers-options/webpack.config.js b/test/build/config/top-multi-compilers-options/webpack.config.js new file mode 100644 index 00000000000..42189f57083 --- /dev/null +++ b/test/build/config/top-multi-compilers-options/webpack.config.js @@ -0,0 +1,43 @@ +class SimpleProgressWebpackPlugin { + constructor(options) { + this.options = options; + } + + apply(compiler) { + compiler.hooks.done.tap("test", () => { + console.log("Done", this.options.name); + }); + } +} + +const configs = []; + +for (let i = 0; i < 3; i++) { + configs.push({ + mode: "development", + name: `build${i}`, + entry: "./index.js", + plugins: [ + new SimpleProgressWebpackPlugin({ + name: `build${i}`, + }), + ], + }); +} + +configs.push(async () => { + return { + mode: "development", + name: `build${3}`, + entry: "./index.js", + plugins: [ + new SimpleProgressWebpackPlugin({ + name: `build${3}`, + format: "simple", + }), + ], + }; +}); + +module.exports = configs; +module.exports.parallelism = 1;