Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix: respect --watch-options-stdin
  • Loading branch information
alexander-akait committed Dec 25, 2020
1 parent 05636b7 commit 2d1e001
Show file tree
Hide file tree
Showing 24 changed files with 410 additions and 113 deletions.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -90,7 +90,7 @@
"strip-ansi": "^6.0.0",
"ts-jest": "^26.4.3",
"typescript": "^3.9.7",
"webpack": "^5.10.0",
"webpack": "^5.11.0",
"webpack-bundle-analyzer": "^3.9.0",
"webpack-dev-server": "^3.11.0",
"yeoman-test": "^2.7.0"
Expand Down
32 changes: 31 additions & 1 deletion packages/serve/src/index.ts
Expand Up @@ -82,8 +82,38 @@ class ServeCommand {

const compiler = await cli.createCompiler(webpackOptions);

if (!compiler) {
return;
}

let servers;

if (cli.needWatchStdin(compiler) || devServerOptions.stdin) {
// TODO
// Compatibility with old `stdin` option for `webpack-dev-server`
// Should be removed for the next major release on both sides
if (devServerOptions.stdin) {
delete devServerOptions.stdin;
}

process.stdin.on('end', () => {
Promise.all(
servers.map((server) => {
return new Promise((resolve) => {
server.close(() => {
resolve();
});
});
}),
).then(() => {
process.exit(0);
});
});
process.stdin.resume();
}

try {
await startDevServer(compiler, devServerOptions, logger);
servers = await startDevServer(compiler, devServerOptions, logger);
} catch (error) {
if (error.name === 'ValidationError') {
logger.error(error.message);
Expand Down
8 changes: 8 additions & 0 deletions packages/webpack-cli/lib/utils/cli-flags.js
Expand Up @@ -8,6 +8,7 @@ const minimumHelpFlags = [
'env',
'mode',
'watch',
'watch-options-stdin',
'stats',
'devtool',
'entry',
Expand Down Expand Up @@ -151,6 +152,13 @@ const builtInFlags = [
description: 'Watch for files changes.',
negatedDescription: 'Do not watch for file changes.',
},
{
name: 'watch-options-stdin',
usage: '--watch-options-stdin',
type: Boolean,
negative: true,
description: 'Stop watching when stdin stream has ended.',
},
];

// Extract all the flags being exported from core.
Expand Down
91 changes: 66 additions & 25 deletions packages/webpack-cli/lib/webpack-cli.js
Expand Up @@ -1021,6 +1021,13 @@ class WebpackCLI {
configOptions.watch = options.watch;
}

if (typeof options.watchOptionsStdin !== 'undefined') {
configOptions.watchOptions = {
...configOptions.watchOptions,
...{ stdin: options.watchOptionsStdin },
};
}

return configOptions;
};

Expand Down Expand Up @@ -1058,6 +1065,14 @@ class WebpackCLI {
return config;
}

needWatchStdin(compiler) {
if (compiler.compilers) {
return compiler.compilers.some((compiler) => compiler.options.watchOptions && compiler.options.watchOptions.stdin);
}

return compiler.options.watchOptions && compiler.options.watchOptions.stdin;
}

async createCompiler(options, callback) {
const isValidationError = (error) => {
// https://github.com/webpack/webpack/blob/master/lib/index.js#L267
Expand Down Expand Up @@ -1098,6 +1113,11 @@ class WebpackCLI {
process.exit(2);
}

// TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
if (compiler && compiler.compiler) {
compiler = compiler.compiler;
}

return compiler;
}

Expand All @@ -1114,9 +1134,11 @@ class WebpackCLI {
process.exitCode = 1;
}

// TODO remove after drop webpack@4
const statsForWebpack4 = webpack.Stats && webpack.Stats.presetToOptions;

const getStatsOptions = (stats) => {
// TODO remove after drop webpack@4
if (webpack.Stats && webpack.Stats.presetToOptions) {
if (statsForWebpack4) {
if (!stats) {
stats = {};
} else if (typeof stats === 'boolean' || typeof stats === 'string') {
Expand Down Expand Up @@ -1144,32 +1166,42 @@ class WebpackCLI {
return stats;
};

const getStatsOptionsFromCompiler = (compiler) => getStatsOptions(compiler.options ? compiler.options.stats : undefined);

if (!compiler) {
return;
}

const foundStats = compiler.compilers
? { children: compiler.compilers.map(getStatsOptionsFromCompiler) }
: getStatsOptionsFromCompiler(compiler);
const handleWriteError = (error) => {
logger.error(error);
process.exit(2);
};
? {
children: compiler.compilers.map((compiler) =>
getStatsOptions(compiler.options ? compiler.options.stats : undefined),
),
}
: getStatsOptions(compiler.options ? compiler.options.stats : undefined);

// TODO webpack@4 doesn't support `{ children: [{ colors: true }, { colors: true }] }` for stats
if (statsForWebpack4 && compiler.compilers) {
foundStats.colors = foundStats.children.some((child) => child.colors);
}

if (options.json) {
const handleWriteError = (error) => {
logger.error(error);
process.exit(2);
};

if (options.json === true) {
createJsonStringifyStream(stats.toJson(foundStats))
.on('error', handleWriteError)
.pipe(process.stdout)
.on('error', handleWriteError)
.on('close', () => process.stdout.write('\n'));
} else if (typeof options.json === 'string') {
createJsonStringifyStream(stats.toJson(foundStats))
.on('error', handleWriteError)
.pipe(createWriteStream(options.json))
.on('error', handleWriteError)
.on('close', () => logger.success(`stats are successfully stored as json to ${options.json}`));
if (options.json === true) {
createJsonStringifyStream(stats.toJson(foundStats))
.on('error', handleWriteError)
.pipe(process.stdout)
.on('error', handleWriteError)
.on('close', () => process.stdout.write('\n'));
} else {
createJsonStringifyStream(stats.toJson(foundStats))
.on('error', handleWriteError)
.pipe(createWriteStream(options.json))
.on('error', handleWriteError)
.on('close', () => logger.success(`stats are successfully stored as json to ${options.json}`));
}
} else {
const printedStats = stats.toString(foundStats);

Expand All @@ -1184,9 +1216,18 @@ class WebpackCLI {

compiler = await this.createCompiler(options, callback);

// TODO webpack@4 return Watching and MultiWathing instead Compiler and MultiCompiler, remove this after drop webpack@4
if (compiler && compiler.compiler) {
compiler = compiler.compiler;
if (!compiler) {
return;
}

const isWatch = (compiler) =>
compiler.compilers ? compiler.compilers.some((compiler) => compiler.options.watch) : compiler.options.watch;

if (isWatch(compiler) && this.needWatchStdin(compiler)) {
process.stdin.on('end', () => {
process.exit(0);
});
process.stdin.resume();
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions test/config/multiple/multiple-config.test.js
@@ -1,3 +1,5 @@
const stripAnsi = require('strip-ansi');

const { run } = require('../../utils/test-utils');

describe('Multiple config flag: ', () => {
Expand All @@ -8,7 +10,7 @@ describe('Multiple config flag: ', () => {
expect(exitCode).toEqual(0);
expect(stderr).toBeFalsy();
// Should spawn multiple compilers
expect(stdout).toContain('amd:');
expect(stdout).toContain('commonjs:');
expect(stripAnsi(stdout)).toContain('amd:');
expect(stripAnsi(stdout)).toContain('commonjs:');
});
});
2 changes: 1 addition & 1 deletion test/core-flags/watch-flags.test.js
Expand Up @@ -11,7 +11,7 @@ describe('watch config related flag', () => {
const property = flag.name.split('watch-options-')[1];
const propName = hyphenToUpperCase(property);

if (flag.type === Boolean && flag.name !== 'watch') {
if (flag.type === Boolean && flag.name !== 'watch' && flag.name !== 'watch-options-stdin') {
it(`should config --${flag.name} correctly`, () => {
const { exitCode, stderr, stdout } = run(__dirname, [`--${flag.name}`]);

Expand Down
32 changes: 32 additions & 0 deletions test/error/error-in-plugin/error.test.js
@@ -0,0 +1,32 @@
'use strict';

const { run } = require('../../utils/test-utils');

describe('error', () => {
it('should log error with stacktrace', async () => {
const { exitCode, stderr, stdout } = await run(__dirname);

expect(exitCode).toBe(2);
expect(stderr).toContain('Error: test');
expect(stderr).toMatch(/at .+ (.+)/);
expect(stdout).toBeFalsy();
});

it('should log error with stacktrace using the "bundle" command', async () => {
const { exitCode, stderr, stdout } = await run(__dirname, ['bundle']);

expect(exitCode).toBe(2);
expect(stderr).toContain('Error: test');
expect(stderr).toMatch(/at .+ (.+)/);
expect(stdout).toBeFalsy();
});

it('should log error with stacktrace using the "serve" command', async () => {
const { exitCode, stderr, stdout } = await run(__dirname, ['serve']);

expect(exitCode).toBe(2);
expect(stderr).toContain('Error: test');
expect(stderr).toMatch(/at .+ (.+)/);
expect(stdout).toBeFalsy();
});
});
File renamed without changes.
File renamed without changes.
14 changes: 0 additions & 14 deletions test/error/error.test.js

This file was deleted.

73 changes: 73 additions & 0 deletions test/error/invalid-schema/invalid-schema.test.js
@@ -0,0 +1,73 @@
'use strict';
const { run, isWebpack5 } = require('../../utils/test-utils');

describe('invalid schema', () => {
it('should log error on invalid config', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['--config', './webpack.config.mock.js']);

expect(exitCode).toEqual(2);
expect(stderr).toContain('Invalid configuration object');
expect(stdout).toBeFalsy();
});

it('should log error on invalid config using the "bundle" command', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['bundle', '--config', './webpack.config.mock.js']);

expect(exitCode).toEqual(2);
expect(stderr).toContain('Invalid configuration object');
expect(stdout).toBeFalsy();
});

it('should log error on invalid config using the "serve" command', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['serve', '--config', './webpack.config.mock.js']);

expect(exitCode).toEqual(2);
expect(stderr).toContain('Invalid configuration object');
expect(stdout).toBeFalsy();
});

it('should log error on invalid option', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['--mode', 'Yukihira']);

expect(exitCode).toEqual(2);

if (isWebpack5) {
expect(stderr).toContain("Invalid value 'Yukihira' for the '--mode' option");
expect(stderr).toContain("Expected: 'development | production | none'");
} else {
expect(stderr).toContain('Invalid configuration object');
}

expect(stdout).toBeFalsy();
});

it('should log error on invalid option using "bundle" command', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['bundle', '--mode', 'Yukihira']);

expect(exitCode).toEqual(2);

if (isWebpack5) {
expect(stderr).toContain("Invalid value 'Yukihira' for the '--mode' option");
expect(stderr).toContain("Expected: 'development | production | none'");
} else {
expect(stderr).toContain('Invalid configuration object');
}

expect(stdout).toBeFalsy();
});

it('should log error on invalid option using "server" command', () => {
const { exitCode, stderr, stdout } = run(__dirname, ['serve', '--mode', 'Yukihira']);

expect(exitCode).toEqual(2);

if (isWebpack5) {
expect(stderr).toContain("Invalid value 'Yukihira' for the '--mode' option");
expect(stderr).toContain("Expected: 'development | production | none'");
} else {
expect(stderr).toContain('Invalid configuration object');
}

expect(stdout).toBeFalsy();
});
});
File renamed without changes.
27 changes: 0 additions & 27 deletions test/invalid-schema/invalid-schema.test.js

This file was deleted.

File renamed without changes.
File renamed without changes.
@@ -1,7 +1,7 @@
'use strict';

const stripAnsi = require('strip-ansi');
const { run, runAndGetWatchProc, isWebpack5 } = require('../utils/test-utils');
const { run, runAndGetWatchProc, isWebpack5 } = require('../../utils/test-utils');
const { writeFileSync } = require('fs');
const { resolve } = require('path');

Expand Down

0 comments on commit 2d1e001

Please sign in to comment.