time-analytics-webpack-plugin
Version:
analytize the time of loaders and plugins
350 lines • 15.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProxyPlugin = void 0;
/* eslint-disable @typescript-eslint/no-this-alias */ // use function, so that we could put logic firstly
/* eslint-disable @typescript-eslint/no-shadow */ // could not come up with that many name
/* eslint-disable @typescript-eslint/naming-convention */ // use _ as private field name
const crypto_1 = require("crypto");
const analyzer_1 = require("./analyzer");
const TimeAnalyticsPlugin_1 = require("./TimeAnalyticsPlugin");
const utils_1 = require("./utils");
// TODO: remove webpack 4 support
let isWebpack4WarnLogged = false;
class ProxyPlugin {
validatePluginIsUsedOnce(plugin) {
const pluginName = plugin.constructor.name;
if (this.injectedPluginNames.has(pluginName)) {
utils_1.ConsoleHelper.warn(`${pluginName} is used twice, are you sure you really want to do this?`);
}
else {
this.injectedPluginNames.add(pluginName);
}
}
constructor(proxiedPlugin) {
this.injectedPluginNames = new Set();
this._hookProviderCandidatesClassName = ['Compiler', 'Compilation', 'ContextModuleFactory', 'JavascriptParser', 'NormalModuleFactory'];
this.cachedProxyForHooksProvider = new Map();
this.cachedUnfrozenHooks = new Map();
this.cachedProxyForHooks = new Map();
this.cachedProxyForHookMap = new Map();
this.cachedProxyForHookMapFor = new Map();
this.cachedProxyForHook = new Map();
this.knownTapMethodNames = ['tap', 'tapAsync', 'tapPromise'];
this.cachedProxyForTap = new Map();
this.cachedProxyForTapAsync = new Map();
this.cachedProxyForTapPromise = new Map();
this.validatePluginIsUsedOnce(proxiedPlugin);
this._proxiedPlugin = proxiedPlugin;
this.proxiedPluginName = proxiedPlugin.constructor.name;
}
apply(compiler) {
const proxiedCompiler = this._proxyForHookProviderCandidates(compiler);
if ((0, TimeAnalyticsPlugin_1.isWebpackPlugin)(this._proxiedPlugin)) {
this._proxiedPlugin.apply(proxiedCompiler);
}
else {
this._proxiedPlugin.apply(proxiedCompiler, proxiedCompiler);
}
}
_isHooksProvider(candidate) {
const className = candidate?.constructor?.name; // not a pretty accurate condition, but it should be enough.
if (!className)
return false;
return this._hookProviderCandidatesClassName.includes(className);
}
_proxyForHookProviderCandidates(candidate) {
if (!this._isHooksProvider(candidate)) {
return candidate;
}
return this._proxyForHooksProvider(candidate);
}
_proxyForHooksProvider(hooksProvider) {
const that = this;
return getOrCreate(this.cachedProxyForHooksProvider, hooksProvider, __proxyForHooksProviderWorker);
function __proxyForHooksProviderWorker(hooksProvider) {
return new Proxy(hooksProvider, {
get: (target, property) => {
if (property === 'hooks') {
const originHooks = target[property];
// Webpack 4 not freeze the hooks, but Webpack 5 freeze
(0, utils_1.assert)(originHooks.constructor.name === 'Object', '`Hooks` should just be plain object');
const unfrozenHooks = getOrCreate(that.cachedUnfrozenHooks, originHooks, _createUnfrozenHooks);
return that._proxyForHooks(unfrozenHooks, originHooks);
}
return target[property];
},
});
}
/**
* If we use a proxy on frozen object, it's invalid to return a different object with the origin object.
* So we need to `Unfrozen` it firstly.
*/
function _createUnfrozenHooks(originHooks) {
let hooks;
if (Object.isFrozen(originHooks)) {
// TODO: try to remove in webpack 6
hooks = {
// Add this lazily to avoid a warnning:
// DeprecationWarning: Compilation.hooks.normalModuleLoader was moved to NormalModule.getCompilationHooks(compilation).loader
// ...originHooks
};
}
else {
// TODO: remove this support
if (!isWebpack4WarnLogged) {
utils_1.ConsoleHelper.warn('It seems you are using Webpack 4. However, this plugin is designed for Webpack 5.');
isWebpack4WarnLogged = true;
}
hooks = originHooks;
}
return hooks;
}
}
_proxyForHooks(hooks, originHooks) {
const that = this;
return getOrCreate(this.cachedProxyForHooks, hooks, _proxyForHooksWorker);
function _proxyForHooksWorker(hooks) {
return new Proxy(hooks, {
get: function (target, property) {
/**
* hooks is frozen in webpak 5, we need to unfrozen it firstly, @see {@link _createUnfrozenHooks} for more background.
* Add the property lazily
*/
if (!(property in target)) {
target[property] = originHooks[property];
}
(0, utils_1.assert)(typeof property !== 'symbol', 'Getting Symbol property from "hooks", it should never happen, right?');
const method = target[property];
switch (true) {
case isHook(method):
return that._proxyForHook(method);
case isFakeHook(method): {
(0, utils_1.assert)(Object.isFrozen(method), 'fake hook should be frozen');
const unfrozenFakeHook = { ...method };
return that._proxyForHook(unfrozenFakeHook);
}
case isHookMap(method):
return that._proxyForHookMap(method);
default:
(0, utils_1.fail)('unhandled property from hook');
}
},
});
}
}
_proxyForHookMap(hookMap) {
const that = this;
return getOrCreate(this.cachedProxyForHookMap, hookMap, _proxyForHookMapWorker);
function _proxyForHookMapWorker(hookMap) {
return new Proxy(hookMap, {
get: function (target, property) {
const origin = target[property];
if (property === 'for') {
return that._proxyForHookMapFor(origin);
}
return origin;
},
});
}
}
_proxyForHookMapFor(hookMapFor) {
const that = this;
return getOrCreate(this.cachedProxyForHookMapFor, hookMapFor, _proxyForHookMapForWorker);
function _proxyForHookMapForWorker(hookMapFor) {
return new Proxy(hookMapFor, {
apply: (target, thisArg, argArray) => {
const originHook = target.apply(thisArg, argArray);
(0, utils_1.assert)(isHook(originHook));
return that._proxyForHook(originHook);
},
});
}
}
_proxyForHook(hook) {
const that = this;
return getOrCreate(this.cachedProxyForHook, hook, _proxyForHookWorker);
function _proxyForHookWorker(hook) {
return new Proxy(hook, {
get: function (target, property) {
(0, utils_1.assert)(typeof property !== 'symbol', 'Getting Symbol property from "hook", it should never happen, right?');
if (isIgnoreProperty(target, property))
return target[property];
(0, utils_1.assert)(that.knownTapMethodNames.includes(property));
const tapMethod = target[property];
switch (property) {
case 'tap':
return that._proxyForTap(tapMethod);
case 'tapAsync':
return that._proxyForTapAsync(tapMethod);
case 'tapPromise':
return that._proxyForTapPromise(tapMethod);
default:
(0, utils_1.fail)(`${property} is called on a hook, but we could not handle it now.`);
}
},
});
}
function isIgnoreProperty(target, property) {
// `_XXX` is the implement detail that is used internally
// it might be a bad idea, but we want to handle every thing explicitly to take full control of it
const isImplementationDetail = property.startsWith('_');
// if the property is not a function, we do not want to take over it.
const isFunction = typeof target[property] === 'function';
// call, callAsync, isUsed and compilte might be used by childCompiler
const isIgnoredProperty = ['intercept', 'call', 'callAsync', 'isUsed', 'compile'].includes(property);
return isImplementationDetail || !isFunction || isIgnoredProperty;
}
}
_proxyForTap(tap) {
return getOrCreate(this.cachedProxyForTap, tap, this._proxyForTapWorker.bind(this));
}
_proxyForTapAsync(tap) {
return getOrCreate(this.cachedProxyForTapAsync, tap, this._proxyForTapAsyncWorker.bind(this));
}
_proxyForTapPromise(tap) {
return getOrCreate(this.cachedProxyForTapPromise, tap, this._proxyForTapPromiseWorker.bind(this));
}
_proxyForTapWorker(tap) {
return new Proxy(tap, {
apply: (target, thisArg, argArray) => {
(0, utils_1.assert)(argArray.length == 2, 'tap should receive only two parameters');
const options = argArray[0];
const originFn = argArray[1];
const wrappedFn = wrapTapCallback.call(this, originFn);
return target.apply(thisArg, [options, wrappedFn]);
},
});
}
_proxyForTapAsyncWorker(tap) {
return new Proxy(tap, {
apply: (target, thisArg, argArray) => {
(0, utils_1.assert)(argArray.length == 2, 'tapAsync should receive only two parameters');
const options = argArray[0];
const originFn = argArray[1];
const wrappedFn = wrapTapAsyncCallback.call(this, originFn);
return target.apply(thisArg, [options, wrappedFn]);
},
});
}
_proxyForTapPromiseWorker(tap) {
return new Proxy(tap, {
apply: (target, thisArg, argArray) => {
(0, utils_1.assert)(argArray.length == 2, 'tapPromise should receive only two parameters');
const options = argArray[0];
const originFn = argArray[1];
const wrappedFn = wrapTapPromiseCallback.call(this, originFn);
return target.apply(thisArg, [options, wrappedFn]);
},
});
}
}
exports.ProxyPlugin = ProxyPlugin;
function wrapTapCallback(tapCallback) {
const pluginName = this.proxiedPluginName;
const proxyForHookProviderCandidates = this._proxyForHookProviderCandidates.bind(this);
return function (...args) {
const wrapedArgs = args.map(proxyForHookProviderCandidates);
const uuid = (0, crypto_1.randomUUID)();
analyzer_1.analyzer.collectPluginInfo({
kind: analyzer_1.AnalyzeInfoKind.plugin,
eventType: analyzer_1.PluginEventType.start,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.normal,
});
const origionalReturn = tapCallback(...wrapedArgs);
analyzer_1.analyzer.collectPluginInfo({
kind: analyzer_1.AnalyzeInfoKind.plugin,
eventType: analyzer_1.PluginEventType.end,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.normal,
});
return origionalReturn;
};
}
function wrapTapAsyncCallback(tapCallback) {
const pluginName = this.proxiedPluginName;
const proxyForHookProviderCandidates = this._proxyForHookProviderCandidates.bind(this);
return function (...args) {
const callback = args[args.length - 1];
const noncallbackArgs = args.slice(0, -1);
const wrapedNoncallbackArgs = noncallbackArgs.map(proxyForHookProviderCandidates);
const uuid = (0, crypto_1.randomUUID)();
analyzer_1.analyzer.collectPluginInfo({
kind: analyzer_1.AnalyzeInfoKind.plugin,
eventType: analyzer_1.PluginEventType.start,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.async,
});
const wrappedCallback = () => {
analyzer_1.analyzer.collectPluginInfo({
kind: analyzer_1.AnalyzeInfoKind.plugin,
eventType: analyzer_1.PluginEventType.end,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.async,
});
callback();
};
const origionalReturn = tapCallback(...wrapedNoncallbackArgs, wrappedCallback);
return origionalReturn;
};
}
function wrapTapPromiseCallback(tapCallback) {
const pluginName = this.proxiedPluginName;
const proxyForHookProviderCandidates = this._proxyForHookProviderCandidates.bind(this);
return async function (...args) {
const wrapedArgs = args.map(proxyForHookProviderCandidates);
const uuid = (0, crypto_1.randomUUID)();
analyzer_1.analyzer.collectPluginInfo({
eventType: analyzer_1.PluginEventType.start,
kind: analyzer_1.AnalyzeInfoKind.plugin,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.promise,
});
await tapCallback(...wrapedArgs);
analyzer_1.analyzer.collectPluginInfo({
eventType: analyzer_1.PluginEventType.end,
kind: analyzer_1.AnalyzeInfoKind.plugin,
pluginName,
time: (0, utils_1.now)(),
tapCallId: uuid,
tapType: analyzer_1.TapType.promise,
});
return;
};
}
function getOrCreate(cache, key, factory) {
if (!cache.has(key)) {
const proxyForHooks = factory(key);
cache.set(key, proxyForHooks);
}
return cache.get(key);
}
function isHook(obj) {
return (0, utils_1.isConstructorNameInPrototypeChain)('Hook', obj);
}
/**
* This is a webpack implementation detail.
*
* Some hook will be removed in webpack 6, and they are not `Tapable` class but a fake hook.
*
* An example hook is `additionalAssets`
*
* @deprecated should be removed if webpack does not use fake hook internally
*/
function isFakeHook(obj) {
return obj._fakeHook;
}
function isHookMap(obj) {
return (0, utils_1.isConstructorNameInPrototypeChain)('HookMap', obj);
}
//# sourceMappingURL=ProxyPlugin.js.map