diff --git a/declarations/WebpackOptions.d.ts b/declarations/WebpackOptions.d.ts index 000396370..54c4ac52a 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 * @@ -256,16 +266,6 @@ export type OptimizationSplitChunksSizes = */ [k: string]: number; }; -/** - * 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 { /** @@ -314,6 +314,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. */ @@ -1435,6 +1448,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 92e9be128..32bd1f2f0 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -23,6 +23,7 @@ const ChunkRenderError = require("./ChunkRenderError"); const ChunkTemplate = require("./ChunkTemplate"); const DependencyTemplates = require("./DependencyTemplates"); const Entrypoint = require("./Entrypoint"); +const ErrorHelpers = require("./ErrorHelpers"); const FileSystemInfo = require("./FileSystemInfo"); const { connectChunkGroupAndChunk, @@ -41,6 +42,7 @@ const RuntimeGlobals = require("./RuntimeGlobals"); const RuntimeTemplate = require("./RuntimeTemplate"); const Stats = require("./Stats"); const WebpackError = require("./WebpackError"); +const { Logger, LogType } = require("./logging/Logger"); const StatsFactory = require("./stats/StatsFactory"); const StatsPrinter = require("./stats/StatsPrinter"); const AsyncQueue = require("./util/AsyncQueue"); @@ -127,6 +129,14 @@ const { arrayToSetDeprecation } = require("./util/deprecation"); * @property {(Record string>)=} contentHashWithLength */ +/** + * @typedef {Object} LogEntry + * @property {string} type + * @property {any[]} args + * @property {number} time + * @property {string[]=} trace + */ + /** * @typedef {Object} ModulePathData * @property {string|number} id @@ -405,6 +415,9 @@ class Compilation { "compilerIndex" ]), + /** @type {SyncBailHook<[string, LogEntry], true>} */ + log: new SyncBailHook(["origin", "logEntry"]), + /** @type {HookMap>} */ statsPreset: new HookMap(() => new SyncHook(["options", "context"])), /** @type {SyncHook<[Object, Object]>} */ @@ -520,6 +533,8 @@ class Compilation { this.warnings = []; /** @type {Compilation[]} */ this.children = []; + /** @type {Map} */ + this.logging = new Map(); /** @type {Map} */ this.dependencyFactories = new Map(); /** @type {DependencyTemplates} */ @@ -582,6 +597,69 @@ class Compilation { return statsPrinter; } + /** + * @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]}`); + } + } + } + }); + } + /** * @param {Module} module module to be added that was created * @param {ModuleCallback} callback returns the module in the compilation, diff --git a/lib/Compiler.js b/lib/Compiler.js index 39dedf346..689a00d0e 100644 --- a/lib/Compiler.js +++ b/lib/Compiler.js @@ -24,6 +24,7 @@ const RequestShortener = require("./RequestShortener"); const ResolverFactory = require("./ResolverFactory"); const Stats = require("./Stats"); const Watching = require("./Watching"); +const { Logger } = require("./logging/Logger"); const { join, dirname, mkdirp } = require("./util/fs"); const { makePathsRelative } = require("./util/identifier"); @@ -133,6 +134,9 @@ class Compiler { /** @type {SyncHook<[]>} */ watchClose: new SyncHook([]), + /** @type {SyncBailHook<[string, string, any[]], true>} */ + infrastructurelog: new SyncBailHook(["origin", "type", "args"]), + // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook<[]>} */ @@ -178,6 +182,8 @@ class Compiler { /** @type {ResolverFactory} */ this.resolverFactory = new ResolverFactory(); + this.infrastructureLogger = undefined; + /** @type {WebpackOptions} */ this.options = /** @type {WebpackOptions} */ ({}); @@ -201,6 +207,33 @@ class Compiler { 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); + } + } + }); + } + /** * @param {WatchOptions} watchOptions the watcher's options * @param {Callback} handler signals when the call finishes diff --git a/lib/NormalModule.js b/lib/NormalModule.js index 28f60c8bf..7436d6cc2 100644 --- a/lib/NormalModule.js +++ b/lib/NormalModule.js @@ -248,16 +248,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(warning, { - from: requestShortener.shorten(currentLoader.loader) + from: getCurrentLoaderName() }) ); }, @@ -265,13 +269,20 @@ class NormalModule extends Module { if (!(error instanceof Error)) { error = new NonErrorEmittedError(error); } - const currentLoader = this.getCurrentLoader(loaderContext); this.errors.push( new ModuleError(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("|") + ); + }, resolve(context, request, callback) { resolver.resolve({}, context, request, {}, callback); }, diff --git a/lib/WebpackOptionsDefaulter.js b/lib/WebpackOptionsDefaulter.js index da5d82b4f..133a49894 100644 --- a/lib/WebpackOptionsDefaulter.js +++ b/lib/WebpackOptionsDefaulter.js @@ -443,6 +443,12 @@ class WebpackOptionsDefaulter extends OptionsDefaulter { this.set("resolveLoader.mainFields", ["loader", "main"]); this.set("resolveLoader.extensions", [".js"]); this.set("resolveLoader.mainFiles", ["index"]); + + 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 000000000..2382d7ac6 --- /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 000000000..eb4fe5574 --- /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("../../declarations/WebpackOptions").FilterItemTypes} FilterItemTypes */ +/** @typedef {import("../../declarations/WebpackOptions").FilterTypes} FilterTypes */ +/** @typedef {import("./Logger").LogTypeEnum} LogTypeEnum */ + +/** @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 000000000..c9d56383f --- /dev/null +++ b/lib/logging/runtime.js @@ -0,0 +1,42 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +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 6bcd02748..ddde3b5b4 100644 --- a/lib/node/NodeEnvironmentPlugin.js +++ b/lib/node/NodeEnvironmentPlugin.js @@ -7,16 +7,30 @@ const CachedInputFileSystem = require("enhanced-resolve/lib/CachedInputFileSystem"); const fs = require("graceful-fs"); +const createConsoleLogger = require("../logging/createConsoleLogger"); const NodeWatchFileSystem = require("./NodeWatchFileSystem"); /** @typedef {import("../Compiler")} Compiler */ class NodeEnvironmentPlugin { + constructor(options) { + this.options = options || {}; + } + /** * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { + compiler.infrastructureLogger = createConsoleLogger( + Object.assign( + { + level: "info", + debug: false + }, + this.options.infrastructureLogging + ) + ); compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000); const inputFileSystem = compiler.inputFileSystem; compiler.outputFileSystem = fs; diff --git a/lib/stats/DefaultStatsFactoryPlugin.js b/lib/stats/DefaultStatsFactoryPlugin.js index 1ceeeba91..8c71db13d 100644 --- a/lib/stats/DefaultStatsFactoryPlugin.js +++ b/lib/stats/DefaultStatsFactoryPlugin.js @@ -6,6 +6,7 @@ "use strict"; const formatLocation = require("../formatLocation"); +const { LogType } = require("../logging/Logger"); const AggressiveSplittingPlugin = require("../optimize/AggressiveSplittingPlugin"); const ConcatenatedModule = require("../optimize/ConcatenatedModule"); const SizeLimitsPlugin = require("../performance/SizeLimitsPlugin"); @@ -19,6 +20,7 @@ const { compareModulesById, compareModulesByIdOrIdentifier } = require("../util/comparators"); +const identifierUtils = require("../util/identifier"); /** @typedef {import("webpack-sources").Source} Source */ /** @typedef {import("../Chunk")} Chunk */ @@ -46,6 +48,7 @@ const { /** * @typedef {Object} UsualOptions + * @property {string} context * @property {RequestShortener} requestShortener * @property {string} chunksSort * @property {string} modulesSort @@ -54,6 +57,9 @@ const { * @property {Function[]} excludeModules * @property {Function[]} warningsFilter * @property {number} maxModules + * @property {false|"none"|"error"|"warn"|"info"|"log"|"verbose"} logging + * @property {Function[]} loggingDebug + * @property {boolean} loggingTrace * @property {any} _env */ @@ -280,6 +286,128 @@ const SIMPLE_EXTRACTORS = { context ); }, + logging: (object, compilation, _context, options, factory) => { + const util = require("util"); + const { loggingDebug, loggingTrace, context } = options; + object.logging = {}; + let acceptedTypes; + let collapsedGroups = false; + switch (options.logging) { + 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 "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; + } + let depthInCollapsedGroup = 0; + for (const [origin, logEntries] of compilation.logging) { + const debugMode = loggingDebug.some(fn => fn(origin)); + const groupStack = []; + const rootList = []; + let currentList = rootList; + let processedLogEntries = 0; + for (const entry of logEntries) { + let type = entry.type; + if (!debugMode && !acceptedTypes.has(type)) continue; + + // Expand groups in verbose and debug modes + if (type === LogType.groupCollapsed && (debugMode || collapsedGroups)) + type = LogType.group; + + if (depthInCollapsedGroup === 0) { + processedLogEntries++; + } + + if (type === LogType.groupEnd) { + groupStack.pop(); + if (groupStack.length > 0) { + currentList = groupStack[groupStack.length - 1].children; + } else { + currentList = rootList; + } + if (depthInCollapsedGroup > 0) depthInCollapsedGroup--; + continue; + } + 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)); + } + const newEntry = { + ...entry, + type, + message, + trace: loggingTrace ? entry.trace : undefined, + children: + type === LogType.group || type === LogType.groupCollapsed + ? [] + : undefined + }; + currentList.push(newEntry); + if (newEntry.children) { + groupStack.push(newEntry); + currentList = newEntry.children; + if (depthInCollapsedGroup > 0) { + depthInCollapsedGroup++; + } else if (type === LogType.groupCollapsed) { + depthInCollapsedGroup = 1; + } + } + } + let name = identifierUtils + .makePathsRelative(context, origin, compilation.cache) + .replace(/\|/g, " "); + if (name in object.logging) { + let i = 1; + while (`${name}#${i}` in object.logging) { + i++; + } + name = `${name}#${i}`; + } + object.logging[name] = { + entries: rootList, + filteredEntries: logEntries.length - processedLogEntries, + debug: debugMode + }; + } + }, children: (object, compilation, context, options, factory) => { const { type } = context; object.children = factory.create( diff --git a/lib/stats/DefaultStatsPresetPlugin.js b/lib/stats/DefaultStatsPresetPlugin.js index 8d36409a5..576493f0b 100644 --- a/lib/stats/DefaultStatsPresetPlugin.js +++ b/lib/stats/DefaultStatsPresetPlugin.js @@ -36,6 +36,7 @@ const NAMED_PRESETS = { optimizationBailout: true, errorDetails: true, publicPath: true, + logging: "verbose", orphanModules: true, runtime: true, exclude: false, @@ -55,6 +56,7 @@ const NAMED_PRESETS = { optimizationBailout: true, errorDetails: true, publicPath: true, + logging: true, runtimeModules: true, runtime: true, exclude: false, @@ -65,17 +67,20 @@ const NAMED_PRESETS = { modules: true, maxModules: 0, errors: true, - warnings: true + warnings: true, + logging: "warn" }, "errors-only": { all: false, errors: true, - moduleTrace: true + moduleTrace: true, + logging: "error" }, "errors-warnings": { all: false, errors: true, - warnings: true + warnings: true, + logging: "warn" }, none: { all: false @@ -132,6 +137,10 @@ const DEFAULTS = { errorDetails: OFF_FOR_TO_STRING, warnings: NORMAL_ON, publicPath: OFF_FOR_TO_STRING, + logging: ({ all }, { forToString }) => + (forToString && all !== false ? "info" : false), + loggingDebug: () => [], + loggingTrace: OFF_FOR_TO_STRING, excludeModules: () => [], excludeAssets: () => [], maxModules: (o, { forToString }) => ((forToString ? 15 : Infinity)), @@ -142,7 +151,7 @@ const DEFAULTS = { colors: () => false }; -const normalizeExclude = item => { +const normalizeFilter = item => { if (typeof item === "string") { const regExp = new RegExp( `[\\\\/]${item.replace( @@ -169,13 +178,13 @@ const NORMALIZER = { if (!Array.isArray(value)) { value = value ? [value] : []; } - return value.map(normalizeExclude); + return value.map(normalizeFilter); }, excludeAssets: value => { if (!Array.isArray(value)) { value = value ? [value] : []; } - return value.map(normalizeExclude); + return value.map(normalizeFilter); }, warningsFilter: value => { if (!Array.isArray(value)) { @@ -195,6 +204,16 @@ const NORMALIZER = { `Can only filter warnings with Strings or RegExps. (Given: ${filter})` ); }); + }, + logging: value => { + if (value === true) value = "log"; + return value; + }, + loggingDebug: value => { + if (!Array.isArray(value)) { + value = value ? [value] : []; + } + return value.map(normalizeFilter); } }; diff --git a/lib/stats/DefaultStatsPrinterPlugin.js b/lib/stats/DefaultStatsPrinterPlugin.js index e30bc7cc6..2b4840b1d 100644 --- a/lib/stats/DefaultStatsPrinterPlugin.js +++ b/lib/stats/DefaultStatsPrinterPlugin.js @@ -43,6 +43,12 @@ const printSizes = (sizes, { formatSize }) => { } }; +const mapLines = (str, fn) => + str + .split("\n") + .map(fn) + .join("\n"); + /** * @param {number} n a number * @returns {string} number as two digit string, leading 0 @@ -130,6 +136,14 @@ const SIMPLE_PRINTERS = { : filteredAssets } ${plural(filteredAssets, "asset", "assets")}` : undefined, + "compilation.logging": (logging, context, printer) => + Array.isArray(logging) + ? undefined + : printer.print( + context.type, + Object.entries(logging).map(([name, value]) => ({ ...value, name })), + context + ), "compilation.children[].compilation.name": name => name ? `Child ${name}:` : "Child", @@ -402,12 +416,45 @@ const SIMPLE_PRINTERS = { "error.moduleTrace": moduleTrace => undefined, "error.separator!": () => "\n", + "loggingEntry(error).loggingEntry.message": (message, { red }) => + mapLines(message, x => ` ${red(x)}`), + "loggingEntry(warn).loggingEntry.message": (message, { yellow }) => + mapLines(message, x => ` ${yellow(x)}`), + "loggingEntry(info).loggingEntry.message": (message, { green }) => + mapLines(message, x => ` ${green(x)}`), + "loggingEntry(log).loggingEntry.message": (message, { bold }) => + mapLines(message, x => bold(x)), + "loggingEntry(debug).loggingEntry.message": message => message, + "loggingEntry(trace).loggingEntry.message": message => message, + "loggingEntry(profile).loggingEntry.message": (message, { magenta }) => + mapLines(message, x => `

${magenta(x)}`), + "loggingEntry(profileEnd).loggingEntry.message": (message, { magenta }) => + mapLines(message, x => `

${magenta(x)}`), + "loggingEntry(time).loggingEntry.message": (message, { magenta }) => + mapLines(message, x => ` ${magenta(x)}`), + "loggingEntry(group).loggingEntry.message": (message, { cyan }) => + mapLines(message, x => cyan(x)), + "loggingEntry(groupCollapsed).loggingEntry.message": (message, { cyan }) => + mapLines(message, x => `<+> ${cyan(x)}`), + "loggingEntry(clear).loggingEntry": () => "-------", + "loggingEntry(groupCollapsed).loggingEntry.children": () => "", + "loggingEntry.trace[]": trace => + trace ? mapLines(trace, x => `| ${x}`) : undefined, + "moduleTraceItem.originName": originName => originName, + loggingGroup: loggingGroup => + loggingGroup.entries.length === 0 ? "" : undefined, + "loggingGroup.debug": (flag, { red }) => ((flag ? red("DEBUG") : undefined)), + "loggingGroup.name": (name, { bold }) => bold(`LOG from ${name}`), + "loggingGroup.separator!": () => "\n", + "loggingGroup.filteredEntries": filteredEntries => + filteredEntries > 0 ? `+ ${filteredEntries} hidden lines` : undefined, + "moduleTraceDependency.loc": loc => loc }; -/** @type {Record} */ +/** @type {Record} */ const ITEM_NAMES = { "compilation.assets[]": "asset", "compilation.modules[]": "module", @@ -416,6 +463,7 @@ const ITEM_NAMES = { "compilation.namedChunkGroups[]": "chunkGroup", "compilation.errors[]": "error", "compilation.warnings[]": "error", + "compilation.logging[]": "loggingGroup", "compilation.children[]": "compilation", "asset.chunks[]": "assetChunk", "asset.auxiliaryChunks[]": "assetChunk", @@ -427,6 +475,10 @@ const ITEM_NAMES = { "chunk.origins[]": "chunkOrigin", "chunk.rootModules[]": "module", "chunk.modules[]": "module", + "loggingGroup.entries[]": logEntry => + `loggingEntry(${logEntry.type}).loggingEntry`, + "loggingEntry.children[]": logEntry => + `loggingEntry(${logEntry.type}).loggingEntry`, "error.moduleTrace[]": "moduleTraceItem", "moduleTraceItem.dependencies[]": "moduleTraceDependency" }; @@ -467,6 +519,7 @@ const PREFERED_ORDERS = { "chunks", "modules", "filteredModules", + "logging", "warnings", "errors", "children", @@ -572,6 +625,15 @@ const PREFERED_ORDERS = { error: ERROR_PREFERED_ORDER, warning: ERROR_PREFERED_ORDER, "chunk.childrenByOrder[]": ["type", "children"], + loggingGroup: [ + "debug", + "name", + "separator!", + "entries", + "separator!", + "filtredEntries" + ], + loggingEntry: ["message", "trace", "children"], "chunkGroup.childAssets[]": ["type", "children"] }; @@ -608,7 +670,10 @@ const SIMPLE_ITEMS_JOINER = { .join(" "), "compilation.errors": itemsJoinMoreSpacing, "compilation.warnings": itemsJoinMoreSpacing, + "compilation.logging": itemsJoinMoreSpacing, "moduleTraceItem.dependencies": itemsJoinOneLine, + "loggingEntry.children": items => + indent(items.filter(Boolean).join("\n"), " ", false), "compilation.children": items => items.map(item => indent(item, " ", true)).join("\n") }; @@ -701,7 +766,9 @@ const SIMPLE_ELEMENT_JOINERS = { for (const item of items) { if (!item.content) continue; const needMoreSpace = - item.element === "warnings" || item.element === "errors"; + item.element === "warnings" || + item.element === "errors" || + item.element === "logging"; if (result.length !== 0) { result.push(needMoreSpace || lastNeedMore ? "\n\n" : "\n"); } @@ -797,6 +864,7 @@ const SIMPLE_ELEMENT_JOINERS = { chunkOrigin: items => " > " + joinOneLine(items), "errors[].error": joinError(true), "warnings[].error": joinError(false), + loggingGroup: items => joinExplicitNewLine(items, "").trimRight(), moduleTraceItem: items => " @ " + joinOneLine(items), moduleTraceDependency: joinOneLine }; @@ -973,7 +1041,10 @@ class DefaultStatsPrinterPlugin { const itemName = ITEM_NAMES[key]; stats.hooks.getItemName .for(key) - .tap("DefaultStatsPrinterPlugin", () => itemName); + .tap( + "DefaultStatsPrinterPlugin", + typeof itemName === "string" ? () => itemName : itemName + ); } for (const key of Object.keys(SIMPLE_ITEMS_JOINER)) { diff --git a/lib/webpack.js b/lib/webpack.js index c3edc8ac9..66fa35711 100644 --- a/lib/webpack.js +++ b/lib/webpack.js @@ -52,7 +52,9 @@ const createCompiler = options => { options = new WebpackOptionsDefaulter().process(options); const compiler = new Compiler(options.context); compiler.options = options; - new NodeEnvironmentPlugin().apply(compiler); + new NodeEnvironmentPlugin({ + infrastructureLogging: options.infrastructureLogging + }).apply(compiler); if (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 51b2fc61e..2b89a24f8 100644 --- a/schemas/WebpackOptions.json +++ b/schemas/WebpackOptions.json @@ -252,7 +252,7 @@ }, { "instanceof": "Function", - "tsType": "Function" + "tsType": "((value: string) => boolean)" } ] }, @@ -2006,6 +2006,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" @@ -2263,6 +2292,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 3636839bc..d36965870 100644 --- a/test/Compiler.test.js +++ b/test/Compiler.test.js @@ -709,4 +709,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/JavascriptParser.unittest.js b/test/JavascriptParser.unittest.js index 9797dc16a..07f68ab94 100644 --- a/test/JavascriptParser.unittest.js +++ b/test/JavascriptParser.unittest.js @@ -259,69 +259,63 @@ describe("JavascriptParser", () => { const state = testCases[name][1]; const testParser = new JavascriptParser({}); - testParser.hooks.canRename.tap( - "abc", - "JavascriptParserTest", - expr => true - ); - testParser.hooks.canRename.tap( - "ijk", - "JavascriptParserTest", - expr => true - ); - testParser.hooks.call.tap("abc", "JavascriptParserTest", expr => { + testParser.hooks.canRename + .for("abc") + .tap("JavascriptParserTest", expr => true); + testParser.hooks.canRename + .for("ijk") + .tap("JavascriptParserTest", expr => true); + testParser.hooks.call.for("abc").tap("JavascriptParserTest", expr => { if (!testParser.state.abc) testParser.state.abc = []; testParser.state.abc.push(testParser.parseString(expr.arguments[0])); return true; }); - testParser.hooks.call.tap("cde.abc", "JavascriptParserTest", expr => { + testParser.hooks.call.for("cde.abc").tap("JavascriptParserTest", expr => { if (!testParser.state.cdeabc) testParser.state.cdeabc = []; testParser.state.cdeabc.push(testParser.parseString(expr.arguments[0])); return true; }); - testParser.hooks.call.tap("cde.ddd.abc", "JavascriptParserTest", expr => { - if (!testParser.state.cdedddabc) testParser.state.cdedddabc = []; - testParser.state.cdedddabc.push( - testParser.parseString(expr.arguments[0]) - ); - return true; - }); - testParser.hooks.expression.tap("fgh", "JavascriptParserTest", expr => { - if (!testParser.state.fgh) testParser.state.fgh = []; - testParser.state.fgh.push( - Array.from(testParser.scope.definitions.asSet()).join(" ") - ); - return true; - }); - testParser.hooks.expression.tap( - "fgh.sub", - "JavascriptParserTest", - expr => { + testParser.hooks.call + .for("cde.ddd.abc") + .tap("JavascriptParserTest", expr => { + if (!testParser.state.cdedddabc) testParser.state.cdedddabc = []; + testParser.state.cdedddabc.push( + testParser.parseString(expr.arguments[0]) + ); + return true; + }); + testParser.hooks.expression + .for("fgh") + .tap("JavascriptParserTest", expr => { + if (!testParser.state.fgh) testParser.state.fgh = []; + testParser.state.fgh.push( + Array.from(testParser.scope.definitions.asSet()).join(" ") + ); + return true; + }); + testParser.hooks.expression + .for("fgh.sub") + .tap("JavascriptParserTest", expr => { if (!testParser.state.fghsub) testParser.state.fghsub = []; testParser.state.fghsub.push( testParser.scope.inTry ? "try" : "notry" ); return true; - } - ); - testParser.hooks.expression.tap( - "ijk.sub", - "JavascriptParserTest", - expr => { + }); + testParser.hooks.expression + .for("ijk.sub") + .tap("JavascriptParserTest", expr => { if (!testParser.state.ijksub) testParser.state.ijksub = []; testParser.state.ijksub.push("test"); return true; - } - ); - testParser.hooks.expression.tap( - "memberExpr", - "JavascriptParserTest", - expr => { + }); + testParser.hooks.expression + .for("memberExpr") + .tap("JavascriptParserTest", expr => { if (!testParser.state.expressions) testParser.state.expressions = []; testParser.state.expressions.push(expr.name); return true; - } - ); + }); testParser.hooks.new.tap("xyz", "JavascriptParserTest", expr => { if (!testParser.state.xyz) testParser.state.xyz = []; testParser.state.xyz.push(testParser.parseString(expr.arguments[0])); @@ -367,23 +361,21 @@ describe("JavascriptParser", () => { describe("expression evaluation", () => { function evaluateInParser(source) { const parser = new JavascriptParser(); - parser.hooks.call.tap("test", "JavascriptParserTest", expr => { + parser.hooks.call.for("test").tap("JavascriptParserTest", expr => { parser.state.result = parser.evaluateExpression(expr.arguments[0]); }); - parser.hooks.evaluateIdentifier.tap( - "aString", - "JavascriptParserTest", - expr => + parser.hooks.evaluateIdentifier + .for("aString") + .tap("JavascriptParserTest", expr => new BasicEvaluatedExpression() .setString("aString") .setRange(expr.range) - ); - parser.hooks.evaluateIdentifier.tap( - "b.Number", - "JavascriptParserTest", - expr => + ); + parser.hooks.evaluateIdentifier + .for("b.Number") + .tap("JavascriptParserTest", expr => new BasicEvaluatedExpression().setNumber(123).setRange(expr.range) - ); + ); return parser.parse("test(" + source + ");").result; } @@ -615,7 +607,7 @@ describe("JavascriptParser", () => { }; const parser = new JavascriptParser(); - parser.hooks.call.tap("require", "JavascriptParserTest", expr => { + parser.hooks.call.for("require").tap("JavascriptParserTest", expr => { const param = parser.evaluateExpression(expr.arguments[0]); parser.state.param = param.string; }); diff --git a/test/StatsTestCases.test.js b/test/StatsTestCases.test.js index 64f494166..f9acfd2e8 100644 --- a/test/StatsTestCases.test.js +++ b/test/StatsTestCases.test.js @@ -6,6 +6,15 @@ const fs = require("graceful-fs"); const webpack = require(".."); +/** + * 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 @@ -129,24 +138,21 @@ 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"); } else { actual = actual .replace(/\u001b\[1m\u001b\[([0-9;]*)m/g, "") .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"); } const testPath = path.join(base, testName); - const testPathPattern = testPath.replace( - /[-[\]\\/{}()*+?.^$|]/g, - "\\$&" - ); actual = actual .replace(/\r\n?/g, "\n") .replace(/[\t ]*Version:.+\n/g, "") - .replace(new RegExp(testPathPattern, "g"), "Xdir/" + testName) + .replace(new RegExp(quotemeta(testPath), "g"), "Xdir/" + testName) + .replace(/(\w)\\(\w)/g, "$1/$2") .replace(/, additional resolving: Xms/g, ""); expect(actual).toMatchSnapshot(); done(); diff --git a/test/Validation.test.js b/test/Validation.test.js index 015119a75..d9337c785 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?, experiments?, 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?, experiments?, 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 9ab3962fa..eafbbfcb5 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -1277,6 +1277,43 @@ Child 4 chunks: [767] ./d.js 22 bytes {524} [built]" `; +exports[`StatsTestCases should print correct stats for logging 1`] = ` +"Hash: 17d1aced1f8edabe0aa9 +Time: Xms +Built at: 1970-04-20 12:42:42 + Asset Size Chunks Chunk Names +main.js 1.32 KiB {179} [emitted] main +Entrypoint main = main.js +[390] ./index.js 1 bytes {179} [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: 8a3d2df87a4f01705c82 Time: Xms @@ -2062,20 +2099,48 @@ webpack/runtime/jsonp chunk loading 3.36 KiB {179} [runtime] [used exports unknown] webpack/runtime/publicPath 27 bytes {179} [runtime] [no exports] - [used exports unknown]" + [used exports unknown] + +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`] = ` -"ERROR in ./index.js 1:0-25 +"LOG from MyPlugin + Error ++ 9 hidden lines + +ERROR in ./index.js 1:0-25 Module not found: Error: Can't resolve 'does-not-exist' in 'Xdir/preset-errors-only-error' " `; -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`] = `" 10 modules"`; +exports[`StatsTestCases should print correct stats for preset-minimal 1`] = ` +" 10 modules + +LOG from MyPlugin + Error + Warning ++ 8 hidden lines +" +`; exports[`StatsTestCases should print correct stats for preset-minimal-simple 1`] = `" 1 module"`; @@ -2109,7 +2174,14 @@ Entrypoint main = main.js [767] ./d.js 22 bytes {524} [built] [847] ./a.js 22 bytes {179} [built] [996] ./b.js 22 bytes {996} [built] - + 4 hidden modules" + + 4 hidden modules + +LOG from MyPlugin + Error + Warning + Info ++ 7 hidden lines +" `; exports[`StatsTestCases should print correct stats for preset-normal-performance 1`] = ` @@ -2230,7 +2302,18 @@ chunk {996} 996.js 22 bytes <{179}> [rendered] [996] ./b.js 22 bytes {996} [depth 1] [built] ModuleConcatenation bailout: Module is not an ECMAScript module amd require ./b [10] ./index.js 2:0-16 - [10] Xms -> Xms (resolving: Xms, restoring: Xms, integration: Xms, building: Xms, storing: Xms)" + [10] Xms -> Xms (resolving: Xms, restoring: Xms, integration: Xms, building: Xms, storing: 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 000000000..e69de29bb 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 000000000..f59e88aae --- /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 000000000..ce49b7dc9 --- /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 2912acd3e..695022714 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 7f65a6052..2ac283175 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 5c54a2307..038aa982f 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 53931799c..45e664f35 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 e99589235..0a3d5be8f 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 0bd5f398a..5d85c8aab 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 c44f313ee..e4d57fe96 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()] }; diff --git a/yarn.lock b/yarn.lock index c93d50fc0..d9ebfadb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -373,9 +373,9 @@ integrity sha512-MeatbbUsZ80BEsKPXby6pUZjUM9ZuHIpWElN0siopih3fvnlpX2O9L6D5+dzDIb36lf9tM/8U4PVdLQ+L4qr4A== "@types/node@^10.12.21": - version "10.14.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.12.tgz#0eec3155a46e6c4db1f27c3e588a205f767d622f" - integrity sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg== + version "10.14.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.13.tgz#ac786d623860adf39a3f51d629480aacd6a6eec7" + integrity sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ== "@types/prettier@^1.16.1": version "1.16.1" @@ -609,9 +609,9 @@ acorn@^5.5.3: integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.0.tgz#67f0da2fc339d6cfb5d6fb244fd449f33cd8bbe3" - integrity sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw== + version "6.2.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51" + integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q== ajv-errors@^1.0.0: version "1.0.1" @@ -1321,10 +1321,10 @@ commander@^2.14.1, commander@^2.19.0, commander@^2.9.0, commander@~2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== -comment-parser@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.5.5.tgz#c2584cae7c2f0afc773e96b2ee98f8c10cbd693d" - integrity sha512-oB3TinFT+PV3p8UwDQt71+HkG03+zwPwikDlKU6ZDmql6QX2zFlQ+G0GGSDqyJhdZi4PSlzFBm+YJ+ebOX3Vgw== +comment-parser@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.6.1.tgz#88040c7c0a57c62e64962c3e888518620a42e7c9" + integrity sha512-Putzd7Ilyvknmb1KxGf5el9uw0sPx9gEVnDrm8tlvXGN1i8Uaa2VBxB32hUhfzTlrEhhxNQ+pKq4ZNe8wNxjmw== commondir@^1.0.1: version "1.0.1" @@ -1841,16 +1841,16 @@ eslint-plugin-es@^1.3.1: regexpp "^2.0.1" eslint-plugin-jest@^22.2.2: - version "22.8.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.8.0.tgz#242ef5459e8da25d2c41438e95eb546e03d7fae1" - integrity sha512-2VftZMfILmlhL3VMq5ptHRIuyyXb3ShDEDb1J1UjvWNzm4l+UK/YmwNuTuJcM0gv8pJuOfiR/8ZptJ8Ou68pFw== + version "22.13.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.13.0.tgz#d7d134c6e3c2f67cc50f5fa89a329db579d28428" + integrity sha512-bIr8LL7buUXS8Pk69SFgaDKgyvPQkDu6i8ko0lP54uccszlo4EOwtstDXOZl5Af3JwudbECxRUbCpL/2cKDkkg== eslint-plugin-jsdoc@^15.3.2: - version "15.5.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-15.5.2.tgz#89768320c64ec2f30d12209e1926c4decca0568a" - integrity sha512-5s39RYGaqugWVoOfc6pAwj9yeNh7mclygBWTyYVJX+sGiNchwCtgHbn2AjeonOw0g168CPI3itiXetHj2Yo8gg== + version "15.5.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-15.5.3.tgz#44e408e3bf2f60f3ad18dc829f9b9a1e34f33154" + integrity sha512-lw8wYa1UFV53JLoqKOQR8YBkKlE/aguR+HGyytL9VKsVvm83DK8ReYnNNDRKik3MF661cGuaUuGfIEcdqg9l4A== dependencies: - comment-parser "^0.5.5" + comment-parser "^0.6.0" debug "^4.1.1" flat-map-polyfill "^0.3.8" jsdoctypeparser "5.0.1" @@ -3835,9 +3835,9 @@ lodash.sortby@^4.7.0: integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.4: - version "4.17.14" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" - integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== log-driver@^1.2.7: version "1.2.7" @@ -5500,9 +5500,9 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= simple-git@^1.65.0, simple-git@^1.85.0: - version "1.121.0" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.121.0.tgz#4bdf0828cd1b0bb3cb7ed9bead2771982ef5876a" - integrity sha512-LyYri/nuAX8+cx9nZw38mWO6oHNi//CmiPlkBL7aVjZIsdldve7eeDwXu9L4wP/74MpNHucXkXc/BOuIQShhPg== + version "1.122.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.122.0.tgz#33b2d3a760aa02df470c79fbab5413d4f4e68945" + integrity sha512-plTwhnkIHrw2TFMJbJH/mKwWGgFbj03V9wcfBKa4FsuvgJbpwdlSJnlvkIQWDV1CVLaf2Gl6zSNeRRnxBRhX1g== dependencies: debug "^4.0.1"