diff --git a/packages/generators/src/index.ts b/packages/generators/src/index.ts index d423d9caee6..d4f0a93d18a 100644 --- a/packages/generators/src/index.ts +++ b/packages/generators/src/index.ts @@ -5,10 +5,10 @@ import addonGenerator from './addon-generator'; import initGenerator from './init-generator'; class GeneratorsCommand { - apply(cli): void { + async apply(cli): Promise { const { logger } = cli; - cli.makeCommand( + await cli.makeCommand( { name: 'loader [output-path]', alias: 'l', diff --git a/packages/info/src/index.ts b/packages/info/src/index.ts index d61d2b67303..83b3884d7c1 100644 --- a/packages/info/src/index.ts +++ b/packages/info/src/index.ts @@ -30,8 +30,8 @@ const DEFAULT_DETAILS: Information = { }; class InfoCommand { - apply(cli): void { - cli.makeCommand( + async apply(cli): Promise { + await cli.makeCommand( { name: 'info', alias: 'i', diff --git a/packages/init/src/index.ts b/packages/init/src/index.ts index 1de1b7e0331..e0ddd3b209b 100644 --- a/packages/init/src/index.ts +++ b/packages/init/src/index.ts @@ -2,8 +2,8 @@ import { initGenerator } from '@webpack-cli/generators'; import { modifyHelperUtil, npmPackagesExists } from '@webpack-cli/utils'; class InitCommand { - apply(cli): void { - cli.makeCommand( + async apply(cli): Promise { + await cli.makeCommand( { name: 'init [scaffold...]', alias: 'c', diff --git a/packages/migrate/src/index.ts b/packages/migrate/src/index.ts index c9f458a0877..3e7789a5e6c 100644 --- a/packages/migrate/src/index.ts +++ b/packages/migrate/src/index.ts @@ -149,10 +149,10 @@ function runMigration(currentConfigPath: string, outputConfigPath: string, logge } class MigrationCommand { - apply(cli): void { + async apply(cli): Promise { const { logger } = cli; - cli.makeCommand( + await cli.makeCommand( { name: 'migrate [new-config-path]', alias: 'm', diff --git a/packages/serve/src/index.ts b/packages/serve/src/index.ts index 0987e357b90..240031b69dc 100644 --- a/packages/serve/src/index.ts +++ b/packages/serve/src/index.ts @@ -2,45 +2,43 @@ import startDevServer from './startDevServer'; class ServeCommand { async apply(cli): Promise { - const { logger, utils } = cli; - const isPackageExist = utils.getPkg('webpack-dev-server'); - - if (!isPackageExist) { - try { - await utils.promptInstallation('webpack-dev-server', () => { - // TODO colors - logger.error("For using this command you need to install: 'webpack-dev-server' package"); - }); - } catch (error) { - logger.error("Action Interrupted, use 'webpack-cli help' to see possible commands."); - process.exit(2); - } - } - - let devServerFlags = []; - - try { - // eslint-disable-next-line node/no-extraneous-require - require('webpack-dev-server'); - // eslint-disable-next-line node/no-extraneous-require - devServerFlags = require('webpack-dev-server/bin/cli-flags').devServer; - } catch (err) { - logger.error(`You need to install 'webpack-dev-server' for running 'webpack serve'.\n${err}`); - process.exit(2); - } - - const builtInOptions = cli.getBuiltInOptions(); - - cli.makeCommand( + const { logger } = cli; + + await cli.makeCommand( { name: 'serve', alias: 's', description: 'Run the webpack dev server.', usage: '[options]', pkg: '@webpack-cli/serve', + dependencies: ['webpack-dev-server'], + }, + () => { + let devServerFlags = []; + + try { + // eslint-disable-next-line + devServerFlags = require('webpack-dev-server/bin/cli-flags').devServer; + } catch (error) { + logger.error(`You need to install 'webpack-dev-server' for running 'webpack serve'.\n${error}`); + process.exit(2); + } + + const builtInOptions = cli.getBuiltInOptions(); + + return [...builtInOptions, ...devServerFlags]; }, - [...builtInOptions, ...devServerFlags], async (program) => { + const builtInOptions = cli.getBuiltInOptions(); + let devServerFlags = []; + + try { + // eslint-disable-next-line + devServerFlags = require('webpack-dev-server/bin/cli-flags').devServer; + } catch (error) { + // Nothing, to prevent future updates + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const webpackOptions: Record = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -89,7 +87,7 @@ class ServeCommand { let servers; if (cli.needWatchStdin(compiler) || devServerOptions.stdin) { - // TODO + // TODO remove in the next major release // Compatibility with old `stdin` option for `webpack-dev-server` // Should be removed for the next major release on both sides if (devServerOptions.stdin) { diff --git a/packages/webpack-cli/lib/utils/prompt-installation.js b/packages/webpack-cli/lib/utils/prompt-installation.js index 5f77893cb95..d025d80e6ff 100644 --- a/packages/webpack-cli/lib/utils/prompt-installation.js +++ b/packages/webpack-cli/lib/utils/prompt-installation.js @@ -18,15 +18,13 @@ async function promptInstallation(packageName, preMessage) { process.exit(2); } - // yarn uses 'add' command, rest npm and pnpm both use 'install' - const options = [packageManager === 'yarn' ? 'add' : 'install', '-D', packageName]; - - const commandToBeRun = `${packageManager} ${options.join(' ')}`; - if (preMessage) { preMessage(); } + // yarn uses 'add' command, rest npm and pnpm both use 'install' + const commandToBeRun = `${packageManager} ${[packageManager === 'yarn' ? 'add' : 'install', '-D', packageName].join(' ')}`; + let installConfirm; try { @@ -34,7 +32,7 @@ async function promptInstallation(packageName, preMessage) { { type: 'confirm', name: 'installConfirm', - message: `Would you like to install '${packageName}' package? (That will run '${green(commandToBeRun)}')`, + message: `Would you like to install '${green(packageName)}' package? (That will run '${green(commandToBeRun)}')`, initial: 'Y', stdout: process.stderr, }, diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index b4ede2b8d62..3a7f744ee81 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -29,8 +29,8 @@ class WebpackCLI { this.utils = { toKebabCase, getPkg, promptInstallation }; } - makeCommand(commandOptions, optionsForCommand = [], action) { - const command = program.command(commandOptions.name, { + async makeCommand(commandOptions, options, action) { + const command = this.program.command(commandOptions.name, { noHelp: commandOptions.noHelp, hidden: commandOptions.hidden, isDefault: commandOptions.isDefault, @@ -56,8 +56,50 @@ class WebpackCLI { command.pkg = 'webpack-cli'; } - if (optionsForCommand.length > 0) { - optionsForCommand.forEach((optionForCommand) => { + const { forHelp } = this.program; + + let allDependenciesInstalled = true; + + if (commandOptions.dependencies && commandOptions.dependencies.length > 0) { + for (const dependency of commandOptions.dependencies) { + const isPkgExist = getPkg(dependency); + + if (isPkgExist) { + continue; + } else if (!isPkgExist && forHelp) { + allDependenciesInstalled = false; + continue; + } + + try { + await promptInstallation(dependency, () => { + logger.error( + `For using '${green(commandOptions.name)}' command you need to install: '${green(dependency)}' package`, + ); + }); + } catch (error) { + logger.error("Action Interrupted, use 'webpack-cli help' to see possible commands."); + logger.error(error); + process.exit(2); + } + } + } + + if (options) { + if (typeof options === 'function') { + if (forHelp && !allDependenciesInstalled) { + command.description( + `${commandOptions.description} To see all available options you need to install ${commandOptions.dependencies + .map((dependency) => `'${dependency}'`) + .join(',')}.`, + ); + options = []; + } else { + options = options(); + } + } + + options.forEach((optionForCommand) => { this.makeOption(command, optionForCommand); }); } @@ -271,29 +313,11 @@ class WebpackCLI { await this.bundleCommand(options); }); } else if (commandName === helpCommandOptions.name || commandName === helpCommandOptions.alias) { - this.makeCommand( - { - name: 'help [command]', - alias: 'h', - description: 'Display help for commands and options', - usage: '[command]', - }, - [], - // Stub for the `help` command - () => {}, - ); + // Stub for the `help` command + this.makeCommand(helpCommandOptions, [], () => {}); } else if (commandName === versionCommandOptions.name || commandName === helpCommandOptions.alias) { - this.makeCommand( - { - name: 'version [commands...]', - alias: 'v', - description: "Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands", - usage: '[commands...]', - }, - [], - // Stub for the `help` command - () => {}, - ); + // Stub for the `help` command + this.makeCommand(versionCommandOptions, [], () => {}); } else { const builtInExternalCommandInfo = externalBuiltInCommandsInfo.find( (externalBuiltInCommandInfo) => @@ -310,11 +334,7 @@ class WebpackCLI { if (pkg !== 'webpack-cli' && !getPkg(pkg)) { if (!allowToInstall) { - const isOptions = commandName.startsWith('-'); - - logger.error(`Unknown ${isOptions ? 'option' : 'command'} '${commandName}'`); - logger.error("Run 'webpack --help' to see available commands and options"); - process.exit(2); + return; } try { @@ -464,6 +484,12 @@ class WebpackCLI { (command) => command.name() === possibleCommandName || command.alias() === possibleCommandName, ); + if (!foundCommand) { + logger.error(`Unknown command '${possibleCommandName}'`); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + try { const { name, version } = require(`${foundCommand.pkg}/package.json`); @@ -481,7 +507,7 @@ class WebpackCLI { logger.raw(`webpack-cli ${pkgJSON.version}`); if (getPkg('webpack-dev-server')) { - // eslint-disable-next-line node/no-extraneous-require + // eslint-disable-next-line const { version } = require('webpack-dev-server/package.json'); logger.raw(`webpack-dev-server ${version}`); @@ -547,6 +573,12 @@ class WebpackCLI { } else { const [name, ...optionsWithoutCommandName] = options; + if (name.startsWith('-')) { + logger.error(`Unknown option '${name}'`); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + optionsWithoutCommandName.forEach((option) => { logger.error(`Unknown option '${option}'`); logger.error("Run 'webpack --help' to see available commands and options"); @@ -636,6 +668,8 @@ class WebpackCLI { } } + this.program.forHelp = true; + const optionsForHelp = [].concat(opts.help && !isDefault ? [commandName] : []).concat(options); await outputHelp(optionsForHelp, isVerbose, program); diff --git a/test/help/help.test.js b/test/help/help.test.js index 35c4c568acb..d446a20947c 100644 --- a/test/help/help.test.js +++ b/test/help/help.test.js @@ -219,7 +219,7 @@ describe('help', () => { const { exitCode, stderr, stdout } = run(__dirname, ['help', 'myCommand'], false); expect(exitCode).toBe(2); - expect(stderr).toContain("Unknown command 'myCommand'"); + expect(stderr).toContain("Can't find and load command 'myCommand'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); expect(stdout).toBeFalsy(); }); @@ -228,7 +228,7 @@ describe('help', () => { const { exitCode, stderr, stdout } = run(__dirname, ['help', 'verbose'], false); expect(exitCode).toBe(2); - expect(stderr).toContain("Unknown command 'verbose'"); + expect(stderr).toContain("Can't find and load command 'verbose'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); expect(stdout).toBeFalsy(); }); diff --git a/test/optimization/optimization.test.js b/test/optimization/optimization.test.js deleted file mode 100644 index ea28769da3a..00000000000 --- a/test/optimization/optimization.test.js +++ /dev/null @@ -1,18 +0,0 @@ -const { run, isWebpack5 } = require('../utils/test-utils'); - -describe('optimization option in config', () => { - it('should work with mangleExports disabled', () => { - const { exitCode, stderr, stdout } = run(__dirname, [], false); - - // Should throw when webpack is less than 5 - if (isWebpack5) { - expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); - expect(stdout).toContain('mangleExports: false'); - } else { - expect(exitCode).toBe(2); - expect(stderr).toContain("configuration.optimization has an unknown property 'mangleExports'"); - expect(stdout).toBeFalsy(); - } - }); -}); diff --git a/test/optimization/src/index.js b/test/optimization/src/index.js deleted file mode 100644 index c56f17c89e9..00000000000 --- a/test/optimization/src/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Bokuto") diff --git a/test/optimization/webpack.config.js b/test/optimization/webpack.config.js deleted file mode 100644 index c5ddd675368..00000000000 --- a/test/optimization/webpack.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const WebpackCLITestPlugin = require('../utils/webpack-cli-test-plugin'); - -module.exports = { - plugins: [new WebpackCLITestPlugin()], - optimization: { - mangleExports: false, - }, -}; diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 52d4af7bf56..8515dbced2b 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -8,12 +8,22 @@ const { Writable } = require('readable-stream'); const concat = require('concat-stream'); const { version } = require('webpack'); const stripAnsi = require('strip-ansi'); -const { version: devServerVersion } = require('webpack-dev-server/package.json'); + +const isWebpack5 = version.startsWith('5'); + +let devServerVersion; + +try { + // eslint-disable-next-line + devServerVersion = require('webpack-dev-server/package.json').version; +} catch (error) { + // Nothing +} + +const isDevServer4 = devServerVersion && devServerVersion.startsWith('4'); const WEBPACK_PATH = path.resolve(__dirname, '../../packages/webpack-cli/bin/cli.js'); const ENABLE_LOG_COMPILATION = process.env.ENABLE_PIPE || false; -const isWebpack5 = version.startsWith('5'); -const isDevServer4 = devServerVersion.startsWith('4'); const isWindows = process.platform === 'win32'; const hyphenToUpperCase = (name) => { diff --git a/test/version/version.test.js b/test/version/version.test.js index b9a953703d4..ac2b3deb1ca 100644 --- a/test/version/version.test.js +++ b/test/version/version.test.js @@ -171,13 +171,13 @@ describe('single version flag', () => { expect(stdout).toBeFalsy(); }); - it('should log error when command using command syntax with multi commands', () => { + it('should log version for known command and log error for unknown command using command syntax with multi commands', () => { const { exitCode, stderr, stdout } = run(__dirname, ['version', 'info', 'unknown'], false); expect(exitCode).toBe(2); expect(stderr).toContain("Unknown command 'unknown'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); - expect(stdout).toBeFalsy(); + expect(stdout).toContain(`@webpack-cli/info ${infoPkgJSON.version}`); }); it('should work for multiple commands', () => { @@ -214,22 +214,22 @@ describe('single version flag', () => { expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`); }); - it('should log error when unknown command used with --version flag', () => { + it('should log version for known command and log error for unknown command using the "--version" option', () => { const { exitCode, stderr, stdout } = run(__dirname, ['init', 'abc', '--version'], false); expect(exitCode).toBe(2); expect(stderr).toContain("Unknown command 'abc'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); - expect(stdout).toBeFalsy(); + expect(stdout).toContain(`@webpack-cli/init ${initPkgJSON.version}`); }); - it('should log error when unknown command used with -v alias', () => { + it('should log version for known command and log error for unknown command using the "-v" option', () => { const { exitCode, stderr, stdout } = run(__dirname, ['init', 'abc', '-v'], false); expect(exitCode).toBe(2); expect(stderr).toContain("Unknown command 'abc'"); expect(stderr).toContain("Run 'webpack --help' to see available commands and options"); - expect(stdout).toBeFalsy(); + expect(stdout).toContain(`@webpack-cli/init ${initPkgJSON.version}`); }); it('should not output version with help dashed', () => {