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(webpack-cli): infrastructure logging #2036

Closed
Closed
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 packages/utils/__tests__/run-prettier.test.ts
Expand Up @@ -5,10 +5,13 @@ import path from 'path';
//eslint-disable-next-line node/no-extraneous-import
import rimraf from 'rimraf';
import { runPrettier } from '../src/run-prettier';
import { utils } from 'webpack-cli';

const { logger } = utils;
const outputPath = path.join(__dirname, 'test-assets');
const outputFile = path.join(outputPath, 'test.js');
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();

const consoleSpy = jest.spyOn(logger, 'warn').mockImplementation();

describe('runPrettier', () => {
beforeEach(() => {
Expand Down
6 changes: 2 additions & 4 deletions packages/webpack-cli/__tests__/info.test.js
Expand Up @@ -3,18 +3,17 @@ const path = require('path');

describe('Info', () => {
it('should run with cli', () => {
const { stdout, stderr } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['info'], {
const { stdout } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['info'], {
cwd: path.resolve(__dirname),
reject: false,
});
expect(stdout).toContain('System');
expect(stdout).toContain('Binaries');
expect(stdout).toContain('OS');
expect(stderr).toBeFalsy();
});

it('should work with flags', () => {
const { stdout, stderr } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['info', '--output=json'], {
const { stdout } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['info', '--output=json'], {
cwd: path.resolve(__dirname),
reject: false,
});
Expand All @@ -25,6 +24,5 @@ describe('Info', () => {
expect(output['System']['OS']).toBeTruthy();
};
expect(testJSON).not.toThrow();
expect(stderr).toBeFalsy();
});
});
7 changes: 3 additions & 4 deletions packages/webpack-cli/__tests__/init.test.js
Expand Up @@ -18,12 +18,11 @@ describe('init', () => {
});

it('should work with cli', () => {
const { stdout, stderr } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['init'], {
const { stdout } = spawnSync(path.resolve(__dirname, '../bin/cli.js'), ['init'], {
cwd: genPath,
reject: false,
});
expect(stdout).toBeTruthy();
expect(stderr).toBeFalsy();

expect(stdout).toContain(firstPrompt);
});
it('should run with cli when auto is supplied', () => {
Expand All @@ -32,7 +31,7 @@ describe('init', () => {
reject: false,
});
// Test no prompts are present
expect(stdout).toBeTruthy();

expect(stdout).not.toContain(firstPrompt);

// Skip test in case installation fails
Expand Down
6 changes: 2 additions & 4 deletions packages/webpack-cli/__tests__/serve/serve.test.js
Expand Up @@ -12,16 +12,14 @@ describe('Serve', () => {
}

it('should run with cli', async () => {
const { stdout, stderr } = await runServe([], __dirname);
const { stdout } = await runServe([], __dirname);
expect(stdout).toContain('main.js');
expect(stdout).not.toContain('HotModuleReplacementPlugin');
expect(stderr).toHaveLength(0);
});

it('should work with flags', async () => {
const { stdout, stderr } = await runServe(['--hot'], __dirname);
const { stdout } = await runServe(['--hot'], __dirname);
expect(stdout).toContain('main.js');
expect(stdout).toContain('HotModuleReplacementPlugin');
expect(stderr).toHaveLength(0);
});
});
125 changes: 91 additions & 34 deletions packages/webpack-cli/lib/plugins/WebpackCLIPlugin.js
@@ -1,63 +1,120 @@
const packageExists = require('../utils/package-exists');
const webpack = packageExists('webpack') ? require('webpack') : undefined;
const logger = require('../utils/logger');

const PluginName = 'webpack-cli';
const { getStatsOptions } = require('../utils/stats-options');
const { PluginName } = require('../utils/name');

class WebpackCLIPlugin {
constructor(options) {
this.options = options;

this.isWatch = false;
}

async apply(compiler) {
const compilers = compiler.compilers || [compiler];
const { progress } = this.options;
if (progress) {
this.appendProgressPlugin(compiler, progress);
}

for (const compiler of compilers) {
if (this.options.progress) {
const { ProgressPlugin } = compiler.webpack || webpack;
const isWebpack5 = Boolean(compiler.webpack);

let progressPluginExists;
const logger = compiler.getInfrastructureLogger(PluginName);

if (compiler.options.plugins) {
progressPluginExists = Boolean(compiler.options.plugins.find((plugin) => plugin instanceof ProgressPlugin));
}
const resolveName = (obj) => (obj.name ? `${obj.name} ` : '');

if (!progressPluginExists) {
if (typeof this.options.progress === 'string' && this.options.progress !== 'profile') {
logger.error(
`'${this.options.progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`,
);
process.exit(2);
}
const done = (stats) => {
const printStats = (childCompiler, childStats) => {
const name = resolveName(childCompiler);

const status = childStats.toString({
all: false,
version: true,
timings: true,
});

const normalizedStatus = (() => {
if (isWebpack5) {
return status;
} else {
const [version, time] = status.split('\n').map((line) => {
const value = line.split(':')[1] || '';
return value.trim();
});

const isProfile = this.options.progress === 'profile';
return `${name ? name + `(${version})` : version} compiled in ${time}`;
}
})();

new ProgressPlugin({ profile: isProfile }).apply(compiler);
if (childStats.hasErrors()) {
logger.error(normalizedStatus);
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

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

We should set exitCode = 1 for errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exit code is set in callback in webpack-cli.js this wasn't changed https://github.com/webpack/webpack-cli/pull/2036/files#diff-7c711e2a4ef4b16d2cbbe1658495723402f9692da654cb6fa959127f3dc779f2R357-R359

Wondering if we should reset it if next complication is has no errors 🤔

} else if (childStats.hasWarnings()) {
logger.warn(normalizedStatus);
} else {
logger.info(normalizedStatus);
}
}
}

const compilationName = (compilation) => (compilation.name ? ` ${compilation.name}` : '');
const statsString = childStats.toString(getStatsOptions(childCompiler));
process.stdout.write(statsString + '\n');

compiler.hooks.watchRun.tap(PluginName, (compilation) => {
const { bail, watch } = compilation.options;
if (bail && watch) {
logger.warn('You are using "bail" with "watch". "bail" will still exit webpack when the first error is found.');
if (this.isWatch) {
logger.info(name + 'watching files for updates...');
}
};

if (compiler.compilers) {
compiler.compilers.forEach((compilerFromMultiCompileMode, index) => {
printStats(compilerFromMultiCompileMode, stats.stats[index]);
});
} else {
printStats(compiler, stats);
}
};

logger.success(`Compilation${compilationName(compilation)} starting...`);
compiler.hooks.run.tap(PluginName, (compilation) => {
logger.info(resolveName(compilation) + 'compilation starting...');
});
compiler.hooks.watchRun.tap(PluginName, (compilation) => {
logger.info(resolveName(compilation) + 'compilation starting...');

compiler.hooks.done.tap(PluginName, (compilation) => {
logger.success(`Compilation${compilationName(compilation)} finished`);

if (compilation.options.bail) {
logger.warn('you are using "bail" with "watch". "bail" will still exit webpack when the first error is found.');
}
this.isWatch = true;
});
compiler.hooks.done.tap(PluginName, (stats) => {
process.nextTick(() => {
if (compiler.watchMode) {
logger.success('watching files for updates...');
}
done(stats);
});
});
}

appendProgressPlugin(compiler, progress) {
const { ProgressPlugin } = compiler.webpack || webpack;

const logger = compiler.getInfrastructureLogger(PluginName);

const compilers = compiler.compilers || [compiler];

for (const compiler of compilers) {
let progressPluginExists;

if (compiler.options.plugins) {
progressPluginExists = Boolean(compiler.options.plugins.find((plugin) => plugin instanceof ProgressPlugin));
}

if (!progressPluginExists) {
if (typeof progress === 'string' && progress !== 'profile') {
logger.error(`'${progress}' is an invalid value for the --progress option. Only 'profile' is allowed.`);

process.exit(2);
}

const isProfile = progress === 'profile';

new ProgressPlugin({ profile: isProfile }).apply(compiler);
}
}
}
}

module.exports = WebpackCLIPlugin;
5 changes: 5 additions & 0 deletions packages/webpack-cli/lib/utils/name.js
@@ -0,0 +1,5 @@
const PluginName = 'webpack-cli';

module.exports = {
PluginName,
};
26 changes: 26 additions & 0 deletions packages/webpack-cli/lib/utils/stats-options.js
@@ -0,0 +1,26 @@
const packageExists = require('./package-exists');
const webpack = packageExists('webpack') ? require('webpack') : undefined;
const { options: coloretteOptions } = require('colorette');

const getStatsOptions = (compiler) => {
let options = compiler.options
? typeof compiler.options.stats === 'object'
? Object.assign({}, compiler.options.stats)
: compiler.options.stats
: undefined;

// TODO remove after drop webpack@4
if (webpack.Stats && webpack.Stats.presetToOptions) {
if (!options) {
options = {};
} else if (typeof options === 'boolean' || typeof options === 'string') {
options = webpack.Stats.presetToOptions(options);
}
}

options.colors = typeof options.colors !== 'undefined' ? options.colors : coloretteOptions.enabled;

return options;
};

module.exports = { getStatsOptions };
Copy link
Member

Choose a reason for hiding this comment

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

No utils relates to complation, use methods classes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was execrated to utils as, moved stats display to WebpackCLIPlugin, but wanted to keep json output in WebpackCLI compiler callback.

58 changes: 22 additions & 36 deletions packages/webpack-cli/lib/webpack-cli.js
Expand Up @@ -3,14 +3,15 @@ const packageExists = require('./utils/package-exists');
const webpack = packageExists('webpack') ? require('webpack') : undefined;
const webpackMerge = require('webpack-merge');
const { writeFileSync } = require('fs');
const { options: coloretteOptions, yellow } = require('colorette');
const { yellow } = require('colorette');

const logger = require('./utils/logger');
const { core, groups, coreFlagMap } = require('./utils/cli-flags');
const argParser = require('./utils/arg-parser');
const assignFlagDefaults = require('./utils/flag-defaults');
const WebpackCLIPlugin = require('./plugins/WebpackCLIPlugin');
const promptInstallation = require('./utils/prompt-installation');
const { getStatsOptions } = require('./utils/stats-options');

// CLI arg resolvers
const handleConfigResolution = require('./groups/resolveConfig');
Expand Down Expand Up @@ -327,9 +328,9 @@ class WebpackCLI {
let options = this.compilerConfiguration;
let outputOptions = this.outputConfiguration;

const isRawOutput = typeof outputOptions.json === 'undefined';
const isJsonOutput = typeof outputOptions.json !== 'undefined';

if (isRawOutput) {
if (!isJsonOutput) {
const webpackCLIPlugin = new WebpackCLIPlugin({
progress: outputOptions.progress,
});
Expand Down Expand Up @@ -357,43 +358,28 @@ class WebpackCLI {
process.exitCode = 1;
}

const getStatsOptions = (stats) => {
// TODO remove after drop webpack@4
if (webpack.Stats && webpack.Stats.presetToOptions) {
if (!stats) {
stats = {};
} else if (typeof stats === 'boolean' || typeof stats === 'string') {
stats = webpack.Stats.presetToOptions(stats);
}
}

stats.colors = typeof stats.colors !== 'undefined' ? stats.colors : coloretteOptions.enabled;

return stats;
};

const getStatsOptionsFromCompiler = (compiler) => getStatsOptions(compiler.options ? compiler.options.stats : undefined);
if (isJsonOutput) {
process.nextTick(() => {
const statsOptions = compiler.compilers
? { children: compiler.compilers.map(getStatsOptions) }
: getStatsOptions(compiler);

const foundStats = compiler.compilers
? { children: compiler.compilers.map(getStatsOptionsFromCompiler) }
: getStatsOptionsFromCompiler(compiler);
const json = JSON.stringify(stats.toJson(statsOptions), null, 2);

if (outputOptions.json === true) {
process.stdout.write(JSON.stringify(stats.toJson(foundStats), null, 2) + '\n');
} else if (typeof outputOptions.json === 'string') {
const JSONStats = JSON.stringify(stats.toJson(foundStats), null, 2);
if (typeof outputOptions.json === 'string') {
try {
writeFileSync(outputOptions.json, json);

try {
writeFileSync(outputOptions.json, JSONStats);
logger.info(`stats are successfully stored as json to ${outputOptions.json}`);
} catch (error) {
logger.error(error);

logger.success(`stats are successfully stored as json to ${outputOptions.json}`);
} catch (error) {
logger.error(error);

process.exit(2);
}
} else {
logger.raw(`${stats.toString(foundStats)}`);
process.exit(2);
}
} else {
process.stdout.write(json + '\n');
}
});
}
};

Expand Down
8 changes: 5 additions & 3 deletions test/analyze/analyze-flag.test.js
@@ -1,16 +1,18 @@
'use strict';

const stripAnsi = require('strip-ansi');
const { runAndGetWatchProc } = require('../utils/test-utils');

describe('--analyze flag', () => {
it('should load webpack-bundle-analyzer plugin with --analyze flag', (done) => {
const proc = runAndGetWatchProc(__dirname, ['--analyze'], false, '', true);

proc.stdout.on('data', (chunk) => {
const data = chunk.toString();
const data = stripAnsi(chunk.toString());
const str = 'Webpack Bundle Analyzer is started at';

if (data.includes('Webpack Bundle Analyzer is started at')) {
expect(data).toContain('Webpack Bundle Analyzer is started at');
if (data.includes(str)) {
expect(data).toContain(str);

proc.kill();
done();
Expand Down