Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: added the watch command (#2357)
  • Loading branch information
alexander-akait committed Jan 18, 2021
1 parent 451b904 commit 9693f7d
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 103 deletions.
5 changes: 4 additions & 1 deletion OPTIONS.md
Expand Up @@ -5,6 +5,8 @@ Alternative usage: webpack build [options]
Alternative usage: webpack bundle [options]
Alternative usage: webpack b [options]
Alternative usage: webpack build --config <config> [options]
Alternative usage: webpack bundle --config <config> [options]
Alternative usage: webpack b --config <config> [options]
The build tool for modern web applications.
Expand Down Expand Up @@ -700,14 +702,15 @@ 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.
info|i [options] Outputs information about your system.
init|c [options] [scaffold...] Initialize a new webpack configuration.
loader|l [output-path] Scaffold a loader.
migrate|m <config-path> [new-config-path] Migrate a configuration to a new version.
configtest|t <config-path> 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'.
Expand Down
4 changes: 1 addition & 3 deletions packages/configtest/src/index.ts
@@ -1,8 +1,6 @@
import webpack from 'webpack';

class ConfigTestCommand {
async apply(cli): Promise<void> {
const { logger } = cli;
const { logger, webpack } = cli;

await cli.makeCommand(
{
Expand Down
102 changes: 63 additions & 39 deletions 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 };
}

Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand Down Expand Up @@ -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, [], () => {});
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 <config> [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config <config> [options]',
'Usage: webpack [options]\nAlternative usage: webpack --config <config> [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config <config> [options]\nAlternative usage: webpack bundle --config <config> [options]\nAlternative usage: webpack b --config <config> [options]',
);

logger.raw(helpInformation);
Expand All @@ -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 <config> [options]\nAlternative usage: webpack build [options]\nAlternative usage: webpack bundle [options]\nAlternative usage: webpack b [options]\nAlternative usage: webpack build --config <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];

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}'`);
Expand Down Expand Up @@ -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],
})),
Expand Down Expand Up @@ -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);
Expand Down
36 changes: 29 additions & 7 deletions test/help/help.test.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -155,31 +157,51 @@ 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`, () => {
const { exitCode, stderr, stdout } = run(__dirname, [command, '--help'], false);

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`, () => {
const { exitCode, stderr, stdout } = run(__dirname, ['help', command], false);

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', () => {
const { exitCode, stderr, stdout } = run(__dirname, [command, '--help', '--color'], false);

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');
});

Expand All @@ -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');
Expand Down
30 changes: 30 additions & 0 deletions test/version/version.test.js
Expand Up @@ -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);

Expand Down

0 comments on commit 9693f7d

Please sign in to comment.