WebpackPatcher: Use way less closures (#3217)

This commit is contained in:
Nuckyz 2025-02-12 14:03:18 -03:00 committed by GitHub
parent 5d482ff3bf
commit 306890aa13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 236 additions and 244 deletions

View file

@ -134,7 +134,7 @@ export default tseslint.config(
"no-unsafe-optional-chaining": "error", "no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error", "no-useless-backreference": "error",
"use-isnan": "error", "use-isnan": "error",
"prefer-const": "error", "prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error", "prefer-spread": "error",
// Plugin Rules // Plugin Rules

View file

@ -16,11 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* eslint-disable no-fallthrough */
// eslint-disable-next-line spaced-comment
/// <reference types="../src/globals" /> /// <reference types="../src/globals" />
// eslint-disable-next-line spaced-comment
/// <reference types="../src/modules" /> /// <reference types="../src/modules" />
import { createHmac } from "crypto"; import { createHmac } from "crypto";

View file

@ -8,7 +8,7 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { wreq } from "@webpack"; import { wreq } from "@webpack";
import { AnyModuleFactory, ModuleFactory } from "webpack"; import { AnyModuleFactory, ModuleFactory } from "@webpack/wreq.d";
export async function loadLazyChunks() { export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
@ -140,8 +140,8 @@ export async function loadLazyChunks() {
} }
Webpack.factoryListeners.add(factoryListener); Webpack.factoryListeners.add(factoryListener);
for (const factoryId in wreq.m) { for (const moduleId in wreq.m) {
factoryListener(wreq.m[factoryId]); factoryListener(wreq.m[moduleId]);
} }
await chunksSearchingDone; await chunksSearchingDone;

View file

@ -6,9 +6,9 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { addPatch, patches } from "plugins"; import { getBuildNumber, patchTimings } from "@webpack/patcher";
import { getBuildNumber } from "webpack/patchWebpack";
import { addPatch, patches } from "../plugins";
import { loadLazyChunks } from "./loadLazyChunks"; import { loadLazyChunks } from "./loadLazyChunks";
async function runReporter() { async function runReporter() {
@ -51,7 +51,7 @@ async function runReporter() {
} }
} }
for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) { for (const [plugin, moduleId, match, totalTime] of patchTimings) {
if (totalTime > 5) { if (totalTime > 5) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`); new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
} }

View file

@ -20,7 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "webpack"; import { WebpackRequire } from "@webpack/wreq.d";
const settings = definePluginSettings({ const settings = definePluginSettings({
disableAnalytics: { disableAnalytics: {

View file

@ -31,6 +31,7 @@ import { Logger } from "@utils/Logger";
import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { patches } from "@webpack/patcher";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
import Plugins from "~plugins"; import Plugins from "~plugins";
@ -41,7 +42,7 @@ const logger = new Logger("PluginManager", "#a6d189");
export const PMLogger = logger; export const PMLogger = logger;
export const plugins = Plugins; export const plugins = Plugins;
export const patches = [] as Patch[]; export { patches };
/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */ /** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */
let enabledPluginsSubscribedFlux = false; let enabledPluginsSubscribedFlux = false;

View file

@ -9,32 +9,23 @@ import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { interpolateIfDefined } from "@utils/misc"; import { interpolateIfDefined } from "@utils/misc";
import { canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { Patch, PatchReplacement } from "@utils/types";
import { traceFunctionWithResults } from "../debug/Tracer"; import { traceFunctionWithResults } from "../debug/Tracer";
import { patches } from "../plugins"; import { _initWebpack, _shouldIgnoreModule, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack";
import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, MaybeWrappedModuleFactory, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from "."; import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleExports, PatchedModuleFactory, WebpackRequire } from "./wreq.d";
export const patches = [] as Patch[];
export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory"); export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");
export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource"); export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy"); export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
/** A set with all the Webpack instances */
export const allWebpackInstances = new Set<AnyWebpackRequire>(); export const allWebpackInstances = new Set<AnyWebpackRequire>();
export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>;
const logger = new Logger("WebpackInterceptor", "#8caaee"); export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: PatchReplacement["match"], totalTime: number]>;
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
let wreqFallbackApplied = false;
/** Whether we should be patching factories.
*
* This should be disabled if we start searching for the module to get the build number, and then resumed once it's done.
* */
let shouldPatchFactories = true;
export const getBuildNumber = makeLazy(() => { export const getBuildNumber = makeLazy(() => {
try { try {
shouldPatchFactories = false;
try { try {
if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) { if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) {
const hardcodedGetBuildNumber = wreq(128014).b as () => number; const hardcodedGetBuildNumber = wreq(128014).b as () => number;
@ -59,13 +50,23 @@ export const getBuildNumber = makeLazy(() => {
return typeof buildNumber === "number" ? buildNumber : -1; return typeof buildNumber === "number" ? buildNumber : -1;
} catch { } catch {
return -1; return -1;
} finally {
shouldPatchFactories = true;
} }
}); });
type Define = typeof Reflect.defineProperty; export function getFactoryPatchedSource(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
const define: Define = (target, p, attributes) => { return webpackRequire.m[moduleId]?.[SYM_PATCHED_SOURCE];
}
export function getFactoryPatchedBy(moduleId: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
return webpackRequire.m[moduleId]?.[SYM_PATCHED_BY];
}
const logger = new Logger("WebpackInterceptor", "#8caaee");
/** Whether we tried to fallback to the WebpackRequire of the factory, or disabled patches */
let wreqFallbackApplied = false;
const define: typeof Reflect.defineProperty = (target, p, attributes) => {
if (Object.hasOwn(attributes, "value")) { if (Object.hasOwn(attributes, "value")) {
attributes.writable = true; attributes.writable = true;
} }
@ -77,22 +78,17 @@ const define: Define = (target, p, attributes) => {
}); });
}; };
export function getOriginalFactory(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // wreq.m is the Webpack object containing module factories. It is pre-populated with factories, and is also populated via webpackGlobal.push
const moduleFactory = webpackRequire.m[id]; // We use this setter to intercept when wreq.m is defined and apply patching to its factories.
return (moduleFactory?.[SYM_ORIGINAL_FACTORY] ?? moduleFactory) as AnyModuleFactory | undefined;
}
export function getFactoryPatchedSource(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // Factories can be patched in two ways. Eagerly or lazily.
return webpackRequire.m[id]?.[SYM_PATCHED_SOURCE]; // If we are patching eagerly, pre-populated factories are patched immediately and new factories are patched when set.
} // Else, we only patch them when called.
export function getFactoryPatchedBy(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) { // Factories are always wrapped in a proxy, which allows us to intercept the call to them, patch if they werent eagerly patched,
return webpackRequire.m[id]?.[SYM_PATCHED_BY]; // and call them with our wrapper which notifies our listeners.
}
// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push // wreq.m is also wrapped in a proxy to intercept when new factories are set, patch them eargely, if enabled, and wrap them in the factory proxy.
// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories.
// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or defining getters for the patched versions.
// If this is the main Webpack, we also set up the internal references to WebpackRequire. // If this is the main Webpack, we also set up the internal references to WebpackRequire.
define(Function.prototype, "m", { define(Function.prototype, "m", {
@ -131,13 +127,17 @@ define(Function.prototype, "m", {
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0); const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0);
// Patch the pre-populated factories // Patch the pre-populated factories
for (const id in originalModules) { for (const moduleId in originalModules) {
if (updateExistingFactory(originalModules, id, originalModules[id], true)) { const originalFactory = originalModules[moduleId];
if (updateExistingFactory(originalModules, moduleId, originalFactory, originalModules, true)) {
continue; continue;
} }
notifyFactoryListeners(originalModules[id]); notifyFactoryListeners(moduleId, originalFactory);
defineModulesFactoryGetter(id, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]);
const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, originalFactory) : originalFactory, moduleFactoryHandler);
define(originalModules, moduleId, { value: proxiedFactory });
} }
define(originalModules, Symbol.toStringTag, { define(originalModules, Symbol.toStringTag, {
@ -145,7 +145,6 @@ define(Function.prototype, "m", {
enumerable: false enumerable: false
}); });
// The proxy responsible for patching the module factories when they are set, or defining getters for the patched versions
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
/* /*
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
@ -156,6 +155,7 @@ define(Function.prototype, "m", {
} }
}); });
// The proxy for patching eagerly and/or wrapping factories in their proxy.
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = { const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
/* /*
If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype
@ -172,57 +172,96 @@ const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
}, },
*/ */
// The set trap for patching or defining getters for the module factories when new module factories are loaded
set(target, p, newValue, receiver) { set(target, p, newValue, receiver) {
if (updateExistingFactory(target, p, newValue)) { if (updateExistingFactory(target, p, newValue, receiver)) {
return true; return true;
} }
notifyFactoryListeners(newValue); notifyFactoryListeners(p, newValue);
defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue);
return true; const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(p, newValue) : newValue, moduleFactoryHandler);
return Reflect.set(target, p, proxiedFactory, receiver);
}
};
// The proxy for patching lazily and/or running factories with our wrapper.
const moduleFactoryHandler: ProxyHandler<MaybePatchedModuleFactory> = {
apply(target, thisArg: unknown, argArray: Parameters<AnyModuleFactory>) {
// SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id
const moduleId = target.name;
// SYM_ORIGINAL_FACTORY means the factory has already been patched
if (target[SYM_ORIGINAL_FACTORY] != null) {
return runFactoryWithWrap(moduleId, target as PatchedModuleFactory, thisArg, argArray);
}
const patchedFactory = patchFactory(moduleId, target);
return runFactoryWithWrap(moduleId, patchedFactory, thisArg, argArray);
},
get(target, p, receiver) {
if (target[SYM_ORIGINAL_FACTORY] != null && (p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY)) {
return Reflect.get(target[SYM_ORIGINAL_FACTORY], p, target[SYM_ORIGINAL_FACTORY]);
}
const v = Reflect.get(target, p, receiver);
// Make proxied factories `toString` return their original factory `toString`
if (p === "toString") {
return v.bind(target[SYM_ORIGINAL_FACTORY] ?? target);
}
return v;
} }
}; };
/** /**
* Update a factory that exists in any Webpack instance with a new original factory. * Update a factory that exists in any Webpack instance with a new original factory.
* *
* @target The module factories where this new original factory is being set * @param moduleFactoriesTarget The module factories where this new original factory is being set
* @param id The id of the module * @param moduleId The id of the module
* @param newFactory The new original factory * @param newFactory The new original factory
* @param receiver The receiver of the new factory
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget * @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance * @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance
*/ */
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) { function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget: boolean = false) {
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined; let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined; let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
for (const wreq of allWebpackInstances) { for (const wreq of allWebpackInstances) {
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) {
continue;
}
if (Object.hasOwn(wreq.m, id)) { if (Object.hasOwn(wreq.m, moduleId)) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, moduleId);
moduleFactoriesWithFactory = wreq.m; moduleFactoriesWithFactory = wreq.m;
break; break;
} }
} }
if (existingFactory != null) { if (existingFactory != null) {
// If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required. // If existingFactory exists in any Webpack instance, it's either wrapped in our proxy, or it has already been required.
// So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original, // In the case it is wrapped in our proxy, we need the Webpack instance with this new original factory to also have our proxy.
// and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) // So, define the descriptor of the existing factory on it.
if (moduleFactoriesWithFactory !== moduleFactoriesTarget) { if (moduleFactoriesWithFactory !== moduleFactoriesTarget) {
Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory); Reflect.defineProperty(receiver, moduleId, existingFactory);
} }
// Persist patched source and patched by in the new original factory, if the patched one has already been required const existingFactoryValue = moduleFactoriesWithFactory![moduleId];
if (IS_DEV && existingFactory.value != null) {
newFactory[SYM_PATCHED_SOURCE] = existingFactory.value[SYM_PATCHED_SOURCE]; // Update with the new original factory, if it does have a current original factory
newFactory[SYM_PATCHED_BY] = existingFactory.value[SYM_PATCHED_BY]; if (existingFactoryValue[SYM_ORIGINAL_FACTORY] != null) {
existingFactoryValue[SYM_ORIGINAL_FACTORY] = newFactory;
} }
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget); // Persist patched source and patched by in the new original factory
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = existingFactoryValue[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = existingFactoryValue[SYM_PATCHED_BY];
}
return true;
} }
return false; return false;
@ -231,12 +270,13 @@ function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id
/** /**
* Notify all factory listeners. * Notify all factory listeners.
* *
* @param moduleId The id of the module
* @param factory The original factory to notify for * @param factory The original factory to notify for
*/ */
function notifyFactoryListeners(factory: AnyModuleFactory) { function notifyFactoryListeners(moduleId: PropertyKey, factory: AnyModuleFactory) {
for (const factoryListener of factoryListeners) { for (const factoryListener of factoryListeners) {
try { try {
factoryListener(factory); factoryListener(factory, moduleId);
} catch (err) { } catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener); logger.error("Error in Webpack factory listener:\n", err, factoryListener);
} }
@ -244,190 +284,138 @@ function notifyFactoryListeners(factory: AnyModuleFactory) {
} }
/** /**
* Define the getter for returning the patched version of the module factory. * Run a (possibly) patched module factory with a wrapper which notifies our listeners.
* *
* If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original * @param moduleId The id of the module
* and only be patched when accessed for the first time. * @param patchedFactory The (possibly) patched module factory
* * @param thisArg The `value` of the call to the factory
* @param id The id of the module * @param argArray The arguments of the call to the factory
* @param factory The original or patched module factory
*/ */
function defineModulesFactoryGetter(id: PropertyKey, factory: MaybeWrappedModuleFactory) { function runFactoryWithWrap(moduleId: PropertyKey, patchedFactory: PatchedModuleFactory, thisArg: unknown, argArray: Parameters<MaybePatchedModuleFactory>) {
const descriptor: PropertyDescriptor = { const originalFactory = patchedFactory[SYM_ORIGINAL_FACTORY];
get() {
// SYM_ORIGINAL_FACTORY means the factory is already patched
if (!shouldPatchFactories || factory[SYM_ORIGINAL_FACTORY] != null) {
return factory;
}
return (factory = wrapAndPatchFactory(id, factory)); if (patchedFactory === originalFactory) {
}, // @ts-expect-error Clear up ORIGINAL_FACTORY if the factory did not have any patch applied
set(newFactory: MaybeWrappedModuleFactory) { delete patchedFactory[SYM_ORIGINAL_FACTORY];
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = factory[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = factory[SYM_PATCHED_BY];
}
if (factory[SYM_ORIGINAL_FACTORY] != null) {
factory.toString = newFactory.toString.bind(newFactory);
factory[SYM_ORIGINAL_FACTORY] = newFactory;
} else {
factory = newFactory;
}
}
};
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
// have the patched version
for (const wreq of allWebpackInstances) {
define(wreq.m, id, descriptor);
} }
}
/** // Restore the original factory in all the module factories objects, discarding our proxy and allowing it to be garbage collected
* Wraps and patches a module factory. for (const wreq of allWebpackInstances) {
* define(wreq.m, moduleId, { value: originalFactory });
* @param id The id of the module }
* @param factory The original or patched module factory
* @returns The wrapper for the patched module factory
*/
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
const [patchedFactory, patchedSource, patchedBy] = patchFactory(id, originalFactory);
const wrappedFactory: WrappedModuleFactory = function (...args) { let [module, exports, require] = argArray;
// Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance
for (const wreq of allWebpackInstances) {
define(wreq.m, id, { value: wrappedFactory[SYM_ORIGINAL_FACTORY] });
}
// eslint-disable-next-line prefer-const if (wreq == null) {
let [module, exports, require] = args; if (!wreqFallbackApplied) {
wreqFallbackApplied = true;
if (wreq == null) { // Make sure the require argument is actually the WebpackRequire function
if (!wreqFallbackApplied) { if (typeof require === "function" && require.m != null) {
wreqFallbackApplied = true; const { stack } = new Error();
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
// Make sure the require argument is actually the WebpackRequire function logger.warn(
if (typeof require === "function" && require.m != null) { "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called wrapped module factory (" +
const { stack } = new Error(); `id: ${String(moduleId)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; ")"
);
logger.warn( // Could technically be wrong, but it's better than nothing
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + _initWebpack(require as WebpackRequire);
`id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
")"
);
_initWebpack(require as WebpackRequire);
} else if (IS_DEV) {
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
} else if (IS_DEV) { } else if (IS_DEV) {
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args); logger.error("WebpackRequire was not initialized, running modules without patches instead.");
return originalFactory.apply(thisArg, argArray);
} }
} else if (IS_DEV) {
return originalFactory.apply(thisArg, argArray);
}
}
let factoryReturn: unknown;
try {
factoryReturn = patchedFactory.apply(thisArg, argArray);
} catch (err) {
// Just re-throw Discord errors
if (patchedFactory === originalFactory) {
throw err;
} }
let factoryReturn: unknown; logger.error("Error in patched module factory:\n", err);
try { return originalFactory.apply(thisArg, argArray);
// Call the patched factory }
factoryReturn = patchedFactory.apply(this, args);
} catch (err) { exports = module.exports;
// Just re-throw Discord errors if (exports == null) {
if (patchedFactory === wrappedFactory[SYM_ORIGINAL_FACTORY]) { return factoryReturn;
throw err; }
if (typeof require === "function") {
const shouldIgnoreModule = _shouldIgnoreModule(exports);
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, moduleId, {
value: require.c[moduleId],
enumerable: false,
configurable: true,
writable: true
});
} }
logger.error("Error in patched module factory:\n", err);
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
exports = module.exports;
if (exports == null) {
return factoryReturn; return factoryReturn;
} }
if (typeof require === "function") {
const shouldIgnoreModule = _shouldIgnoreModule(exports);
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
}
return factoryReturn;
}
}
for (const callback of moduleListeners) {
try {
callback(exports, id);
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
}
}
for (const [filter, callback] of waitForSubscriptions) {
try {
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, id);
continue;
}
if (typeof exports !== "object") {
continue;
}
for (const exportKey in exports) {
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, id);
break;
}
}
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
return factoryReturn;
};
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
wrappedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
if (IS_DEV && patchedFactory !== originalFactory) {
wrappedFactory[SYM_PATCHED_SOURCE] = patchedSource;
wrappedFactory[SYM_PATCHED_BY] = patchedBy;
originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
originalFactory[SYM_PATCHED_BY] = patchedBy;
} }
// @ts-expect-error Allow GC to get into action, if possible for (const callback of moduleListeners) {
originalFactory = undefined; try {
return wrappedFactory; callback(exports, moduleId);
} catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback);
}
}
for (const [filter, callback] of waitForSubscriptions) {
try {
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, moduleId);
continue;
}
if (typeof exports !== "object") {
continue;
}
for (const exportKey in exports) {
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, moduleId);
break;
}
}
} catch (err) {
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
return factoryReturn;
} }
/** /**
* Patches a module factory. * Patches a module factory.
* *
* @param id The id of the module * @param moduleId The id of the module
* @param factory The original module factory * @param originalFactory The original module factory
* @returns The patched module factory, the patched source of it, and the plugins that patched it * @returns The patched module factory
*/ */
function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFactory: AnyModuleFactory, patchedSource: string, patchedBy: Set<string>] { function patchFactory(moduleId: PropertyKey, originalFactory: AnyModuleFactory): PatchedModuleFactory {
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + String(factory); let code: string = "0," + String(originalFactory);
let patchedSource = code; let patchedSource = code;
let patchedFactory = factory; let patchedFactory = originalFactory;
const patchedBy = new Set<string>(); const patchedBy = new Set<string>();
@ -442,8 +430,8 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
continue; continue;
} }
// Reporter eagerly patches and cannot retrieve the build number because this code runs before the module for it is loaded // Eager patches cannot retrieve the build number because this code runs before the module for it is loaded
const buildNumber = IS_REPORTER ? -1 : getBuildNumber(); const buildNumber = Settings.eagerPatches ? -1 : getBuildNumber();
const shouldCheckBuildNumber = !Settings.eagerPatches && buildNumber !== -1; const shouldCheckBuildNumber = !Settings.eagerPatches && buildNumber !== -1;
if ( if (
@ -463,7 +451,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
}); });
const previousCode = code; const previousCode = code;
const previousFactory = factory; const previousFactory = originalFactory;
let markedAsPatched = false; let markedAsPatched = false;
// We change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
@ -482,18 +470,18 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
} }
const lastCode = code; const lastCode = code;
const lastFactory = factory; const lastFactory = originalFactory;
try { try {
const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string); const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
if (IS_REPORTER) { if (IS_REPORTER) {
patchTimings.push([patch.plugin, id, replacement.match, totalTime]); patchTimings.push([patch.plugin, moduleId, replacement.match, totalTime]);
} }
if (newCode === code) { if (newCode === code) {
if (!patch.noWarn) { if (!patch.noWarn) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`); logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(moduleId)}): ${replacement.match}`);
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); logger.debug("Function Source:\n", code);
} }
@ -515,7 +503,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
} }
code = newCode; code = newCode;
patchedSource = `// Webpack Module ${String(id)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`; patchedSource = `// Webpack Module ${String(moduleId)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(moduleId)}`;
patchedFactory = (0, eval)(patchedSource); patchedFactory = (0, eval)(patchedSource);
if (!patchedBy.has(patch.plugin)) { if (!patchedBy.has(patch.plugin)) {
@ -523,7 +511,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
markedAsPatched = true; markedAsPatched = true;
} }
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(moduleId)}): ${replacement.match}\n`, err);
if (IS_DEV) { if (IS_DEV) {
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!); diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
@ -550,7 +538,14 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFacto
} }
} }
return [patchedFactory, patchedSource, patchedBy]; patchedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
if (IS_DEV && patchedFactory !== originalFactory) {
originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
originalFactory[SYM_PATCHED_BY] = patchedBy;
}
return patchedFactory as PatchedModuleFactory;
} }
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) { function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {

View file

@ -90,7 +90,7 @@ export const filters = {
}; };
export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;
export type FactoryListernFn = (factory: AnyModuleFactory) => void; export type FactoryListernFn = (factory: AnyModuleFactory, moduleId: PropertyKey) => void;
export const waitForSubscriptions = new Map<FilterFn, CallbackFn>(); export const waitForSubscriptions = new Map<FilterFn, CallbackFn>();
export const moduleListeners = new Set<CallbackFn>(); export const moduleListeners = new Set<CallbackFn>();

View file

@ -200,12 +200,10 @@ export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: M
[SYM_PATCHED_BY]?: Set<string>; [SYM_PATCHED_BY]?: Set<string>;
}; };
export type WrappedModuleFactory = AnyModuleFactory & { export type PatchedModuleFactory = AnyModuleFactory & {
[SYM_ORIGINAL_FACTORY]: AnyModuleFactory; [SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
[SYM_PATCHED_SOURCE]?: string; [SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>; [SYM_PATCHED_BY]?: Set<string>;
}; };
export type MaybeWrappedModuleFactory = AnyModuleFactory | WrappedModuleFactory; export type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory;
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;

View file

@ -29,7 +29,9 @@
"@shared/*": ["./shared/*"], "@shared/*": ["./shared/*"],
"@webpack/types": ["./webpack/common/types"], "@webpack/types": ["./webpack/common/types"],
"@webpack/common": ["./webpack/common"], "@webpack/common": ["./webpack/common"],
"@webpack": ["./webpack/webpack"] "@webpack": ["./webpack/webpack"],
"@webpack/patcher": ["./webpack/patchWebpack"],
"@webpack/wreq.d": ["./webpack/wreq.d"],
}, },
"plugins": [ "plugins": [