From 3ab68f929677e129ff3a43c70b2e04b99768c71b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 06:04:21 -0300 Subject: [PATCH] Option for eager patching --- scripts/generateReport.ts | 2 +- src/api/Settings.ts | 4 +- src/components/VencordSettings/VencordTab.tsx | 5 + src/plugins/devCompanion.dev/index.tsx | 3 +- src/webpack/patchWebpack.ts | 125 +++++++++++++----- 5 files changed, 100 insertions(+), 39 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 4ed44f7b4..7fc435249 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -285,7 +285,7 @@ async function runtime(token: string) { Object.defineProperty(navigator, "languages", { get: function () { return ["en-US", "en"]; - }, + } }); // Monkey patch Logger to not log with custom css diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 490e6ef7f..a96e6ca4e 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -32,9 +32,10 @@ export interface Settings { autoUpdate: boolean; autoUpdateNotification: boolean, useQuickCss: boolean; - enableReactDevtools: boolean; themeLinks: string[]; + eagerPatches: boolean; enabledThemes: string[]; + enableReactDevtools: boolean; frameless: boolean; transparent: boolean; winCtrlQ: boolean; @@ -81,6 +82,7 @@ const DefaultSettings: Settings = { autoUpdateNotification: true, useQuickCss: true, themeLinks: [], + eagerPatches: false, enabledThemes: [], enableReactDevtools: false, frameless: false, diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index c0a66fdc7..c9702b435 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -66,6 +66,11 @@ function VencordSettings() { title: "Enable React Developer Tools", note: "Requires a full restart" }, + { + key: "eagerPatches", + title: "Apply Vencord patches before they are needed", + note: "Increases startup timing, but may make app usage more fluid. Note that the difference of having this on or off is minimal." + }, !IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? { key: "frameless", title: "Disable the window frame", diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 25fd563e4..37834e6a2 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -160,7 +160,8 @@ function initWs(isManual = false) { return reply("Expected exactly one 'find' matches, found " + keys.length); const mod = candidates[keys[0]]; - let src = String(mod.original ?? mod).replaceAll("\n", ""); + // @ts-ignore + let src = String(mod.$$vencordOriginal ?? mod).replaceAll("\n", ""); if (src.startsWith("function(")) { src = "0," + src; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 918446935..f5ebea7e1 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; @@ -16,23 +17,53 @@ import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, mod const logger = new Logger("WebpackInterceptor", "#8caaee"); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); +const allProxiedModules = new Set(); + const modulesProxyhandler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => [propName, (...args: any[]) => Reflect[propName](...args)] )), get: (target, p) => { - const mod = Reflect.get(target, p); + const propValue = Reflect.get(target, p); - // If the property is not a module id, return the value of it without trying to patch + // If the property is not a number, we are not dealing with a module factory + // $$vencordOriginal means the factory is already patched, $$vencordRequired means it has already been required + // and replaced with the original // @ts-ignore - if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod; + if (propValue == null || Number.isNaN(Number(p)) || propValue.$$vencordOriginal != null || propValue.$$vencordRequired === true) { + return propValue; + } - const patchedMod = patchFactory(p, mod); - Reflect.set(target, p, patchedMod); + // This patches factories if eagerPatches are disabled + const patchedFactory = patchFactory(p, propValue); + Reflect.set(target, p, patchedFactory); - return patchedMod; + return patchedFactory; + }, + set: (target, p, newValue) => { + // $$vencordRequired means we are resetting the factory to its original after being required + // If the property is not a number, we are not dealing with a module factory + if (!Settings.eagerPatches || newValue?.$$vencordRequired === true || Number.isNaN(Number(p))) { + return Reflect.set(target, p, newValue); + } + + const existingFactory = Reflect.get(target, p); + + // Check if this factory is already patched + // @ts-ignore + if (existingFactory?.$$vencordOriginal === newValue) { + return true; + } + + const patchedFactory = patchFactory(p, newValue); + + // Modules are only patched once, so we need to set the patched factory on all the modules + for (const proxiedModules of allProxiedModules) { + Reflect.set(proxiedModules, p, patchedFactory); + } + + return true; }, - set: (target, p, newValue) => Reflect.set(target, p, newValue), ownKeys: target => { const keys = Reflect.ownKeys(target); for (const key of UNCONFIGURABLE_PROPERTIES) { @@ -63,7 +94,9 @@ Object.defineProperty(Function.prototype, "O", { if (callback != null && initCallbackRegex.test(callback.toString())) { Object.defineProperty(this, "O", { value: originalOnChunksLoaded, - configurable: true + configurable: true, + enumerable: true, + writable: true }); const wreq = this; @@ -104,15 +137,17 @@ Object.defineProperty(Function.prototype, "O", { Object.defineProperty(this, "O", { value: onChunksLoaded, - configurable: true + configurable: true, + enumerable: true, + writable: true }); } }); // wreq.m is the webpack object containing module factories. -// This is pre-populated with modules, and is also populated via webpackGlobal.push -// The sentry module also has their own webpack with a pre-populated modules object, so this also targets that -// We replace its prototype with our proxy, which is responsible for returning patched module factories containing our patches +// This is pre-populated with module factories, and is also populated via webpackGlobal.push +// The sentry module also has their own webpack with a pre-populated module factories object, so this also targets that +// We replace its prototype with our proxy, which is responsible for patching the module factories Object.defineProperty(Function.prototype, "m", { configurable: true, @@ -124,35 +159,47 @@ Object.defineProperty(Function.prototype, "m", { logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); // The new object which will contain the factories - const modules = Object.assign({}, originalModules); + const proxiedModules: WebpackRequire["m"] = {}; - // Clear the original object so pre-populated factories are patched - for (const propName in originalModules) { - delete originalModules[propName]; + for (const id in originalModules) { + // If we have eagerPatches enabled we have to patch the pre-populated factories + if (Settings.eagerPatches) { + proxiedModules[id] = patchFactory(id, originalModules[id]); + } else { + proxiedModules[id] = originalModules[id]; + } + + // Clear the original object so pre-populated factories are patched if eagerPatches are disabled + delete originalModules[id]; } - Object.setPrototypeOf(originalModules, new Proxy(modules, modulesProxyhandler)); + // @ts-ignore + originalModules.$$proxiedModules = proxiedModules; + allProxiedModules.add(proxiedModules); + Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyhandler)); } Object.defineProperty(this, "m", { value: originalModules, - configurable: true + configurable: true, + enumerable: true, + writable: true }); } }); let webpackNotInitializedLogged = false; -function patchFactory(id: PropertyKey, mod: ModuleFactory) { +function patchFactory(id: PropertyKey, factory: ModuleFactory) { for (const factoryListener of factoryListeners) { try { - factoryListener(mod); + factoryListener(factory); } catch (err) { logger.error("Error in Webpack factory listener:\n", err, factoryListener); } } - const originalMod = mod; + const originalFactory = factory; const patchedBy = new Set(); // Discords Webpack chunks for some ungodly reason contain random @@ -164,7 +211,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { // cause issues. // // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, - let code: string = "0," + mod.toString().replaceAll("\n", ""); + let code: string = "0," + factory.toString().replaceAll("\n", ""); for (let i = 0; i < patches.length; i++) { const patch = patches[i]; @@ -179,14 +226,14 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { patchedBy.add(patch.plugin); const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); - const previousMod = mod; + const previousFactory = factory; const previousCode = code; // We change all patch.replacement to array in plugins/index for (const replacement of patch.replacement as PatchReplacement[]) { if (replacement.predicate && !replacement.predicate()) continue; - const lastMod = mod; + const lastFactory = factory; const lastCode = code; canonicalizeReplacement(replacement, patch.plugin); @@ -203,7 +250,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); - mod = previousMod; + factory = previousFactory; code = previousCode; patchedBy.delete(patch.plugin); break; @@ -213,7 +260,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { } code = newCode; - mod = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`); + factory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`); } catch (err) { logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); @@ -254,12 +301,12 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); - mod = previousMod; + factory = previousFactory; code = previousCode; break; } - mod = lastMod; + factory = lastFactory; code = lastCode; } } @@ -268,23 +315,29 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { } const patchedFactory: ModuleFactory = (module, exports, require) => { + // @ts-ignore + originalFactory.$$vencordRequired = true; + for (const proxiedModules of allProxiedModules) { + proxiedModules[id] = originalFactory; + } + if (wreq == null && IS_DEV) { if (!webpackNotInitializedLogged) { webpackNotInitializedLogged = true; logger.error("WebpackRequire was not initialized, running modules without patches instead."); } - return void originalMod(module, exports, require); + return void originalFactory(module, exports, require); } try { - mod(module, exports, require); + factory(module, exports, require); } catch (err) { // Just rethrow Discord errors - if (mod === originalMod) throw err; + if (factory === originalFactory) throw err; logger.error("Error in patched module", err); - return void originalMod(module, exports, require); + return void originalFactory(module, exports, require); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it @@ -297,8 +350,8 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { Object.defineProperty(require.c, id, { value: require.c[id], configurable: true, - writable: true, - enumerable: false + enumerable: false, + writable: true }); return; } @@ -326,9 +379,9 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) { } }; - patchedFactory.toString = originalMod.toString.bind(originalMod); + patchedFactory.toString = originalFactory.toString.bind(originalFactory); // @ts-ignore - patchedFactory.$$vencordOriginal = originalMod; + patchedFactory.$$vencordOriginal = originalFactory; return patchedFactory; }