Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added the watch command #2357

Merged
merged 7 commits into from Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduce time to initial run

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