diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index 617bf45136d..6a9cb23965c 100644 --- a/declarations/WebpackOptions.d.ts +++ b/declarations/WebpackOptions.d.ts @@ -75,6 +75,16 @@ export type ExternalItem = * via the `definition` "ArrayOfStringValues". */ export type ArrayOfStringValues = string[]; +/** + * This interface was referenced by `WebpackOptions`'s JSON-Schema + * via the `definition` "FilterTypes". + */ +export type FilterTypes = FilterItemTypes | FilterItemTypes[]; +/** + * This interface was referenced by `WebpackOptions`'s JSON-Schema + * via the `definition` "FilterItemTypes". + */ +export type FilterItemTypes = RegExp | string | ((value: string) => boolean); /** * One or multiple rule conditions * @@ -235,16 +245,6 @@ export type WebpackPluginFunction = ( * via the `definition` "RuleSetRules". */ export type RuleSetRules = RuleSetRule[]; -/** - * This interface was referenced by `WebpackOptions`'s JSON-Schema - * via the `definition` "FilterTypes". - */ -export type FilterTypes = FilterItemTypes | FilterItemTypes[]; -/** - * This interface was referenced by `WebpackOptions`'s JSON-Schema - * via the `definition` "FilterItemTypes". - */ -export type FilterItemTypes = RegExp | string | Function; export interface WebpackOptions { /** @@ -293,6 +293,19 @@ export interface WebpackOptions { * Specify dependencies that shouldn't be resolved by webpack, but should become dependencies of the resulting bundle. The kind of the dependency depends on `output.libraryTarget`. */ externals?: Externals; + /** + * Options for infrastructure level logging + */ + infrastructureLogging?: { + /** + * Enable debug logging for specific loggers + */ + debug?: FilterTypes | boolean; + /** + * Log level + */ + level?: "none" | "error" | "warn" | "info" | "log" | "verbose"; + }; /** * Custom values available in the loader context. */ @@ -1343,6 +1356,18 @@ export interface StatsOptions { * add the hash of the compilation */ hash?: boolean; + /** + * add logging output + */ + logging?: boolean | ("none" | "error" | "warn" | "info" | "log" | "verbose"); + /** + * Include debug logging of specified loggers (i. e. for plugins or loaders). Filters can be Strings, RegExps or Functions + */ + loggingDebug?: FilterTypes | boolean; + /** + * add stack traces to logging output + */ + loggingTrace?: boolean; /** * Set the maximum number of modules to be shown */ diff --git a/lib/Compilation.js b/lib/Compilation.js index a56b0a9c4e2..9a77119b253 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -36,6 +36,8 @@ const SortableSet = require("./util/SortableSet"); const GraphHelpers = require("./GraphHelpers"); const ModuleDependency = require("./dependencies/ModuleDependency"); const compareLocations = require("./compareLocations"); +const { Logger, LogType } = require("./logging/Logger"); +const ErrorHelpers = require("./ErrorHelpers"); /** @typedef {import("./Module")} Module */ /** @typedef {import("./Compiler")} Compiler */ @@ -95,6 +97,14 @@ const compareLocations = require("./compareLocations"); * @property {DependenciesBlockVariable[]} variables */ +/** + * @typedef {Object} LogEntry + * @property {string} type + * @property {any[]} args + * @property {number} time + * @property {string[]=} trace + */ + /** * @param {Chunk} a first chunk to sort by id * @param {Chunk} b second chunk to sort by id @@ -384,6 +394,9 @@ class Compilation extends Tapable { "compilerIndex" ]), + /** @type {SyncBailHook} */ + log: new SyncBailHook(["origin", "logEntry"]), + // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ @@ -469,6 +482,8 @@ class Compilation extends Tapable { this.warnings = []; /** @type {Compilation[]} */ this.children = []; + /** @type {Map} */ + this.logging = new Map(); /** @type {Map} */ this.dependencyFactories = new Map(); /** @type {Map} */ @@ -499,6 +514,69 @@ class Compilation extends Tapable { return new Stats(this); } + /** + * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name + * @returns {Logger} a logger with that name + */ + getLogger(name) { + if (!name) { + throw new TypeError("Compilation.getLogger(name) called without a name"); + } + /** @type {LogEntry[] | undefined} */ + let logEntries; + return new Logger((type, args) => { + if (typeof name === "function") { + name = name(); + if (!name) { + throw new TypeError( + "Compilation.getLogger(name) called with a function not returning a name" + ); + } + } + let trace; + switch (type) { + case LogType.warn: + case LogType.error: + case LogType.trace: + trace = ErrorHelpers.cutOffLoaderExecution(new Error("Trace").stack) + .split("\n") + .slice(3); + break; + } + /** @type {LogEntry} */ + const logEntry = { + time: Date.now(), + type, + args, + trace + }; + if (this.hooks.log.call(name, logEntry) === undefined) { + if (logEntry.type === LogType.profileEnd) { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.profileEnd === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.profileEnd(`[${name}] ${logEntry.args[0]}`); + } + } + if (logEntries === undefined) { + logEntries = this.logging.get(name); + if (logEntries === undefined) { + logEntries = []; + this.logging.set(name, logEntries); + } + } + logEntries.push(logEntry); + if (logEntry.type === LogType.profile) { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.profile === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.profile(`[${name}] ${logEntry.args[0]}`); + } + } + } + }); + } + /** * @typedef {Object} AddModuleResult * @property {Module} module the added or existing module diff --git a/lib/Compiler.js b/lib/Compiler.js index 24121892166..97ae85c3437 100644 --- a/lib/Compiler.js +++ b/lib/Compiler.js @@ -27,6 +27,7 @@ const ResolverFactory = require("./ResolverFactory"); const RequestShortener = require("./RequestShortener"); const { makePathsRelative } = require("./util/identifier"); const ConcurrentCompilationError = require("./ConcurrentCompilationError"); +const { Logger } = require("./logging/Logger"); /** @typedef {import("../declarations/WebpackOptions").Entry} Entry */ /** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */ @@ -84,6 +85,9 @@ class Compiler extends Tapable { /** @type {SyncHook} */ watchClose: new SyncHook([]), + /** @type {SyncBailHook} */ + infrastructurelog: new SyncBailHook(["origin", "type", "args"]), + // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ @@ -137,6 +141,8 @@ class Compiler extends Tapable { /** @type {ResolverFactory} */ this.resolverFactory = new ResolverFactory(); + this.infrastructureLogger = undefined; + // TODO remove in webpack 5 this.resolvers = { normal: { @@ -196,6 +202,33 @@ class Compiler extends Tapable { this._assetEmittingWrittenFiles = new Map(); } + /** + * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name + * @returns {Logger} a logger with that name + */ + getInfrastructureLogger(name) { + if (!name) { + throw new TypeError( + "Compiler.getInfrastructureLogger(name) called without a name" + ); + } + return new Logger((type, args) => { + if (typeof name === "function") { + name = name(); + if (!name) { + throw new TypeError( + "Compiler.getInfrastructureLogger(name) called with a function not returning a name" + ); + } + } + if (this.hooks.infrastructurelog.call(name, type, args) === undefined) { + if (this.infrastructureLogger !== undefined) { + this.infrastructureLogger(name, type, args); + } + } + }); + } + watch(watchOptions, handler) { if (this.running) return handler(new ConcurrentCompilationError()); diff --git a/lib/NormalModule.js b/lib/NormalModule.js index ba8d31819f2..a6d9915378d 100644 --- a/lib/NormalModule.js +++ b/lib/NormalModule.js @@ -147,16 +147,20 @@ class NormalModule extends Module { createLoaderContext(resolver, options, compilation, fs) { const requestShortener = compilation.runtimeTemplate.requestShortener; + const getCurrentLoaderName = () => { + const currentLoader = this.getCurrentLoader(loaderContext); + if (!currentLoader) return "(not in loader scope)"; + return requestShortener.shorten(currentLoader.loader); + }; const loaderContext = { version: 2, emitWarning: warning => { if (!(warning instanceof Error)) { warning = new NonErrorEmittedError(warning); } - const currentLoader = this.getCurrentLoader(loaderContext); this.warnings.push( new ModuleWarning(this, warning, { - from: requestShortener.shorten(currentLoader.loader) + from: getCurrentLoaderName() }) ); }, @@ -164,13 +168,20 @@ class NormalModule extends Module { if (!(error instanceof Error)) { error = new NonErrorEmittedError(error); } - const currentLoader = this.getCurrentLoader(loaderContext); this.errors.push( new ModuleError(this, error, { - from: requestShortener.shorten(currentLoader.loader) + from: getCurrentLoaderName() }) ); }, + getLogger: name => { + const currentLoader = this.getCurrentLoader(loaderContext); + return compilation.getLogger(() => + [currentLoader && currentLoader.loader, name, this.identifier()] + .filter(Boolean) + .join("|") + ); + }, // TODO remove in webpack 5 exec: (code, filename) => { // @ts-ignore Argument of type 'this' is not assignable to parameter of type 'Module'. diff --git a/lib/Stats.js b/lib/Stats.js index 28dd0fba457..7a586cb4da7 100644 --- a/lib/Stats.js +++ b/lib/Stats.js @@ -9,6 +9,7 @@ const SizeFormatHelpers = require("./SizeFormatHelpers"); const formatLocation = require("./formatLocation"); const identifierUtils = require("./util/identifier"); const compareLocations = require("./compareLocations"); +const { LogType } = require("./logging/Logger"); const optionsOrFallback = (...args) => { let optionValues = []; @@ -198,6 +199,18 @@ class Stats { options.publicPath, !forToString ); + const showLogging = optionOrLocalFallback( + options.logging, + forToString ? "info" : true + ); + const showLoggingTrace = optionOrLocalFallback( + options.loggingTrace, + !forToString + ); + const loggingDebug = [] + .concat(optionsOrFallback(options.loggingDebug, [])) + .map(testAgainstGivenOption); + const excludeModules = [] .concat(optionsOrFallback(options.excludeModules, options.exclude, [])) .map(testAgainstGivenOption); @@ -699,6 +712,117 @@ class Stats { obj.filteredModules = compilation.modules.length - obj.modules.length; obj.modules.sort(sortByField(sortModules, obj.modules)); } + if (showLogging) { + const util = require("util"); + obj.logging = {}; + let acceptedTypes; + let collapsedGroups = false; + switch (showLogging) { + case "none": + acceptedTypes = new Set([]); + break; + case "error": + acceptedTypes = new Set([LogType.error]); + break; + case "warn": + acceptedTypes = new Set([LogType.error, LogType.warn]); + break; + case "info": + acceptedTypes = new Set([LogType.error, LogType.warn, LogType.info]); + break; + case true: + case "log": + acceptedTypes = new Set([ + LogType.error, + LogType.warn, + LogType.info, + LogType.log, + LogType.group, + LogType.groupEnd, + LogType.groupCollapsed, + LogType.clear + ]); + break; + case "verbose": + acceptedTypes = new Set([ + LogType.error, + LogType.warn, + LogType.info, + LogType.log, + LogType.group, + LogType.groupEnd, + LogType.groupCollapsed, + LogType.profile, + LogType.profileEnd, + LogType.time, + LogType.clear + ]); + collapsedGroups = true; + break; + } + for (const [origin, logEntries] of compilation.logging) { + const debugMode = loggingDebug.some(fn => fn(origin)); + let collapseCounter = 0; + let processedLogEntries = logEntries; + if (!debugMode) { + processedLogEntries = processedLogEntries.filter(entry => { + if (!acceptedTypes.has(entry.type)) return false; + if (!collapsedGroups) { + switch (entry.type) { + case LogType.groupCollapsed: + collapseCounter++; + return collapseCounter === 1; + case LogType.group: + if (collapseCounter > 0) collapseCounter++; + return collapseCounter === 0; + case LogType.groupEnd: + if (collapseCounter > 0) { + collapseCounter--; + return false; + } + return true; + default: + return collapseCounter === 0; + } + } + return true; + }); + } + processedLogEntries = processedLogEntries.map(entry => { + let message = undefined; + if (entry.type === LogType.time) { + message = `${entry.args[0]}: ${entry.args[1] * 1000 + + entry.args[2] / 1000000}ms`; + } else if (entry.args && entry.args.length > 0) { + message = util.format(entry.args[0], ...entry.args.slice(1)); + } + return { + type: + (debugMode || collapsedGroups) && + entry.type === LogType.groupCollapsed + ? LogType.group + : entry.type, + message, + trace: showLoggingTrace && entry.trace ? entry.trace : undefined + }; + }); + let name = identifierUtils + .makePathsRelative(context, origin, compilation.cache) + .replace(/\|/g, " "); + if (name in obj.logging) { + let i = 1; + while (`${name}#${i}` in obj.logging) { + i++; + } + name = `${name}#${i}`; + } + obj.logging[name] = { + entries: processedLogEntries, + filteredEntries: logEntries.length - processedLogEntries.length, + debug: debugMode + }; + } + } if (showChildren) { obj.children = compilation.children.map((child, idx) => { const childOptions = Stats.getChildOptions(options, idx); @@ -1305,6 +1429,95 @@ class Stats { processModulesList(obj, ""); + if (obj.logging) { + for (const origin of Object.keys(obj.logging)) { + const logData = obj.logging[origin]; + if (logData.entries.length > 0) { + newline(); + if (logData.debug) { + colors.red("DEBUG "); + } + colors.bold("LOG from " + origin); + newline(); + let indent = ""; + for (const entry of logData.entries) { + let color = colors.normal; + let prefix = ""; + switch (entry.type) { + case LogType.clear: + colors.normal(`${indent}-------`); + newline(); + continue; + case LogType.error: + color = colors.red; + prefix = " "; + break; + case LogType.warn: + color = colors.yellow; + prefix = " "; + break; + case LogType.info: + color = colors.green; + prefix = " "; + break; + case LogType.log: + color = colors.bold; + break; + case LogType.trace: + case LogType.debug: + color = colors.normal; + break; + case LogType.profile: + color = colors.magenta; + prefix = "

"; + break; + case LogType.profileEnd: + color = colors.magenta; + prefix = "

"; + break; + case LogType.time: + color = colors.magenta; + prefix = " "; + break; + case LogType.group: + color = colors.cyan; + break; + case LogType.groupCollapsed: + color = colors.cyan; + prefix = "<+> "; + break; + case LogType.groupEnd: + if (indent.length > 2) + indent = indent.slice(0, indent.length - 2); + continue; + } + if (entry.message) { + for (const line of entry.message.split("\n")) { + colors.normal(`${indent}${prefix}`); + color(line); + newline(); + } + } + if (entry.trace) { + for (const line of entry.trace) { + colors.normal(`${indent}| ${line}`); + newline(); + } + } + switch (entry.type) { + case LogType.group: + indent += " "; + break; + } + } + if (logData.filteredEntries) { + colors.normal(`+ ${logData.filteredEntries} hidden lines`); + newline(); + } + } + } + } + if (obj._showWarnings && obj.warnings) { for (const warning of obj.warnings) { newline(); @@ -1375,6 +1588,7 @@ class Stats { optimizationBailout: true, errorDetails: true, publicPath: true, + logging: "verbose", exclude: false, maxModules: Infinity }; @@ -1391,6 +1605,7 @@ class Stats { optimizationBailout: true, errorDetails: true, publicPath: true, + logging: true, exclude: false, maxModules: Infinity }; @@ -1400,19 +1615,22 @@ class Stats { modules: true, maxModules: 0, errors: true, - warnings: true + warnings: true, + logging: "warn" }; case "errors-only": return { all: false, errors: true, - moduleTrace: true + moduleTrace: true, + logging: "error" }; case "errors-warnings": return { all: false, errors: true, - warnings: true + warnings: true, + logging: "warn" }; default: return {}; diff --git a/lib/WebpackOptionsDefaulter.js b/lib/WebpackOptionsDefaulter.js index 53adbf3058b..c11529cb30e 100644 --- a/lib/WebpackOptionsDefaulter.js +++ b/lib/WebpackOptionsDefaulter.js @@ -363,6 +363,12 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { options.resolveLoader.plugins.length > 0 ); }); + + this.set("infrastructureLogging", "call", value => + Object.assign({}, value) + ); + this.set("infrastructureLogging.level", "info"); + this.set("infrastructureLogging.debug", false); } } diff --git a/lib/logging/Logger.js b/lib/logging/Logger.js new file mode 100644 index 00000000000..6bdaea0dbc3 --- /dev/null +++ b/lib/logging/Logger.js @@ -0,0 +1,126 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra + */ + +"use strict"; + +/** + * @enum {string} + */ +const LogType = Object.freeze({ + error: "error", // message, c style arguments + warn: "warn", // message, c style arguments + info: "info", // message, c style arguments + log: "log", // message, c style arguments + debug: "debug", // message, c style arguments + + trace: "trace", // no arguments + + group: "group", // [label] + groupCollapsed: "groupCollapsed", // [label] + groupEnd: "groupEnd", // [label] + + profile: "profile", // [profileName] + profileEnd: "profileEnd", // [profileName] + + time: "time", // name, time as [seconds, nanoseconds] + + clear: "clear" // no arguments +}); + +exports.LogType = LogType; + +/** @typedef {LogType} LogTypeEnum */ + +const LOG_SYMBOL = Symbol("webpack logger raw log method"); +const TIMERS_SYMBOL = Symbol("webpack logger times"); + +class WebpackLogger { + /** + * @param {function(LogType, any[]=): void} log log function + */ + constructor(log) { + this[LOG_SYMBOL] = log; + } + + error(...args) { + this[LOG_SYMBOL](LogType.error, args); + } + + warn(...args) { + this[LOG_SYMBOL](LogType.warn, args); + } + + info(...args) { + this[LOG_SYMBOL](LogType.info, args); + } + + log(...args) { + this[LOG_SYMBOL](LogType.log, args); + } + + debug(...args) { + this[LOG_SYMBOL](LogType.debug, args); + } + + assert(assertion, ...args) { + if (!assertion) { + this[LOG_SYMBOL](LogType.error, args); + } + } + + trace() { + this[LOG_SYMBOL](LogType.trace, ["Trace"]); + } + + clear() { + this[LOG_SYMBOL](LogType.clear); + } + + group(...args) { + this[LOG_SYMBOL](LogType.group, args); + } + + groupCollapsed(...args) { + this[LOG_SYMBOL](LogType.groupCollapsed, args); + } + + groupEnd(...args) { + this[LOG_SYMBOL](LogType.groupEnd, args); + } + + profile(label) { + this[LOG_SYMBOL](LogType.profile, [label]); + } + + profileEnd(label) { + this[LOG_SYMBOL](LogType.profileEnd, [label]); + } + + time(label) { + this[TIMERS_SYMBOL] = this[TIMERS_SYMBOL] || new Map(); + this[TIMERS_SYMBOL].set(label, process.hrtime()); + } + + timeLog(label) { + const prev = this[TIMERS_SYMBOL] && this[TIMERS_SYMBOL].get(label); + if (!prev) { + throw new Error(`No such label '${label}' for WebpackLogger.timeLog()`); + } + const time = process.hrtime(prev); + this[LOG_SYMBOL](LogType.time, [label, ...time]); + } + + timeEnd(label) { + const prev = this[TIMERS_SYMBOL] && this[TIMERS_SYMBOL].get(label); + if (!prev) { + throw new Error(`No such label '${label}' for WebpackLogger.timeEnd()`); + } + const time = process.hrtime(prev); + this[TIMERS_SYMBOL].delete(label); + this[LOG_SYMBOL](LogType.time, [label, ...time]); + } +} + +exports.Logger = WebpackLogger; diff --git a/lib/logging/createConsoleLogger.js b/lib/logging/createConsoleLogger.js new file mode 100644 index 00000000000..b6b49660d19 --- /dev/null +++ b/lib/logging/createConsoleLogger.js @@ -0,0 +1,188 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const { LogType } = require("./Logger"); + +/** @typedef {import("./Logger").LogTypeEnum} LogTypeEnum */ +/** @typedef {import("../../declarations/WebpackOptions").FilterTypes} FilterTypes */ +/** @typedef {import("../../declarations/WebpackOptions").FilterItemTypes} FilterItemTypes */ + +/** @typedef {function(string): boolean} FilterFunction */ + +/** + * @typedef {Object} LoggerOptions + * @property {false|true|"none"|"error"|"warn"|"info"|"log"|"verbose"} options.level loglevel + * @property {FilterTypes|boolean} options.debug filter for debug logging + */ + +/** + * @param {FilterItemTypes} item an input item + * @returns {FilterFunction} filter funtion + */ +const filterToFunction = item => { + if (typeof item === "string") { + const regExp = new RegExp( + `[\\\\/]${item.replace( + // eslint-disable-next-line no-useless-escape + /[-[\]{}()*+?.\\^$|]/g, + "\\$&" + )}([\\\\/]|$|!|\\?)` + ); + return ident => regExp.test(ident); + } + if (item && typeof item === "object" && typeof item.test === "function") { + return ident => item.test(ident); + } + if (typeof item === "function") { + return item; + } + if (typeof item === "boolean") { + return () => item; + } +}; + +/** + * @enum {number} */ +const LogLevel = { + none: 6, + false: 6, + error: 5, + warn: 4, + info: 3, + log: 2, + true: 2, + verbose: 1 +}; + +/** + * @param {LoggerOptions} options options object + * @returns {function(string, LogTypeEnum, any[]): void} logging function + */ +module.exports = ({ level = "info", debug = false }) => { + const debugFilters = + typeof debug === "boolean" + ? [() => debug] + : /** @type {FilterItemTypes[]} */ ([]) + .concat(debug) + .map(filterToFunction); + /** @type {number} */ + const loglevel = LogLevel[`${level}`] || 0; + + /** + * @param {string} name name of the logger + * @param {LogTypeEnum} type type of the log entry + * @param {any[]} args arguments of the log entry + * @returns {void} + */ + const logger = (name, type, args) => { + const labeledArgs = (prefix = "") => { + if (Array.isArray(args)) { + if (args.length > 0 && typeof args[0] === "string") { + return [`${prefix}[${name}] ${args[0]}`, ...args.slice(1)]; + } else { + return [`${prefix}[${name}]`, ...args]; + } + } else { + return []; + } + }; + const debug = debugFilters.some(f => f(name)); + switch (type) { + case LogType.debug: + if (!debug) return; + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.debug === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.debug(...labeledArgs()); + } else { + console.log(...labeledArgs()); + } + break; + case LogType.log: + if (!debug && loglevel > LogLevel.log) return; + console.log(...labeledArgs()); + break; + case LogType.info: + if (!debug && loglevel > LogLevel.info) return; + console.info(...labeledArgs(" ")); + break; + case LogType.warn: + if (!debug && loglevel > LogLevel.warn) return; + console.warn(...labeledArgs(" ")); + break; + case LogType.error: + if (!debug && loglevel > LogLevel.error) return; + console.error(...labeledArgs(" ")); + break; + case LogType.trace: + if (!debug) return; + console.trace(); + break; + case LogType.group: + if (!debug && loglevel > LogLevel.log) return; + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.group === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.group(...labeledArgs()); + } else { + console.log(...labeledArgs()); + } + break; + case LogType.groupCollapsed: + if (!debug && loglevel > LogLevel.log) return; + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.groupCollapsed === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.groupCollapsed(...labeledArgs()); + } else { + console.log(...labeledArgs(" ")); + } + break; + case LogType.groupEnd: + if (!debug && loglevel > LogLevel.log) return; + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.groupEnd === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.groupEnd(); + } else { + console.log(...labeledArgs(" ")); + } + break; + case LogType.time: + if (!debug && loglevel > LogLevel.log) return; + console.log( + `[${name}] ${args[0]}: ${args[1] * 1000 + args[2] / 1000000}ms` + ); + break; + case LogType.profile: + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.profile === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.profile(...labeledArgs()); + } + break; + case LogType.profileEnd: + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.profileEnd === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.profileEnd(...labeledArgs()); + } + break; + case LogType.clear: + if (!debug && loglevel > LogLevel.log) return; + // eslint-disable-next-line node/no-unsupported-features/node-builtins + if (typeof console.clear === "function") { + // eslint-disable-next-line node/no-unsupported-features/node-builtins + console.clear(); + } + break; + default: + throw new Error(`Unexpected LogType ${type}`); + } + }; + return logger; +}; diff --git a/lib/logging/runtime.js b/lib/logging/runtime.js new file mode 100644 index 00000000000..7b4bbf7b22f --- /dev/null +++ b/lib/logging/runtime.js @@ -0,0 +1,35 @@ +const SyncBailHook = require("tapable/lib/SyncBailHook"); +const { Logger } = require("./Logger"); +const createConsoleLogger = require("./createConsoleLogger"); + +/** @type {createConsoleLogger.LoggerOptions} */ +let currentDefaultLoggerOptions = { + level: "info", + debug: false +}; +let currentDefaultLogger = createConsoleLogger(currentDefaultLoggerOptions); + +/** + * @param {string} name name of the logger + * @returns {Logger} a logger + */ +exports.getLogger = name => { + return new Logger((type, args) => { + if (exports.hooks.log.call(name, type, args) === undefined) { + currentDefaultLogger(name, type, args); + } + }); +}; + +/** + * @param {createConsoleLogger.LoggerOptions} options new options, merge with old options + * @returns {void} + */ +exports.configureDefaultLogger = options => { + Object.assign(currentDefaultLoggerOptions, options); + currentDefaultLogger = createConsoleLogger(currentDefaultLoggerOptions); +}; + +exports.hooks = { + log: new SyncBailHook(["origin", "type", "args"]) +}; diff --git a/lib/node/NodeEnvironmentPlugin.js b/lib/node/NodeEnvironmentPlugin.js index 3f76bffa533..8a236ea85a5 100644 --- a/lib/node/NodeEnvironmentPlugin.js +++ b/lib/node/NodeEnvironmentPlugin.js @@ -8,9 +8,23 @@ const NodeWatchFileSystem = require("./NodeWatchFileSystem"); const NodeOutputFileSystem = require("./NodeOutputFileSystem"); const NodeJsInputFileSystem = require("enhanced-resolve/lib/NodeJsInputFileSystem"); const CachedInputFileSystem = require("enhanced-resolve/lib/CachedInputFileSystem"); +const createConsoleLogger = require("../logging/createConsoleLogger"); class NodeEnvironmentPlugin { + constructor(options) { + this.options = options || {}; + } + apply(compiler) { + compiler.infrastructureLogger = createConsoleLogger( + Object.assign( + { + level: "info", + debug: false + }, + this.options.infrastructureLogging + ) + ); compiler.inputFileSystem = new CachedInputFileSystem( new NodeJsInputFileSystem(), 60000 diff --git a/lib/webpack.js b/lib/webpack.js index 3e121f41fa9..80f810b35f2 100644 --- a/lib/webpack.js +++ b/lib/webpack.js @@ -40,7 +40,9 @@ const webpack = (options, callback) => { compiler = new Compiler(options.context); compiler.options = options; - new NodeEnvironmentPlugin().apply(compiler); + new NodeEnvironmentPlugin({ + infrastructureLogging: options.infrastructureLogging + }).apply(compiler); if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { diff --git a/schemas/WebpackOptions.json b/schemas/WebpackOptions.json index 81ad909373a..4a6b31b2a27 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -166,7 +166,7 @@ }, { "instanceof": "Function", - "tsType": "Function" + "tsType": "((value: string) => boolean)" } ] }, @@ -1822,6 +1822,35 @@ "description": "add the hash of the compilation", "type": "boolean" }, + "logging": { + "description": "add logging output", + "anyOf": [ + { + "description": "enable/disable logging output (true: shows normal logging output, loglevel: log)", + "type": "boolean" + }, + { + "description": "specify log level of logging output", + "enum": ["none", "error", "warn", "info", "log", "verbose"] + } + ] + }, + "loggingDebug": { + "description": "Include debug logging of specified loggers (i. e. for plugins or loaders). Filters can be Strings, RegExps or Functions", + "anyOf": [ + { + "$ref": "#/definitions/FilterTypes" + }, + { + "description": "Enable/Disable debug logging for all loggers", + "type": "boolean" + } + ] + }, + "loggingTrace": { + "description": "add stack traces to logging output", + "type": "boolean" + }, "maxModules": { "description": "Set the maximum number of modules to be shown", "type": "number" @@ -1996,6 +2025,29 @@ } ] }, + "infrastructureLogging": { + "description": "Options for infrastructure level logging", + "type": "object", + "additionalProperties": false, + "properties": { + "debug": { + "description": "Enable debug logging for specific loggers", + "anyOf": [ + { + "$ref": "#/definitions/FilterTypes" + }, + { + "description": "Enable/Disable debug logging for all loggers", + "type": "boolean" + } + ] + }, + "level": { + "description": "Log level", + "enum": ["none", "error", "warn", "info", "log", "verbose"] + } + } + }, "loader": { "description": "Custom values available in the loader context.", "type": "object" diff --git a/test/Compiler.test.js b/test/Compiler.test.js index c241c9c088c..ba7dfa617d6 100644 --- a/test/Compiler.test.js +++ b/test/Compiler.test.js @@ -514,4 +514,152 @@ describe("Compiler", () => { done(); }); }); + describe("infrastructure logging", () => { + const CONSOLE_METHODS = [ + "error", + "warn", + "info", + "log", + "debug", + "trace", + "profile", + "profileEnd", + "group", + "groupEnd", + "groupCollapsed" + ]; + const spies = {}; + beforeEach(() => { + for (const method of CONSOLE_METHODS) { + if (console[method]) { + spies[method] = jest.spyOn(console, method).mockImplementation(); + } + } + }); + afterEach(() => { + for (const method in spies) { + spies[method].mockRestore(); + delete spies[method]; + } + }); + class MyPlugin { + apply(compiler) { + const logger = compiler.getInfrastructureLogger("MyPlugin"); + logger.time("Time"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + logger.timeEnd("Time"); + } + } + it("should log to the console (verbose)", done => { + const compiler = webpack({ + context: path.join(__dirname, "fixtures"), + entry: "./a", + output: { + path: "/", + filename: "bundle.js" + }, + infrastructureLogging: { + level: "verbose" + }, + plugins: [new MyPlugin()] + }); + compiler.outputFileSystem = new MemoryFs(); + compiler.run((err, stats) => { + expect(spies.group).toHaveBeenCalledTimes(1); + expect(spies.group).toHaveBeenCalledWith("[MyPlugin] Group"); + expect(spies.groupCollapsed).toHaveBeenCalledTimes(1); + expect(spies.groupCollapsed).toHaveBeenCalledWith( + "[MyPlugin] Collaped group" + ); + expect(spies.error).toHaveBeenCalledTimes(1); + expect(spies.error).toHaveBeenCalledWith(" [MyPlugin] Error"); + expect(spies.warn).toHaveBeenCalledTimes(1); + expect(spies.warn).toHaveBeenCalledWith(" [MyPlugin] Warning"); + expect(spies.info).toHaveBeenCalledTimes(1); + expect(spies.info).toHaveBeenCalledWith(" [MyPlugin] Info"); + expect(spies.log).toHaveBeenCalledTimes(3); + expect(spies.log).toHaveBeenCalledWith("[MyPlugin] Log"); + expect(spies.log).toHaveBeenCalledWith( + "[MyPlugin] Log inside collapsed group" + ); + expect(spies.debug).toHaveBeenCalledTimes(0); + expect(spies.groupEnd).toHaveBeenCalledTimes(2); + done(); + }); + }); + it("should log to the console (debug mode)", done => { + const compiler = webpack({ + context: path.join(__dirname, "fixtures"), + entry: "./a", + output: { + path: "/", + filename: "bundle.js" + }, + infrastructureLogging: { + level: "error", + debug: /MyPlugin/ + }, + plugins: [new MyPlugin()] + }); + compiler.outputFileSystem = new MemoryFs(); + compiler.run((err, stats) => { + expect(spies.group).toHaveBeenCalledTimes(1); + expect(spies.group).toHaveBeenCalledWith("[MyPlugin] Group"); + expect(spies.groupCollapsed).toHaveBeenCalledTimes(1); + expect(spies.groupCollapsed).toHaveBeenCalledWith( + "[MyPlugin] Collaped group" + ); + expect(spies.error).toHaveBeenCalledTimes(1); + expect(spies.error).toHaveBeenCalledWith(" [MyPlugin] Error"); + expect(spies.warn).toHaveBeenCalledTimes(1); + expect(spies.warn).toHaveBeenCalledWith(" [MyPlugin] Warning"); + expect(spies.info).toHaveBeenCalledTimes(1); + expect(spies.info).toHaveBeenCalledWith(" [MyPlugin] Info"); + expect(spies.log).toHaveBeenCalledTimes(3); + expect(spies.log).toHaveBeenCalledWith("[MyPlugin] Log"); + expect(spies.log).toHaveBeenCalledWith( + "[MyPlugin] Log inside collapsed group" + ); + expect(spies.debug).toHaveBeenCalledTimes(1); + expect(spies.debug).toHaveBeenCalledWith("[MyPlugin] Debug"); + expect(spies.groupEnd).toHaveBeenCalledTimes(2); + done(); + }); + }); + it("should log to the console (none)", done => { + const compiler = webpack({ + context: path.join(__dirname, "fixtures"), + entry: "./a", + output: { + path: "/", + filename: "bundle.js" + }, + infrastructureLogging: { + level: "none" + }, + plugins: [new MyPlugin()] + }); + compiler.outputFileSystem = new MemoryFs(); + compiler.run((err, stats) => { + expect(spies.group).toHaveBeenCalledTimes(0); + expect(spies.groupCollapsed).toHaveBeenCalledTimes(0); + expect(spies.error).toHaveBeenCalledTimes(0); + expect(spies.warn).toHaveBeenCalledTimes(0); + expect(spies.info).toHaveBeenCalledTimes(0); + expect(spies.log).toHaveBeenCalledTimes(0); + expect(spies.debug).toHaveBeenCalledTimes(0); + expect(spies.groupEnd).toHaveBeenCalledTimes(0); + done(); + }); + }); + }); }); diff --git a/test/Stats.unittest.js b/test/Stats.unittest.js index 8e75cd60e1d..b8689b74959 100644 --- a/test/Stats.unittest.js +++ b/test/Stats.unittest.js @@ -11,6 +11,7 @@ describe("Stats", () => { children: [], errors: ["firstError"], hash: "1234", + logging: new Map(), compiler: { context: "" } @@ -30,6 +31,7 @@ describe("Stats", () => { children: [], errors: ["firstError"], hash: "1234", + logging: new Map(), compiler: { context: "" } @@ -41,6 +43,7 @@ describe("Stats", () => { children: [], warnings: ["firstError"], hash: "1234", + logging: new Map(), compiler: { context: "" } @@ -54,6 +57,7 @@ describe("Stats", () => { children: [], errors: [], hash: "1234", + logging: new Map(), compiler: { context: "" } @@ -65,6 +69,7 @@ describe("Stats", () => { children: [], warnings: [], hash: "1234", + logging: new Map(), compiler: { context: "" } @@ -123,6 +128,7 @@ describe("Stats", () => { }, getPublicPath: () => "path" }, + logging: new Map(), compiler: { context: "" } @@ -149,6 +155,7 @@ describe("Stats", () => { }, getPublicPath: () => "path" }, + logging: new Map(), compiler: { context: "" } @@ -165,6 +172,7 @@ describe("Stats", () => { filteredModules: 0, errors: [], hash: "1234", + logging: {}, modules: [], outputPath: "/", publicPath: "path", diff --git a/test/StatsTestCases.test.js b/test/StatsTestCases.test.js index f0ca3ae0e6e..4ab83467937 100644 --- a/test/StatsTestCases.test.js +++ b/test/StatsTestCases.test.js @@ -7,6 +7,15 @@ const fs = require("fs"); const webpack = require("../lib/webpack"); const Stats = require("../lib/Stats"); +/** + * Escapes regular expression metacharacters + * @param {string} str String to quote + * @returns {string} Escaped string + */ +const quotemeta = str => { + return str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&"); +}; + const base = path.join(__dirname, "statsCases"); const outputBase = path.join(__dirname, "js", "stats"); const tests = fs @@ -104,7 +113,7 @@ describe("StatsTestCases", () => { if (!hasColorSetting) { actual = actual .replace(/\u001b\[[0-9;]*m/g, "") - .replace(/[0-9]+(\s?ms)/g, "X$1") + .replace(/[.0-9]+(\s?ms)/g, "X$1") .replace( /^(\s*Built at:) (.*)$/gm, "$1 Thu Jan 01 1970 00:00:00 GMT" @@ -115,7 +124,7 @@ describe("StatsTestCases", () => { .replace(/\u001b\[1m/g, "") .replace(/\u001b\[39m\u001b\[22m/g, "") .replace(/\u001b\[([0-9;]*)m/g, "") - .replace(/[0-9]+(<\/CLR>)?(\s?ms)/g, "X$1$2") + .replace(/[.0-9]+(<\/CLR>)?(\s?ms)/g, "X$1$2") .replace( /^(\s*Built at:) (.*)$/gm, "$1 Thu Jan 01 1970 00:00:00 GMT" @@ -124,7 +133,11 @@ describe("StatsTestCases", () => { actual = actual .replace(/\r\n?/g, "\n") .replace(/[\t ]*Version:.+\n/g, "") - .replace(path.join(base, testName), "Xdir/" + testName) + .replace( + new RegExp(quotemeta(path.join(base, testName)), "g"), + "Xdir/" + testName + ) + .replace(/(\w)\\(\w)/g, "$1/$2") .replace(/ dependencies:Xms/g, ""); expect(actual).toMatchSnapshot(); done(); diff --git a/test/Validation.test.js b/test/Validation.test.js index 4d86a8de76f..448b62b34f3 100644 --- a/test/Validation.test.js +++ b/test/Validation.test.js @@ -193,7 +193,7 @@ describe("Validation", () => { expect(msg).toMatchInlineSnapshot(` "Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - configuration has an unknown property 'postcss'. These properties are valid: - object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, externals?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, serve?, stats?, target?, watch?, watchOptions? } + object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry?, externals?, infrastructureLogging?, loader?, mode?, module?, name?, node?, optimization?, output?, parallelism?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, serve?, stats?, target?, watch?, watchOptions? } For typos: please correct them. For loader options: webpack >= v2.0.0 no longer allows custom properties in configuration. Loaders should be updated to allow passing options via loader options in module.rules. diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index 166b50555a1..41bf722fdf9 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -1235,6 +1235,42 @@ Child 4 chunks: [5] ./e.js 22 bytes {3} [built]" `; +exports[`StatsTestCases should print correct stats for logging 1`] = ` +"Hash: aa5b75cccf66cd9b1ffa +Time: Xms +Built at: Thu Jan 01 1970 00:00:00 GMT + Asset Size Chunks Chunk Names +main.js 3.57 KiB 0 [emitted] main +Entrypoint main = main.js +[0] ./index.js 0 bytes {0} [built] + +LOG from MyPlugin + Plugin is now active +<+> Nested ++ 3 hidden lines + +DEBUG LOG from node_modules/custom-loader/index.js node_modules/custom-loader/index.js!index.js + An error +| at Object..module.exports (Xdir/logging/node_modules/custom-loader/index.js:5:9) + A warning +| at Object..module.exports (Xdir/logging/node_modules/custom-loader/index.js:6:9) +Unimportant + Info message + Just log + Just debug + Measure: Xms + Nested + Log inside collapsed group + Trace + | at Object..module.exports (Xdir/logging/node_modules/custom-loader/index.js:15:9) + Measure: Xms + ------- + After clear + +DEBUG LOG from node_modules/custom-loader/index.js Named Logger node_modules/custom-loader/index.js!index.js +Message with named logger" +`; + exports[`StatsTestCases should print correct stats for max-modules 1`] = ` "Hash: d0b29852af8ccc4949b7 Time: Xms @@ -1903,21 +1939,47 @@ chunk {3} 3.js 54 bytes <{0}> >{1}< [rendered] [4] ./d.js 22 bytes {1} [depth 2] [built] ModuleConcatenation bailout: Module is not an ECMAScript module [5] ./e.js 22 bytes {1} [depth 2] [built] - ModuleConcatenation bailout: Module is not an ECMAScript module" + ModuleConcatenation bailout: Module is not an ECMAScript module + +LOG from MyPlugin +Group + Error + Warning + Info + Log + <+> Collaped group ++ 3 hidden lines" `; exports[`StatsTestCases should print correct stats for preset-errors-only 1`] = `""`; exports[`StatsTestCases should print correct stats for preset-errors-only-error 1`] = ` " +LOG from MyPlugin + Error ++ 9 hidden lines + ERROR in ./index.js Module not found: Error: Can't resolve 'does-not-exist' in 'Xdir/preset-errors-only-error' @ ./index.js 1:0-25" `; -exports[`StatsTestCases should print correct stats for preset-errors-warnings 1`] = `""`; +exports[`StatsTestCases should print correct stats for preset-errors-warnings 1`] = ` +" +LOG from MyPlugin + Error + Warning ++ 8 hidden lines" +`; -exports[`StatsTestCases should print correct stats for preset-minimal 1`] = `" 6 modules"`; +exports[`StatsTestCases should print correct stats for preset-minimal 1`] = ` +" 6 modules + +LOG from MyPlugin + Error + Warning ++ 8 hidden lines" +`; exports[`StatsTestCases should print correct stats for preset-minimal-simple 1`] = `" 1 module"`; @@ -1950,7 +2012,13 @@ Entrypoint main = main.js [2] ./b.js 22 bytes {2} [built] [3] ./c.js 54 bytes {3} [built] [4] ./d.js 22 bytes {1} [built] -[5] ./e.js 22 bytes {1} [built]" +[5] ./e.js 22 bytes {1} [built] + +LOG from MyPlugin + Error + Warning + Info ++ 7 hidden lines" `; exports[`StatsTestCases should print correct stats for preset-normal-performance 1`] = ` @@ -2054,7 +2122,17 @@ chunk {3} 3.js 54 bytes <{0}> >{1}< [rendered] [3] ./c.js 54 bytes {3} [depth 1] [built] ModuleConcatenation bailout: Module is not an ECMAScript module amd require ./c [0] ./index.js 3:0-16 - [0] Xms -> factory:Xms building:Xms = Xms" + [0] Xms -> factory:Xms building:Xms = Xms + +LOG from MyPlugin +Group + Error + Warning + Info + Log + Collaped group + Log inside collapsed group ++ 1 hidden lines" `; exports[`StatsTestCases should print correct stats for resolve-plugin-context 1`] = ` diff --git a/test/statsCases/logging/index.js b/test/statsCases/logging/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/statsCases/logging/node_modules/custom-loader/index.js b/test/statsCases/logging/node_modules/custom-loader/index.js new file mode 100644 index 00000000000..f59e88aae18 --- /dev/null +++ b/test/statsCases/logging/node_modules/custom-loader/index.js @@ -0,0 +1,21 @@ +/* eslint-disable node/no-unsupported-features/node-builtins */ +module.exports = function(source) { + const logger = this.getLogger ? this.getLogger() : console; + logger.time("Measure"); + logger.error("An error"); + logger.warn("A %s", "warning"); + logger.group("Unimportant"); + logger.info("Info message"); + logger.log("Just log"); + logger.debug("Just debug"); + logger.timeLog("Measure"); + logger.groupCollapsed("Nested"); + logger.log("Log inside collapsed group"); + logger.groupEnd("Nested"); + logger.trace(); + logger.timeEnd("Measure"); + logger.clear(); + logger.log("After clear"); + this.getLogger("Named Logger").debug("Message with named logger"); + return source; +}; diff --git a/test/statsCases/logging/webpack.config.js b/test/statsCases/logging/webpack.config.js new file mode 100644 index 00000000000..ce49b7dc908 --- /dev/null +++ b/test/statsCases/logging/webpack.config.js @@ -0,0 +1,36 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.info("Plugin is now active"); + logger.debug("Debug message should not be visible"); + logger.groupCollapsed("Nested"); + logger.log("Log inside collapsed group"); + logger.groupEnd("Nested"); + + const otherLogger = compilation.getLogger("MyOtherPlugin"); + otherLogger.debug("debug message only"); + }); + } +} + +module.exports = { + mode: "production", + entry: "./index", + performance: false, + module: { + rules: [ + { + test: /index\.js$/, + use: "custom-loader" + } + ] + }, + plugins: [new MyPlugin()], + stats: { + colors: true, + logging: true, + loggingDebug: "custom-loader", + loggingTrace: true + } +}; diff --git a/test/statsCases/preset-detailed/webpack.config.js b/test/statsCases/preset-detailed/webpack.config.js index 2912acd3e4f..69502271482 100644 --- a/test/statsCases/preset-detailed/webpack.config.js +++ b/test/statsCases/preset-detailed/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: "detailed" + stats: "detailed", + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-errors-only-error/webpack.config.js b/test/statsCases/preset-errors-only-error/webpack.config.js index 7f65a605240..2ac28317527 100644 --- a/test/statsCases/preset-errors-only-error/webpack.config.js +++ b/test/statsCases/preset-errors-only-error/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: "errors-only" + stats: "errors-only", + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-errors-warnings/webpack.config.js b/test/statsCases/preset-errors-warnings/webpack.config.js index 5c54a2307dc..038aa982fe9 100644 --- a/test/statsCases/preset-errors-warnings/webpack.config.js +++ b/test/statsCases/preset-errors-warnings/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: "errors-warnings" + stats: "errors-warnings", + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-minimal/webpack.config.js b/test/statsCases/preset-minimal/webpack.config.js index 53931799cc0..45e664f3508 100644 --- a/test/statsCases/preset-minimal/webpack.config.js +++ b/test/statsCases/preset-minimal/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: "minimal" + stats: "minimal", + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-none/webpack.config.js b/test/statsCases/preset-none/webpack.config.js index e99589235da..0a3d5be8ffb 100644 --- a/test/statsCases/preset-none/webpack.config.js +++ b/test/statsCases/preset-none/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: false + stats: false, + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-normal/webpack.config.js b/test/statsCases/preset-normal/webpack.config.js index 0bd5f398a97..5d85c8aabeb 100644 --- a/test/statsCases/preset-normal/webpack.config.js +++ b/test/statsCases/preset-normal/webpack.config.js @@ -1,5 +1,24 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", - stats: "normal" + stats: "normal", + plugins: [new MyPlugin()] }; diff --git a/test/statsCases/preset-verbose/webpack.config.js b/test/statsCases/preset-verbose/webpack.config.js index c44f313ee8d..e4d57fe96ae 100644 --- a/test/statsCases/preset-verbose/webpack.config.js +++ b/test/statsCases/preset-verbose/webpack.config.js @@ -1,6 +1,25 @@ +class MyPlugin { + apply(compiler) { + compiler.hooks.compilation.tap("MyPlugin", compilation => { + const logger = compilation.getLogger("MyPlugin"); + logger.group("Group"); + logger.error("Error"); + logger.warn("Warning"); + logger.info("Info"); + logger.log("Log"); + logger.debug("Debug"); + logger.groupCollapsed("Collaped group"); + logger.log("Log inside collapsed group"); + logger.groupEnd(); + logger.groupEnd(); + }); + } +} + module.exports = { mode: "production", entry: "./index", profile: true, - stats: "verbose" + stats: "verbose", + plugins: [new MyPlugin()] };