diff --git a/OPTIONS.md b/OPTIONS.md index 797fe3b1ce6..3fbe2a31dd4 100644 --- a/OPTIONS.md +++ b/OPTIONS.md @@ -5,6 +5,8 @@ Alternative usage: webpack build [options] Alternative usage: webpack bundle [options] Alternative usage: webpack b [options] Alternative usage: webpack build --config [options] +Alternative usage: webpack bundle --config [options] +Alternative usage: webpack b --config [options] The build tool for modern web applications. @@ -700,6 +702,7 @@ Global options: Commands: build|bundle|b [options] Run webpack (default command, can be omitted). + watch|w [options] Run webpack and watch for files changes. version|v [commands...] Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands. help|h [command] [option] Display help for commands and options. serve|s [options] Run the webpack dev server. @@ -707,7 +710,7 @@ Commands: init|c [options] [scaffold...] Initialize a new webpack configuration. loader|l [output-path] Scaffold a loader. migrate|m [new-config-path] Migrate a configuration to a new version. - configtest|t Tests webpack configuration against validation errors. + configtest|t [config-path] Tests webpack configuration against validation errors. plugin|p [output-path] Scaffold a plugin. To see list of all supported commands and options run 'webpack --help=verbose'. diff --git a/packages/configtest/src/index.ts b/packages/configtest/src/index.ts index 2a03fad1683..f9b5b92304a 100644 --- a/packages/configtest/src/index.ts +++ b/packages/configtest/src/index.ts @@ -1,8 +1,6 @@ -import webpack from 'webpack'; - class ConfigTestCommand { async apply(cli): Promise { - const { logger } = cli; + const { logger, webpack } = cli; await cli.makeCommand( { diff --git a/packages/webpack-cli/lib/webpack-cli.js b/packages/webpack-cli/lib/webpack-cli.js index e640f1ca053..3e7427e2658 100644 --- a/packages/webpack-cli/lib/webpack-cli.js +++ b/packages/webpack-cli/lib/webpack-cli.js @@ -1,31 +1,28 @@ -const path = require('path'); const { program } = require('commander'); const getPkg = require('./utils/package-exists'); const webpack = getPkg('webpack') ? require('webpack') : undefined; +const path = require('path'); const { merge } = require('webpack-merge'); const { extensions, jsVariants } = require('interpret'); const rechoir = require('rechoir'); const { createWriteStream, existsSync } = require('fs'); const { distance } = require('fastest-levenshtein'); const { options: coloretteOptions, yellow, cyan, green, bold } = require('colorette'); -const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext'); const logger = require('./utils/logger'); const { cli, flags } = require('./utils/cli-flags'); const CLIPlugin = require('./plugins/CLIPlugin'); const promptInstallation = require('./utils/prompt-installation'); - const toKebabCase = require('./utils/to-kebab-case'); -const { resolve, extname } = path; - class WebpackCLI { constructor() { - this.logger = logger; // Initialize program this.program = program; this.program.name('webpack'); this.program.storeOptionsAsProperties(false); + this.webpack = webpack; + this.logger = logger; this.utils = { toKebabCase, getPkg, promptInstallation }; } @@ -234,6 +231,12 @@ class WebpackCLI { description: 'Run webpack (default command, can be omitted).', usage: '[options]', }; + const watchCommandOptions = { + name: 'watch', + alias: 'w', + description: 'Run webpack and watch for files changes.', + usage: '[options]', + }; const versionCommandOptions = { name: 'version [commands...]', alias: 'v', @@ -283,7 +286,13 @@ class WebpackCLI { }, ]; - const knownCommands = [buildCommandOptions, versionCommandOptions, helpCommandOptions, ...externalBuiltInCommandsInfo]; + const knownCommands = [ + buildCommandOptions, + watchCommandOptions, + versionCommandOptions, + helpCommandOptions, + ...externalBuiltInCommandsInfo, + ]; const getCommandName = (name) => name.split(' ')[0]; const isKnownCommand = (name) => knownCommands.find( @@ -294,6 +303,9 @@ class WebpackCLI { const isBuildCommand = (name) => getCommandName(buildCommandOptions.name) === name || (Array.isArray(buildCommandOptions.alias) ? buildCommandOptions.alias.includes(name) : buildCommandOptions.alias === name); + const isWatchCommand = (name) => + getCommandName(watchCommandOptions.name) === name || + (Array.isArray(watchCommandOptions.alias) ? watchCommandOptions.alias.includes(name) : watchCommandOptions.alias === name); const isHelpCommand = (name) => getCommandName(helpCommandOptions.name) === name || (Array.isArray(helpCommandOptions.alias) ? helpCommandOptions.alias.includes(name) : helpCommandOptions.alias === name); @@ -338,21 +350,40 @@ class WebpackCLI { return { commandName: isDefault ? buildCommandOptions.name : commandName, options, isDefault }; }; const loadCommandByName = async (commandName, allowToInstall = false) => { - if (isBuildCommand(commandName)) { - await this.makeCommand(buildCommandOptions, this.getBuiltInOptions(), async (program) => { - const options = program.opts(); + const isBuildCommandUsed = isBuildCommand(commandName); + const isWatchCommandUsed = isWatchCommand(commandName); - if (program.args.length > 0) { - const possibleCommands = [].concat([buildCommandOptions.name]).concat(program.args); + if (isBuildCommandUsed || isWatchCommandUsed) { + await this.makeCommand( + isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, + this.getBuiltInOptions(), + async (program) => { + const options = program.opts(); - logger.error('Running multiple commands at the same time is not possible'); - logger.error(`Found commands: ${possibleCommands.map((item) => `'${item}'`).join(', ')}`); - logger.error("Run 'webpack --help' to see available commands and options"); - process.exit(2); - } + if (program.args.length > 0) { + const possibleCommands = [].concat([buildCommandOptions.name]).concat(program.args); - await this.bundleCommand(options); - }); + logger.error('Running multiple commands at the same time is not possible'); + logger.error(`Found commands: ${possibleCommands.map((item) => `'${item}'`).join(', ')}`); + logger.error("Run 'webpack --help' to see available commands and options"); + process.exit(2); + } + + if (isWatchCommandUsed) { + if (typeof options.watch !== 'undefined') { + logger.warn( + `No need to use the ${ + options.watch ? "'--watch, -w'" : "'--no-watch'" + } option together with the 'watch' command, it does not make sense`, + ); + } + + options.watch = true; + } + + await this.bundleCommand(options); + }, + ); } else if (isHelpCommand(commandName)) { // Stub for the `help` command this.makeCommand(helpCommandOptions, [], () => {}); @@ -492,9 +523,9 @@ class WebpackCLI { // Make `-v, --version` options // Make `version|v [commands...]` command const outputVersion = async (options) => { - // Filter `bundle`, `version` and `help` commands + // Filter `bundle`, `watch`, `version` and `help` commands const possibleCommandNames = options.filter( - (option) => !isBuildCommand(option) && !isVersionCommand(option) && !isHelpCommand(option), + (option) => !isBuildCommand(option) && !isWatchCommand(option) && !isVersionCommand(option) && !isHelpCommand(option), ); possibleCommandNames.forEach((possibleCommandName) => { @@ -616,7 +647,7 @@ class WebpackCLI { .replace(buildCommandOptions.description, 'The build tool for modern web applications.') .replace( /Usage:.+/, - 'Usage: webpack [options]\nAlternative usage: webpack --config [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config [options]', + 'Usage: webpack [options]\nAlternative usage: webpack --config [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config [options]\nAlternative usage: webpack bundle --config [options]\nAlternative usage: webpack b --config [options]', ); logger.raw(helpInformation); @@ -643,25 +674,21 @@ class WebpackCLI { let helpInformation = command.helpInformation().trimRight(); if (isBuildCommand(name)) { - helpInformation = helpInformation - .replace(buildCommandOptions.description, 'The build tool for modern web applications.') - .replace( - /Usage:.+/, - 'Usage: webpack [options]\nAlternative usage: webpack --config [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config [options]', - ); + helpInformation = helpInformation.replace('build|bundle', 'build|bundle|b'); } logger.raw(helpInformation); outputGlobalOptions(); } else if (isHelpCommandSyntax) { - let commandName; + let isCommandSpecified = false; + let commandName = buildCommandOptions.name; let optionName; if (options.length === 1) { - commandName = buildCommandOptions.name; optionName = options[0]; } else if (options.length === 2) { + isCommandSpecified = true; commandName = options[0]; optionName = options[1]; @@ -694,14 +721,10 @@ class WebpackCLI { option.flags.replace(/^.+[[<]/, '').replace(/(\.\.\.)?[\]>].*$/, '') + (option.variadic === true ? '...' : ''); const value = option.required ? '<' + nameOutput + '>' : option.optional ? '[' + nameOutput + ']' : ''; - logger.raw( - `Usage: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.long}${value ? ` ${value}` : ''}`, - ); + logger.raw(`Usage: webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.long}${value ? ` ${value}` : ''}`); if (option.short) { - logger.raw( - `Short: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.short}${value ? ` ${value}` : ''}`, - ); + logger.raw(`Short: webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.short}${value ? ` ${value}` : ''}`); } if (option.description) { @@ -806,7 +829,7 @@ class WebpackCLI { async resolveConfig(options) { const loadConfig = async (configPath) => { - const ext = extname(configPath); + const ext = path.extname(configPath); const interpreted = Object.keys(jsVariants).find((variant) => variant === ext); if (interpreted) { @@ -906,7 +929,7 @@ class WebpackCLI { if (options.config && options.config.length > 0) { const evaluatedConfigs = await Promise.all( options.config.map(async (value) => { - const configPath = resolve(value); + const configPath = path.resolve(value); if (!existsSync(configPath)) { logger.error(`The specified config file doesn't exist in '${configPath}'`); @@ -940,7 +963,7 @@ class WebpackCLI { .map((filename) => // Since .cjs is not available on interpret side add it manually to default config extension list [...Object.keys(extensions), '.cjs'].map((ext) => ({ - path: resolve(filename + ext), + path: path.resolve(filename + ext), ext: ext, module: extensions[ext], })), @@ -1373,6 +1396,7 @@ class WebpackCLI { } if (options.json) { + const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext'); const handleWriteError = (error) => { logger.error(error); process.exit(2); diff --git a/test/help/help.test.js b/test/help/help.test.js index ad3a1089c4c..9b40070f5ab 100644 --- a/test/help/help.test.js +++ b/test/help/help.test.js @@ -19,7 +19,8 @@ describe('help', () => { expect(stdout).not.toContain('--cache-type'); // verbose expect(stdout).toContain('Global options:'); expect(stdout).toContain('Commands:'); - expect(stdout.match(/bundle\|b/g)).toHaveLength(1); + expect(stdout.match(/build\|bundle\|b/g)).toHaveLength(1); + expect(stdout.match(/watch\|w/g)).toHaveLength(1); expect(stdout.match(/version\|v/g)).toHaveLength(1); expect(stdout.match(/help\|h/g)).toHaveLength(1); expect(stdout.match(/serve\|s/g)).toHaveLength(1); @@ -51,7 +52,8 @@ describe('help', () => { expect(stdout).toContain('Global options:'); expect(stdout).toContain('Commands:'); - expect(stdout.match(/bundle\|b/g)).toHaveLength(1); + expect(stdout.match(/build\|bundle\|b/g)).toHaveLength(1); + expect(stdout.match(/watch\|w/g)).toHaveLength(1); expect(stdout.match(/version\|v/g)).toHaveLength(1); expect(stdout.match(/help\|h/g)).toHaveLength(1); expect(stdout.match(/serve\|s/g)).toHaveLength(1); @@ -155,7 +157,27 @@ describe('help', () => { expect(stdout).toContain('Made with ♥ by the webpack team'); }); - const commands = ['build', 'bundle', 'loader', 'plugin', 'info', 'init', 'serve', 'migrate']; + const commands = [ + 'build', + 'bundle', + 'b', + 'watch', + 'w', + 'serve', + 's', + 'info', + 'i', + 'init', + 'c', + 'loader', + 'l', + 'plugin', + 'p', + 'configtest', + 't', + 'migrate', + 'm', + ]; commands.forEach((command) => { it(`should show help information for '${command}' command using the "--help" option`, () => { @@ -163,7 +185,7 @@ describe('help', () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); - expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`); + expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`); }); it(`should show help information for '${command}' command using command syntax`, () => { @@ -171,7 +193,7 @@ describe('help', () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); - expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`); + expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`); }); it('should show help information and respect the "--color" flag using the "--help" option', () => { @@ -179,7 +201,7 @@ describe('help', () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); - expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`); + expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`); expect(stdout).toContain(coloretteEnabled ? bold('Made with ♥ by the webpack team') : 'Made with ♥ by the webpack team'); }); @@ -188,7 +210,7 @@ describe('help', () => { expect(exitCode).toBe(0); expect(stderr).toBeFalsy(); - expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`); + expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`); // TODO bug in tests // expect(stdout).not.toContain(bold('Made with ♥ by the webpack team')); expect(stdout).toContain('Made with ♥ by the webpack team'); diff --git a/test/version/version.test.js b/test/version/version.test.js index 5a3cc96d2d2..1cc9fb69ce5 100644 --- a/test/version/version.test.js +++ b/test/version/version.test.js @@ -105,6 +105,36 @@ describe('single version flag', () => { expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`); }); + it('outputs version with b', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['b', '--version'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`); + expect(stdout).toContain(`webpack ${webpack.version}`); + expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`); + }); + + it('outputs version with watch', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['watch', '--version'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`); + expect(stdout).toContain(`webpack ${webpack.version}`); + expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`); + }); + + it('outputs version with w', () => { + const { exitCode, stderr, stdout } = run(__dirname, ['w', '--version'], false); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`); + expect(stdout).toContain(`webpack ${webpack.version}`); + expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`); + }); + it('outputs version with plugin', () => { const { exitCode, stderr, stdout } = run(__dirname, ['plugin', '--version'], false); diff --git a/test/watch/basic/basic.test.js b/test/watch/basic/basic.test.js new file mode 100644 index 00000000000..34fee148278 --- /dev/null +++ b/test/watch/basic/basic.test.js @@ -0,0 +1,169 @@ +'use strict'; + +const stripAnsi = require('strip-ansi'); +const { run, runAndGetWatchProc, isWebpack5 } = require('../../utils/test-utils'); +const { writeFileSync } = require('fs'); +const { resolve } = require('path'); + +const wordsInStatsv4 = ['Hash', 'Version', 'Time', 'Built at:', 'main.js']; +const wordsInStatsv5 = ['asset', 'index.js', 'compiled successfully']; + +describe('basic', () => { + it('should work with negative value', async () => { + const { exitCode, stderr, stdout } = await run(__dirname, ['-c', './watch.config.js', '--no-watch']); + + expect(exitCode).toBe(0); + expect(stderr).toBeFalsy(); + expect(stdout).toBeTruthy(); + }); + + it('should recompile upon file change using the `--watch` option', (done) => { + const proc = runAndGetWatchProc(__dirname, ['--watch', '--mode', 'development'], false, '', true); + + let modified = false; + + proc.stdout.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + if (data.includes('index.js')) { + if (isWebpack5) { + for (const word of wordsInStatsv5) { + expect(data).toContain(word); + } + } else { + for (const word of wordsInStatsv4) { + expect(data).toContain(word); + } + } + + if (!modified) { + process.nextTick(() => { + writeFileSync(resolve(__dirname, './src/index.js'), `console.log('watch flag test');`); + }); + + modified = true; + } else { + proc.kill(); + done(); + } + } + }); + }); + + it('should recompile upon file change using the `watch` command', (done) => { + const proc = runAndGetWatchProc(__dirname, ['watch', '--mode', 'development'], false, '', true); + + let modified = false; + + proc.stdout.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + if (data.includes('index.js')) { + if (isWebpack5) { + for (const word of wordsInStatsv5) { + expect(data).toContain(word); + } + } else { + for (const word of wordsInStatsv4) { + expect(data).toContain(word); + } + } + + if (!modified) { + process.nextTick(() => { + writeFileSync(resolve(__dirname, './src/index.js'), `console.log('watch flag test');`); + }); + + modified = true; + } else { + proc.kill(); + done(); + } + } + }); + }); + + it('should recompile upon file change using the `command` option and the `--watch` option and log warning', (done) => { + const proc = runAndGetWatchProc(__dirname, ['watch', '--watch', '--mode', 'development'], false, '', true); + + let modified = false; + let hasWarning = false; + + proc.stdout.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + if (data.includes('index.js')) { + if (isWebpack5) { + for (const word of wordsInStatsv5) { + expect(data).toContain(word); + } + } else { + for (const word of wordsInStatsv4) { + expect(data).toContain(word); + } + } + + if (!modified && !hasWarning) { + process.nextTick(() => { + writeFileSync(resolve(__dirname, './src/index.js'), `console.log('watch flag test');`); + }); + + modified = true; + } else { + proc.kill(); + done(); + } + } + }); + + proc.stderr.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + hasWarning = true; + + expect(data).toContain("No need to use the '--watch, -w' option together with the 'watch' command, it does not make sense"); + }); + }); + + it('should recompile upon file change using the `command` option and the `--no-watch` option and log warning', (done) => { + const proc = runAndGetWatchProc(__dirname, ['watch', '--no-watch', '--mode', 'development'], false, '', true); + + let modified = false; + let hasWarning = false; + + proc.stdout.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + if (data.includes('index.js')) { + if (isWebpack5) { + for (const word of wordsInStatsv5) { + expect(data).toContain(word); + } + } else { + for (const word of wordsInStatsv4) { + expect(data).toContain(word); + } + } + + if (!modified && !hasWarning) { + process.nextTick(() => { + writeFileSync(resolve(__dirname, './src/index.js'), `console.log('watch flag test');`); + }); + + modified = true; + } else { + proc.kill(); + done(); + } + } + }); + + proc.stderr.on('data', (chunk) => { + const data = stripAnsi(chunk.toString()); + + hasWarning = true; + + expect(data).toContain("No need to use the '--no-watch' option together with the 'watch' command, it does not make sense"); + }); + }); +}); diff --git a/test/watch/simple/src/index.js b/test/watch/basic/src/index.js similarity index 100% rename from test/watch/simple/src/index.js rename to test/watch/basic/src/index.js diff --git a/test/watch/simple/watch.config.js b/test/watch/basic/watch.config.js similarity index 100% rename from test/watch/simple/watch.config.js rename to test/watch/basic/watch.config.js diff --git a/test/watch/simple/watch.test.js b/test/watch/simple/watch.test.js deleted file mode 100644 index 4ddf7386acb..00000000000 --- a/test/watch/simple/watch.test.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const stripAnsi = require('strip-ansi'); -const { run, runAndGetWatchProc, isWebpack5 } = require('../../utils/test-utils'); -const { writeFileSync } = require('fs'); -const { resolve } = require('path'); - -const wordsInStatsv4 = ['Hash', 'Version', 'Time', 'Built at:', 'main.js']; -const wordsInStatsv5 = ['asset', 'index.js', 'compiled successfully']; - -describe('--watch flag', () => { - it('should work with negative value', async () => { - const { exitCode, stderr, stdout } = await run(__dirname, ['-c', './watch.config.js', '--no-watch']); - - expect(exitCode).toBe(0); - expect(stderr).toBeFalsy(); - expect(stdout).toBeTruthy(); - }); - - it('should recompile upon file change', (done) => { - const proc = runAndGetWatchProc(__dirname, ['--watch', '--mode', 'development'], false, '', true); - - let modified = false; - - proc.stdout.on('data', (chunk) => { - const data = stripAnsi(chunk.toString()); - - if (data.includes('index.js')) { - if (isWebpack5) { - for (const word of wordsInStatsv5) { - expect(data).toContain(word); - } - } else { - for (const word of wordsInStatsv4) { - expect(data).toContain(word); - } - } - - if (!modified) { - process.nextTick(() => { - writeFileSync(resolve(__dirname, './src/index.js'), `console.log('watch flag test');`); - }); - - modified = true; - } else { - proc.kill(); - done(); - } - } - }); - }); -}); diff --git a/test/watch/stdin/stdin.test.js b/test/watch/stdin/stdin.test.js index 06d285d53ab..e7d3be46210 100644 --- a/test/watch/stdin/stdin.test.js +++ b/test/watch/stdin/stdin.test.js @@ -1,7 +1,7 @@ const { runAndGetWatchProc } = require('../../utils/test-utils'); describe('--watch-options-stdin', () => { - it.only('should stop the process when stdin ends using "--watch" and "--watch-options-stdin" options', (done) => { + it('should stop the process when stdin ends using "--watch" and "--watch-options-stdin" options', (done) => { const proc = runAndGetWatchProc(__dirname, ['--watch', '--watch-options-stdin'], false, '', true); let semaphore = false; @@ -19,6 +19,24 @@ describe('--watch-options-stdin', () => { }); }); + it('should stop the process when stdin ends using the "watch" command and the "--watch-options-stdin" option', (done) => { + const proc = runAndGetWatchProc(__dirname, ['watch', '--watch-options-stdin'], false, '', true); + + let semaphore = false; + + proc.on('exit', () => { + expect(semaphore).toBe(true); + + proc.kill(); + + done(); + }); + + proc.stdin.end(() => { + semaphore = true; + }); + }); + it('should stop the process when stdin ends using the config file', (done) => { const proc = runAndGetWatchProc(__dirname, ['--config', './watch.config.js'], false, '', true);