UNPKG

time-analytics-webpack-plugin

Version:
350 lines 15.9 kB
"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