Skip to content

Commit 9693f7d

Browse files
feat: added the watch command (#2357)
1 parent 451b904 commit 9693f7d

File tree

10 files changed

+315
-103
lines changed

10 files changed

+315
-103
lines changed

OPTIONS.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Alternative usage: webpack build [options]
55
Alternative usage: webpack bundle [options]
66
Alternative usage: webpack b [options]
77
Alternative usage: webpack build --config <config> [options]
8+
Alternative usage: webpack bundle --config <config> [options]
9+
Alternative usage: webpack b --config <config> [options]
810
911
The build tool for modern web applications.
1012
@@ -700,14 +702,15 @@ Global options:
700702
701703
Commands:
702704
build|bundle|b [options] Run webpack (default command, can be omitted).
705+
watch|w [options] Run webpack and watch for files changes.
703706
version|v [commands...] Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.
704707
help|h [command] [option] Display help for commands and options.
705708
serve|s [options] Run the webpack dev server.
706709
info|i [options] Outputs information about your system.
707710
init|c [options] [scaffold...] Initialize a new webpack configuration.
708711
loader|l [output-path] Scaffold a loader.
709712
migrate|m <config-path> [new-config-path] Migrate a configuration to a new version.
710-
configtest|t <config-path> Tests webpack configuration against validation errors.
713+
configtest|t [config-path] Tests webpack configuration against validation errors.
711714
plugin|p [output-path] Scaffold a plugin.
712715
713716
To see list of all supported commands and options run 'webpack --help=verbose'.

packages/configtest/src/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import webpack from 'webpack';
2-
31
class ConfigTestCommand {
42
async apply(cli): Promise<void> {
5-
const { logger } = cli;
3+
const { logger, webpack } = cli;
64

75
await cli.makeCommand(
86
{

packages/webpack-cli/lib/webpack-cli.js

+63-39
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
1-
const path = require('path');
21
const { program } = require('commander');
32
const getPkg = require('./utils/package-exists');
43
const webpack = getPkg('webpack') ? require('webpack') : undefined;
4+
const path = require('path');
55
const { merge } = require('webpack-merge');
66
const { extensions, jsVariants } = require('interpret');
77
const rechoir = require('rechoir');
88
const { createWriteStream, existsSync } = require('fs');
99
const { distance } = require('fastest-levenshtein');
1010
const { options: coloretteOptions, yellow, cyan, green, bold } = require('colorette');
11-
const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext');
1211

1312
const logger = require('./utils/logger');
1413
const { cli, flags } = require('./utils/cli-flags');
1514
const CLIPlugin = require('./plugins/CLIPlugin');
1615
const promptInstallation = require('./utils/prompt-installation');
17-
1816
const toKebabCase = require('./utils/to-kebab-case');
1917

20-
const { resolve, extname } = path;
21-
2218
class WebpackCLI {
2319
constructor() {
24-
this.logger = logger;
2520
// Initialize program
2621
this.program = program;
2722
this.program.name('webpack');
2823
this.program.storeOptionsAsProperties(false);
24+
this.webpack = webpack;
25+
this.logger = logger;
2926
this.utils = { toKebabCase, getPkg, promptInstallation };
3027
}
3128

@@ -234,6 +231,12 @@ class WebpackCLI {
234231
description: 'Run webpack (default command, can be omitted).',
235232
usage: '[options]',
236233
};
234+
const watchCommandOptions = {
235+
name: 'watch',
236+
alias: 'w',
237+
description: 'Run webpack and watch for files changes.',
238+
usage: '[options]',
239+
};
237240
const versionCommandOptions = {
238241
name: 'version [commands...]',
239242
alias: 'v',
@@ -283,7 +286,13 @@ class WebpackCLI {
283286
},
284287
];
285288

286-
const knownCommands = [buildCommandOptions, versionCommandOptions, helpCommandOptions, ...externalBuiltInCommandsInfo];
289+
const knownCommands = [
290+
buildCommandOptions,
291+
watchCommandOptions,
292+
versionCommandOptions,
293+
helpCommandOptions,
294+
...externalBuiltInCommandsInfo,
295+
];
287296
const getCommandName = (name) => name.split(' ')[0];
288297
const isKnownCommand = (name) =>
289298
knownCommands.find(
@@ -294,6 +303,9 @@ class WebpackCLI {
294303
const isBuildCommand = (name) =>
295304
getCommandName(buildCommandOptions.name) === name ||
296305
(Array.isArray(buildCommandOptions.alias) ? buildCommandOptions.alias.includes(name) : buildCommandOptions.alias === name);
306+
const isWatchCommand = (name) =>
307+
getCommandName(watchCommandOptions.name) === name ||
308+
(Array.isArray(watchCommandOptions.alias) ? watchCommandOptions.alias.includes(name) : watchCommandOptions.alias === name);
297309
const isHelpCommand = (name) =>
298310
getCommandName(helpCommandOptions.name) === name ||
299311
(Array.isArray(helpCommandOptions.alias) ? helpCommandOptions.alias.includes(name) : helpCommandOptions.alias === name);
@@ -338,21 +350,40 @@ class WebpackCLI {
338350
return { commandName: isDefault ? buildCommandOptions.name : commandName, options, isDefault };
339351
};
340352
const loadCommandByName = async (commandName, allowToInstall = false) => {
341-
if (isBuildCommand(commandName)) {
342-
await this.makeCommand(buildCommandOptions, this.getBuiltInOptions(), async (program) => {
343-
const options = program.opts();
353+
const isBuildCommandUsed = isBuildCommand(commandName);
354+
const isWatchCommandUsed = isWatchCommand(commandName);
344355

345-
if (program.args.length > 0) {
346-
const possibleCommands = [].concat([buildCommandOptions.name]).concat(program.args);
356+
if (isBuildCommandUsed || isWatchCommandUsed) {
357+
await this.makeCommand(
358+
isBuildCommandUsed ? buildCommandOptions : watchCommandOptions,
359+
this.getBuiltInOptions(),
360+
async (program) => {
361+
const options = program.opts();
347362

348-
logger.error('Running multiple commands at the same time is not possible');
349-
logger.error(`Found commands: ${possibleCommands.map((item) => `'${item}'`).join(', ')}`);
350-
logger.error("Run 'webpack --help' to see available commands and options");
351-
process.exit(2);
352-
}
363+
if (program.args.length > 0) {
364+
const possibleCommands = [].concat([buildCommandOptions.name]).concat(program.args);
353365

354-
await this.bundleCommand(options);
355-
});
366+
logger.error('Running multiple commands at the same time is not possible');
367+
logger.error(`Found commands: ${possibleCommands.map((item) => `'${item}'`).join(', ')}`);
368+
logger.error("Run 'webpack --help' to see available commands and options");
369+
process.exit(2);
370+
}
371+
372+
if (isWatchCommandUsed) {
373+
if (typeof options.watch !== 'undefined') {
374+
logger.warn(
375+
`No need to use the ${
376+
options.watch ? "'--watch, -w'" : "'--no-watch'"
377+
} option together with the 'watch' command, it does not make sense`,
378+
);
379+
}
380+
381+
options.watch = true;
382+
}
383+
384+
await this.bundleCommand(options);
385+
},
386+
);
356387
} else if (isHelpCommand(commandName)) {
357388
// Stub for the `help` command
358389
this.makeCommand(helpCommandOptions, [], () => {});
@@ -492,9 +523,9 @@ class WebpackCLI {
492523
// Make `-v, --version` options
493524
// Make `version|v [commands...]` command
494525
const outputVersion = async (options) => {
495-
// Filter `bundle`, `version` and `help` commands
526+
// Filter `bundle`, `watch`, `version` and `help` commands
496527
const possibleCommandNames = options.filter(
497-
(option) => !isBuildCommand(option) && !isVersionCommand(option) && !isHelpCommand(option),
528+
(option) => !isBuildCommand(option) && !isWatchCommand(option) && !isVersionCommand(option) && !isHelpCommand(option),
498529
);
499530

500531
possibleCommandNames.forEach((possibleCommandName) => {
@@ -616,7 +647,7 @@ class WebpackCLI {
616647
.replace(buildCommandOptions.description, 'The build tool for modern web applications.')
617648
.replace(
618649
/Usage:.+/,
619-
'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]',
650+
'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]',
620651
);
621652

622653
logger.raw(helpInformation);
@@ -643,25 +674,21 @@ class WebpackCLI {
643674
let helpInformation = command.helpInformation().trimRight();
644675

645676
if (isBuildCommand(name)) {
646-
helpInformation = helpInformation
647-
.replace(buildCommandOptions.description, 'The build tool for modern web applications.')
648-
.replace(
649-
/Usage:.+/,
650-
'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]',
651-
);
677+
helpInformation = helpInformation.replace('build|bundle', 'build|bundle|b');
652678
}
653679

654680
logger.raw(helpInformation);
655681

656682
outputGlobalOptions();
657683
} else if (isHelpCommandSyntax) {
658-
let commandName;
684+
let isCommandSpecified = false;
685+
let commandName = buildCommandOptions.name;
659686
let optionName;
660687

661688
if (options.length === 1) {
662-
commandName = buildCommandOptions.name;
663689
optionName = options[0];
664690
} else if (options.length === 2) {
691+
isCommandSpecified = true;
665692
commandName = options[0];
666693
optionName = options[1];
667694

@@ -694,14 +721,10 @@ class WebpackCLI {
694721
option.flags.replace(/^.+[[<]/, '').replace(/(\.\.\.)?[\]>].*$/, '') + (option.variadic === true ? '...' : '');
695722
const value = option.required ? '<' + nameOutput + '>' : option.optional ? '[' + nameOutput + ']' : '';
696723

697-
logger.raw(
698-
`Usage: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.long}${value ? ` ${value}` : ''}`,
699-
);
724+
logger.raw(`Usage: webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.long}${value ? ` ${value}` : ''}`);
700725

701726
if (option.short) {
702-
logger.raw(
703-
`Short: webpack${isBuildCommand(commandName) ? '' : ` ${commandName}`} ${option.short}${value ? ` ${value}` : ''}`,
704-
);
727+
logger.raw(`Short: webpack${isCommandSpecified ? ` ${commandName}` : ''} ${option.short}${value ? ` ${value}` : ''}`);
705728
}
706729

707730
if (option.description) {
@@ -806,7 +829,7 @@ class WebpackCLI {
806829

807830
async resolveConfig(options) {
808831
const loadConfig = async (configPath) => {
809-
const ext = extname(configPath);
832+
const ext = path.extname(configPath);
810833
const interpreted = Object.keys(jsVariants).find((variant) => variant === ext);
811834

812835
if (interpreted) {
@@ -906,7 +929,7 @@ class WebpackCLI {
906929
if (options.config && options.config.length > 0) {
907930
const evaluatedConfigs = await Promise.all(
908931
options.config.map(async (value) => {
909-
const configPath = resolve(value);
932+
const configPath = path.resolve(value);
910933

911934
if (!existsSync(configPath)) {
912935
logger.error(`The specified config file doesn't exist in '${configPath}'`);
@@ -940,7 +963,7 @@ class WebpackCLI {
940963
.map((filename) =>
941964
// Since .cjs is not available on interpret side add it manually to default config extension list
942965
[...Object.keys(extensions), '.cjs'].map((ext) => ({
943-
path: resolve(filename + ext),
966+
path: path.resolve(filename + ext),
944967
ext: ext,
945968
module: extensions[ext],
946969
})),
@@ -1373,6 +1396,7 @@ class WebpackCLI {
13731396
}
13741397

13751398
if (options.json) {
1399+
const { stringifyStream: createJsonStringifyStream } = require('@discoveryjs/json-ext');
13761400
const handleWriteError = (error) => {
13771401
logger.error(error);
13781402
process.exit(2);

test/help/help.test.js

+29-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ describe('help', () => {
1919
expect(stdout).not.toContain('--cache-type'); // verbose
2020
expect(stdout).toContain('Global options:');
2121
expect(stdout).toContain('Commands:');
22-
expect(stdout.match(/bundle\|b/g)).toHaveLength(1);
22+
expect(stdout.match(/build\|bundle\|b/g)).toHaveLength(1);
23+
expect(stdout.match(/watch\|w/g)).toHaveLength(1);
2324
expect(stdout.match(/version\|v/g)).toHaveLength(1);
2425
expect(stdout.match(/help\|h/g)).toHaveLength(1);
2526
expect(stdout.match(/serve\|s/g)).toHaveLength(1);
@@ -51,7 +52,8 @@ describe('help', () => {
5152

5253
expect(stdout).toContain('Global options:');
5354
expect(stdout).toContain('Commands:');
54-
expect(stdout.match(/bundle\|b/g)).toHaveLength(1);
55+
expect(stdout.match(/build\|bundle\|b/g)).toHaveLength(1);
56+
expect(stdout.match(/watch\|w/g)).toHaveLength(1);
5557
expect(stdout.match(/version\|v/g)).toHaveLength(1);
5658
expect(stdout.match(/help\|h/g)).toHaveLength(1);
5759
expect(stdout.match(/serve\|s/g)).toHaveLength(1);
@@ -155,31 +157,51 @@ describe('help', () => {
155157
expect(stdout).toContain('Made with ♥ by the webpack team');
156158
});
157159

158-
const commands = ['build', 'bundle', 'loader', 'plugin', 'info', 'init', 'serve', 'migrate'];
160+
const commands = [
161+
'build',
162+
'bundle',
163+
'b',
164+
'watch',
165+
'w',
166+
'serve',
167+
's',
168+
'info',
169+
'i',
170+
'init',
171+
'c',
172+
'loader',
173+
'l',
174+
'plugin',
175+
'p',
176+
'configtest',
177+
't',
178+
'migrate',
179+
'm',
180+
];
159181

160182
commands.forEach((command) => {
161183
it(`should show help information for '${command}' command using the "--help" option`, () => {
162184
const { exitCode, stderr, stdout } = run(__dirname, [command, '--help'], false);
163185

164186
expect(exitCode).toBe(0);
165187
expect(stderr).toBeFalsy();
166-
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`);
188+
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`);
167189
});
168190

169191
it(`should show help information for '${command}' command using command syntax`, () => {
170192
const { exitCode, stderr, stdout } = run(__dirname, ['help', command], false);
171193

172194
expect(exitCode).toBe(0);
173195
expect(stderr).toBeFalsy();
174-
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`);
196+
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`);
175197
});
176198

177199
it('should show help information and respect the "--color" flag using the "--help" option', () => {
178200
const { exitCode, stderr, stdout } = run(__dirname, [command, '--help', '--color'], false);
179201

180202
expect(exitCode).toBe(0);
181203
expect(stderr).toBeFalsy();
182-
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`);
204+
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`);
183205
expect(stdout).toContain(coloretteEnabled ? bold('Made with ♥ by the webpack team') : 'Made with ♥ by the webpack team');
184206
});
185207

@@ -188,7 +210,7 @@ describe('help', () => {
188210

189211
expect(exitCode).toBe(0);
190212
expect(stderr).toBeFalsy();
191-
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' ? '' : command}`);
213+
expect(stdout).toContain(`webpack ${command === 'build' || command === 'bundle' || command === 'b' ? '' : command}`);
192214
// TODO bug in tests
193215
// expect(stdout).not.toContain(bold('Made with ♥ by the webpack team'));
194216
expect(stdout).toContain('Made with ♥ by the webpack team');

test/version/version.test.js

+30
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,36 @@ describe('single version flag', () => {
105105
expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`);
106106
});
107107

108+
it('outputs version with b', () => {
109+
const { exitCode, stderr, stdout } = run(__dirname, ['b', '--version'], false);
110+
111+
expect(exitCode).toBe(0);
112+
expect(stderr).toBeFalsy();
113+
expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`);
114+
expect(stdout).toContain(`webpack ${webpack.version}`);
115+
expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`);
116+
});
117+
118+
it('outputs version with watch', () => {
119+
const { exitCode, stderr, stdout } = run(__dirname, ['watch', '--version'], false);
120+
121+
expect(exitCode).toBe(0);
122+
expect(stderr).toBeFalsy();
123+
expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`);
124+
expect(stdout).toContain(`webpack ${webpack.version}`);
125+
expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`);
126+
});
127+
128+
it('outputs version with w', () => {
129+
const { exitCode, stderr, stdout } = run(__dirname, ['w', '--version'], false);
130+
131+
expect(exitCode).toBe(0);
132+
expect(stderr).toBeFalsy();
133+
expect(stdout).toContain(`webpack-cli ${pkgJSON.version}`);
134+
expect(stdout).toContain(`webpack ${webpack.version}`);
135+
expect(stdout).toContain(`webpack-dev-server ${webpackDevServerPkgJSON.version}`);
136+
});
137+
108138
it('outputs version with plugin', () => {
109139
const { exitCode, stderr, stdout } = run(__dirname, ['plugin', '--version'], false);
110140

0 commit comments

Comments
 (0)