From 2a77941dff67fa0825b729af91be9432286c03f6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 19 May 2024 22:49:58 -0300 Subject: [PATCH 001/125] Proxy modules object for patching --- scripts/generateReport.ts | 6 + src/utils/constants.ts | 1 - src/utils/lazy.ts | 10 +- src/utils/misc.tsx | 3 + src/webpack/patchWebpack.ts | 431 ++++++++++++++++-------------------- 5 files changed, 210 insertions(+), 241 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 164e409df6..6462d91a06 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -465,6 +465,12 @@ async function runtime(token: string) { } })); + // Call the getter for all the values in the modules object + // So modules that were not required get patched by our proxy + for (const id in wreq!.m) { + wreq!.m[id]; + } + console.log("[PUP_DEBUG]", "Finished loading all chunks!"); for (const patch of Vencord.Plugins.patches) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 44d13b54cf..01125c1af1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const SUPPORT_CHANNEL_ID = "1026515880080842772"; diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index a61785df96..b05855fa09 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { UNCONFIGURABLE_PROPERTIES } from "./misc"; + export function makeLazy(factory: () => T, attempts = 5): () => T { let tries = 0; let cache: T; @@ -29,10 +31,6 @@ export function makeLazy(factory: () => T, attempts = 5): () => T { }; } -// Proxies demand that these properties be unmodified, so proxyLazy -// will always return the function default for them. -const unconfigurable = ["arguments", "caller", "prototype"]; - const handler: ProxyHandler = {}; const kGET = Symbol.for("vencord.lazy.get"); @@ -59,14 +57,14 @@ for (const method of [ handler.ownKeys = target => { const v = target[kGET](); const keys = Reflect.ownKeys(v); - for (const key of unconfigurable) { + for (const key of UNCONFIGURABLE_PROPERTIES) { if (!keys.includes(key)) keys.push(key); } return keys; }; handler.getOwnPropertyDescriptor = (target, p) => { - if (typeof p === "string" && unconfigurable.includes(p)) + if (typeof p === "string" && UNCONFIGURABLE_PROPERTIES.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); const descriptor = Reflect.getOwnPropertyDescriptor(target[kGET](), p); diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index fb08c93f6f..5bf2c23984 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -99,3 +99,6 @@ export const isPluginDev = (id: string) => Object.hasOwn(DevsById, id); export function pluralise(amount: number, singular: string, plural = singular + "s") { return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; } + +/** Unconfigurable properties for proxies */ +export const UNCONFIGURABLE_PROPERTIES = ["arguments", "caller", "prototype"]; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index c7e424671f..9eaddb3d3b 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -1,23 +1,11 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { WEBPACK_CHUNK } from "@utils/constants"; + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + import { Logger } from "@utils/Logger"; +import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; import { WebpackInstance } from "discord-types/other"; @@ -29,29 +17,30 @@ import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, s const logger = new Logger("WebpackInterceptor", "#8caaee"); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); -let webpackChunk: any[]; - -// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed -// This way we can patch the factory of everything being pushed to the modules array -Object.defineProperty(window, WEBPACK_CHUNK, { - configurable: true, - - get: () => webpackChunk, - set: v => { - if (v?.push) { - if (!v.push.$$vencordOriginal) { - logger.info(`Patching ${WEBPACK_CHUNK}.push`); - patchPush(v); - - // @ts-ignore - delete window[WEBPACK_CHUNK]; - window[WEBPACK_CHUNK] = v; - } +const modulesProxyhandler: ProxyHandler = { + ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => + [propName, (target: any, ...args: any[]) => Reflect[propName](target, ...args)] + )), + get: (target, p: string) => { + const mod = Reflect.get(target, p); + + // If the property is not a module id, return the value of it without trying to patch + if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod; + + const patchedMod = patchFactory(p, mod); + Reflect.set(target, p, patchedMod); + + return patchedMod; + }, + set: (target, p, newValue) => Reflect.set(target, p, newValue), + ownKeys: target => { + const keys = Reflect.ownKeys(target); + for (const key of UNCONFIGURABLE_PROPERTIES) { + if (!keys.includes(key)) keys.push(key); } - - webpackChunk = v; + return keys; } -}); +}; // wreq.O is the webpack onChunksLoaded function // Discord uses it to await for all the chunks to be loaded before initializing the app @@ -108,251 +97,225 @@ Object.defineProperty(Function.prototype, "O", { } }); -// wreq.m is the webpack module factory. -// normally, this is populated via webpackGlobal.push, which we patch below. -// However, Discord has their .m prepopulated. -// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories -// -// Update: Discord now has TWO webpack instances. Their normal one and sentry -// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules +// wreq.m is the webpack object containing module factories. +// This is pre-populated with modules, and is also populated via webpackGlobal.push +// We replace its prototype with our proxy, which is responsible for returning patched module factories containing our patches Object.defineProperty(Function.prototype, "m", { configurable: true, - set(v: any) { + set(originalModules: any) { // When using react devtools or other extensions, we may also catch their webpack here. // This ensures we actually got the right one const { stack } = new Error(); if (stack?.includes("discord.com") || stack?.includes("discordapp.com")) { logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); - patchFactories(v); + + // The new object which will contain the factories + const modules = Object.assign({}, originalModules); + + // Clear the original object so pre-populated factories are patched + for (const propName in originalModules) { + delete originalModules[propName]; + } + + Object.setPrototypeOf(originalModules, new Proxy(modules, modulesProxyhandler)); } Object.defineProperty(this, "m", { - value: v, + value: originalModules, configurable: true }); } }); -function patchPush(webpackGlobal: any) { - function handlePush(chunk: any) { +let webpackNotInitializedLogged = false; + +function patchFactory(id: string, mod: (module: any, exports: any, require: WebpackInstance) => void) { + for (const factoryListener of factoryListeners) { try { - patchFactories(chunk[1]); + factoryListener(mod); } catch (err) { - logger.error("Error in handlePush", err); + logger.error("Error in Webpack factory listener:\n", err, factoryListener); } - - return handlePush.$$vencordOriginal.call(webpackGlobal, chunk); } - handlePush.$$vencordOriginal = webpackGlobal.push; - handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal); - // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` - // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. - // If we then repatched the new push, we would end up with recursive patching, which leads to our patches - // being applied multiple times. - // Thus, override bind to use the original push - handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args); - - Object.defineProperty(webpackGlobal, "push", { - configurable: true, - - get: () => handlePush, - set(v) { - handlePush.$$vencordOriginal = v; - } - }); -} + const originalMod = mod; + const patchedBy = new Set(); -let webpackNotInitializedLogged = false; + // Discords Webpack chunks for some ungodly reason contain random + // newlines. Cyn recommended this workaround and it seems to work fine, + // however this could potentially break code, so if anything goes weird, + // this is probably why. + // Additionally, `[actual newline]` is one less char than "\n", so if Discord + // ever targets newer browsers, the minifier could potentially use this trick and + // 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", ""); -function patchFactories(factories: Record void>) { - for (const id in factories) { - let mod = factories[id]; + for (let i = 0; i < patches.length; i++) { + const patch = patches[i]; + if (patch.predicate && !patch.predicate()) continue; - const originalMod = mod; - const patchedBy = new Set(); + const moduleMatches = typeof patch.find === "string" + ? code.includes(patch.find) + : patch.find.test(code); - const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) { - if (wreq == null && IS_DEV) { - if (!webpackNotInitializedLogged) { - webpackNotInitializedLogged = true; - logger.error("WebpackRequire was not initialized, running modules without patches instead."); - } + if (!moduleMatches) continue; - return void originalMod(module, exports, require); - } + patchedBy.add(patch.plugin); - try { - mod(module, exports, require); - } catch (err) { - // Just rethrow discord errors - if (mod === originalMod) throw err; + const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); + const previousMod = mod; + const previousCode = code; - logger.error("Error in patched module", err); - return void originalMod(module, exports, require); - } + // We change all patch.replacement to array in plugins/index + for (const replacement of patch.replacement as PatchReplacement[]) { + if (replacement.predicate && !replacement.predicate()) continue; - exports = module.exports; + const lastMod = mod; + const lastCode = code; - if (!exports) return; + canonicalizeReplacement(replacement, patch.plugin); - // There are (at the time of writing) 11 modules exporting the window - // Make these non enumerable to improve webpack search performance - if (exports === window && require.c) { - Object.defineProperty(require.c, id, { - value: require.c[id], - enumerable: false, - configurable: true, - writable: true - }); - return; - } + try { + const newCode = executePatch(replacement.match, replacement.replace as string); + if (newCode === code) { + if (!patch.noWarn) { + logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); + if (IS_DEV) { + logger.debug("Function Source:\n", code); + } + } - for (const callback of moduleListeners) { - try { - callback(exports, id); - } catch (err) { - logger.error("Error in Webpack module listener:\n", err, callback); + if (patch.group) { + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); + mod = previousMod; + code = previousCode; + patchedBy.delete(patch.plugin); + break; + } + + continue; } - } - for (const [filter, callback] of subscriptions) { - try { - if (filter(exports)) { - subscriptions.delete(filter); - callback(exports, id); - } else if (exports.default && filter(exports.default)) { - subscriptions.delete(filter); - callback(exports.default, id); + code = newCode; + mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); + } catch (err) { + logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); + + if (IS_DEV) { + const changeSize = code.length - lastCode.length; + const match = lastCode.match(replacement.match)!; + + // Use 200 surrounding characters of context + const start = Math.max(0, match.index! - 200); + const end = Math.min(lastCode.length, match.index! + match[0].length + 200); + // (changeSize may be negative) + const endPatched = end + changeSize; + + const context = lastCode.slice(start, end); + const patchedContext = code.slice(start, endPatched); + + // inline require to avoid including it in !IS_DEV builds + const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); + let fmt = "%c %s "; + const elements = [] as string[]; + for (const d of diff) { + const color = d.removed + ? "red" + : d.added + ? "lime" + : "grey"; + fmt += "%c%s"; + elements.push("color:" + color, d.value); } - } catch (err) { - logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); + + logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); + logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); + const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); + logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); } - } - } as any as { toString: () => string, original: any, (...args: any[]): void; }; - factory.toString = originalMod.toString.bind(originalMod); - factory.original = originalMod; + patchedBy.delete(patch.plugin); - for (const factoryListener of factoryListeners) { - try { - factoryListener(originalMod); - } catch (err) { - logger.error("Error in Webpack factory listener:\n", err, factoryListener); + if (patch.group) { + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); + mod = previousMod; + code = previousCode; + break; + } + + mod = lastMod; + code = lastCode; } } - // Discords Webpack chunks for some ungodly reason contain random - // newlines. Cyn recommended this workaround and it seems to work fine, - // however this could potentially break code, so if anything goes weird, - // this is probably why. - // Additionally, `[actual newline]` is one less char than "\n", so if Discord - // ever targets newer browsers, the minifier could potentially use this trick and - // 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", ""); - - for (let i = 0; i < patches.length; i++) { - const patch = patches[i]; - if (patch.predicate && !patch.predicate()) continue; - - const moduleMatches = typeof patch.find === "string" - ? code.includes(patch.find) - : patch.find.test(code); - - if (!moduleMatches) continue; - - patchedBy.add(patch.plugin); - - const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); - const previousMod = mod; - 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 lastCode = code; - - canonicalizeReplacement(replacement, patch.plugin); - - try { - const newCode = executePatch(replacement.match, replacement.replace as string); - if (newCode === code) { - if (!patch.noWarn) { - logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); - if (IS_DEV) { - logger.debug("Function Source:\n", code); - } - } + if (!patch.all) patches.splice(i--, 1); + } - if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); - mod = previousMod; - code = previousCode; - patchedBy.delete(patch.plugin); - break; - } + function patchedFactory(module: any, exports: any, require: WebpackInstance) { + if (wreq == null && IS_DEV) { + if (!webpackNotInitializedLogged) { + webpackNotInitializedLogged = true; + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + } - continue; - } + return void originalMod(module, exports, require); + } - code = newCode; - mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); - } catch (err) { - logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); - - if (IS_DEV) { - const changeSize = code.length - lastCode.length; - const match = lastCode.match(replacement.match)!; - - // Use 200 surrounding characters of context - const start = Math.max(0, match.index! - 200); - const end = Math.min(lastCode.length, match.index! + match[0].length + 200); - // (changeSize may be negative) - const endPatched = end + changeSize; - - const context = lastCode.slice(start, end); - const patchedContext = code.slice(start, endPatched); - - // inline require to avoid including it in !IS_DEV builds - const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); - let fmt = "%c %s "; - const elements = [] as string[]; - for (const d of diff) { - const color = d.removed - ? "red" - : d.added - ? "lime" - : "grey"; - fmt += "%c%s"; - elements.push("color:" + color, d.value); - } + try { + mod(module, exports, require); + } catch (err) { + // Just rethrow Discord errors + if (mod === originalMod) throw err; - logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); - logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); - const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); - logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); - } + logger.error("Error in patched module", err); + return void originalMod(module, exports, require); + } - patchedBy.delete(patch.plugin); + // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it + exports = module.exports; + if (exports == null) return; + + // There are (at the time of writing) 11 modules exporting the window + // Make these non enumerable to improve webpack search performance + if (exports === window && require.c) { + Object.defineProperty(require.c, id, { + value: require.c[id], + configurable: true, + writable: true, + enumerable: false + }); + return; + } - if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); - mod = previousMod; - code = previousCode; - break; - } + for (const callback of moduleListeners) { + try { + callback(exports, id); + } catch (err) { + logger.error("Error in Webpack module listener:\n", err, callback); + } + } - mod = lastMod; - code = lastCode; + for (const [filter, callback] of subscriptions) { + try { + if (filter(exports)) { + subscriptions.delete(filter); + callback(exports, id); + } else if (exports.default && filter(exports.default)) { + subscriptions.delete(filter); + callback(exports.default, id); } + } catch (err) { + logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); } - - if (!patch.all) patches.splice(i--, 1); } } + + patchedFactory.toString = originalMod.toString.bind(originalMod); + // @ts-ignore + patchedFactory.$$vencordOriginal = originalMod; + + return patchedFactory; } From b938bdf13893747b0307ebd2c55a07966dcde6cb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 19 May 2024 23:22:48 -0300 Subject: [PATCH 002/125] Clarify about sentry webpack --- src/webpack/patchWebpack.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 64b6e331a4..979bf05773 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -99,6 +99,7 @@ Object.defineProperty(Function.prototype, "O", { // 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 Object.defineProperty(Function.prototype, "m", { configurable: true, From cf9bbfc78f274a08c36fca2011c2bb69a1240bf9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 01:30:17 -0300 Subject: [PATCH 003/125] stop annoying me --- scripts/generateReport.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index a83949c88b..5808b4585d 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -456,17 +456,18 @@ async function runtime(token: string) { }); await chunksSearchingDone; + wreq = wreq!; // Require deferred entry points for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); + wreq(deferredRequire as any); } // All chunks Discord has mapped to asset files, even if they are not used anymore const allChunks = [] as string[]; // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + for (const currentMatch of wreq.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { const id = currentMatch[1] ?? currentMatch[2]; if (id == null) continue; @@ -494,8 +495,8 @@ async function runtime(token: string) { // Call the getter for all the values in the modules object // So modules that were not required get patched by our proxy - for (const id in wreq!.m) { - wreq!.m[id]; + for (const id in wreq.m) { + wreq.m[id]; } console.log("[PUP_DEBUG]", "Finished loading all chunks!"); From c4645f79c663b455bbb81301137af7f843d1ab9a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 06:08:28 -0300 Subject: [PATCH 004/125] Make patchedBy a string set --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 979bf05773..71348b8892 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -141,7 +141,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp } const originalMod = mod; - const patchedBy = new Set(); + const patchedBy = new Set(); // Discords Webpack chunks for some ungodly reason contain random // newlines. Cyn recommended this workaround and it seems to work fine, From 4b2734610f88c1ea14262ba9322a544f167fa268 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 06:08:40 -0300 Subject: [PATCH 005/125] Add WebpackRequire typings --- src/webpack/wreq.d.ts | 140 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/webpack/wreq.d.ts diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts new file mode 100644 index 0000000000..517ad4b990 --- /dev/null +++ b/src/webpack/wreq.d.ts @@ -0,0 +1,140 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +type AnyRecord = Record; + +type ModuleExports = any; + +type Module = { + id: PropertyKey; + loaded: boolean; + exports: ModuleExports; +}; + +/** exports ({@link ModuleExports}) can be anything, however initially it is always an empty object */ +type ModuleFactory = (module: Module, exports: AnyRecord, require: WebpackRequire) => void; + +type AsyncModuleBody = ( + handleDependencies: (deps: Promise[]) => Promise & (() => void) +) => Promise; + +type ChunkHandlers = { + /** + * Ensures the js file for this chunk is loaded, or starts to load if it's not + * @param chunkId The chunk id + * @param promises The promises array to add the loading promise to. + */ + j: (chunkId: string | number, promises: Promise) => void, + /** + * Ensures the css file for this chunk is loaded, or starts to load if it's not + * @param chunkId The chunk id + * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too. + */ + css: (chunkId: string | number, promises: Promise) => void, +}; + +type ScriptLoadDone = (event: Event) => void; + +type OnChunksLoaded = ((result: any, chunkIds: (string | number)[] | undefined, callback: () => any, priority: number) => any) & { + /** Check if a chunk has been loaded */ + j: (chunkId: string | number) => boolean; +}; + +type WebpackRequire = ((moduleId: PropertyKey) => Module) & { + /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ + m: Record; + /** The module cache, where all modules which have been WebpackRequire'd are stored */ + c: Record; + /** + * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: + * @example + * const fromObject = { a: 1 }; + * Object.defineProperty(to, "a", { + * get: () => fromObject.a + * }); + * @returns fromObject + */ + es: (fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord; + /** + * Creates an async module. The body function must be a async function. + * "module.exports" will be decorated with an AsyncModulePromise. + * The body function will be called. + * To handle async dependencies correctly do this inside the body: "([a, b, c] = await handleDependencies([a, b, c]));". + * If "hasAwaitAfterDependencies" is truthy, "handleDependencies()" must be called at the end of the body function. + */ + a: (module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void; + /** getDefaultExport function for compatibility with non-harmony modules */ + n: (module: Module) => () => ModuleExports; + /** + * Create a fake namespace object, useful for faking an __esModule with a default export. + * + * mode & 1: Value is a module id, require it + * + * mode & 2: Merge all properties of value into the namespace + * + * mode & 4: Return value when already namespace object + * + * mode & 16: Return value when it's Promise-like + * + * mode & (8|1): Behave like require + */ + t: (value: any, mode: number) => any; + /** + * Define property getters. For every prop in "definiton", set a getter in "exports" for the value in "definitiion", like this: + * @example + * const exports = {}; + * const definition = { a: 1 }; + * for (const key in definition) { + * Object.defineProperty(exports, key, { get: definition[key] } + * } + */ + d: (exports: AnyRecord, definiton: AnyRecord) => void; + /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ + f: ChunkHandlers; + /** + * The ensure chunk function, it ensures a chunk is loaded, or loads if needed. + * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded. + */ + e: (chunkId: string | number) => Promise; + /** Get the filename name for the css part of a chunk */ + k: (chunkId: string | number) => `${chunkId}.css`; + /** Get the filename for the js part of a chunk */ + u: (chunkId: string | number) => string; + /** The global object, will likely always be the window */ + g: Window; + /** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */ + hmd: (module: Module) => any; + /** Shorthand for Object.prototype.hasOwnProperty */ + o: typeof Object.prototype.hasOwnProperty; + /** + * Function to load a script tag. "done" is called when the loading has finished or a timeout has occurred. + * "done" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`, + * so it will be called when that existing script finishes loading. + */ + l: (url: string, done: ScriptLoadDone, key?: string | number, chunkId?: string | number) => void; + /** Defines __esModule on the exports, marking ES Modules compatibility as true */ + r: (exports: AnyRecord) => void; + /** Node.js module decorator. Decorates a module as a Node.js module */ + nmd: (module: Module) => any; + /** + * Register deferred code which will be executed when the passed chunks are loaded. + * + * If chunkIds is defined, it defers the execution of the callback and returns undefined. + * + * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument. + * + * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code. + * + * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. + */ + O: OnChunksLoaded; + /** Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" */ + v: (exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => void; + /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ + p: string; + /** Document baseURI or WebWorker location.href */ + b: string; +}; From d5bcbf54f9460e9e3ab6734d7efccc579deab839 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 06:17:31 -0300 Subject: [PATCH 006/125] fix --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 517ad4b990..2dbd311500 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -98,7 +98,7 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * The ensure chunk function, it ensures a chunk is loaded, or loads if needed. * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded. */ - e: (chunkId: string | number) => Promise; + e: (chunkId: string | number) => Promise; /** Get the filename name for the css part of a chunk */ k: (chunkId: string | number) => `${chunkId}.css`; /** Get the filename for the js part of a chunk */ From f6a7cdc430b0a6b31f729e4dec2f2cbd2c765afc Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 06:22:37 -0300 Subject: [PATCH 007/125] more fix --- src/webpack/wreq.d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 2dbd311500..25b548caa5 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -131,8 +131,11 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. */ O: OnChunksLoaded; - /** Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" */ - v: (exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => void; + /** + * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" + * @returns The exports of the wasm instance + */ + v: (exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ p: string; /** Document baseURI or WebWorker location.href */ From bc367b1d2a178d0be0192ba524bf4d64879e3ff7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 22 May 2024 06:25:30 -0300 Subject: [PATCH 008/125] and also a fix --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 25b548caa5..c396e615da 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -133,7 +133,7 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { O: OnChunksLoaded; /** * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" - * @returns The exports of the wasm instance + * @returns The exports argument, but now assigned with the exports of the wasm instance */ v: (exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ From 40a1f48267679ac242a6a134f6d1a920ec5a9ebb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 03:10:59 -0300 Subject: [PATCH 009/125] boops --- src/webpack/index.ts | 1 + src/webpack/patchWebpack.ts | 23 ++++++++-------- src/webpack/webpack.ts | 15 ++++++----- src/webpack/wreq.d.ts | 54 ++++++++++++++++++------------------- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/src/webpack/index.ts b/src/webpack/index.ts index 036c2a3fcb..6f1fd25b85 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -18,3 +18,4 @@ export * as Common from "./common"; export * from "./webpack"; +export * from "./wreq.d"; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 71348b8892..b79bec46e2 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -8,20 +8,19 @@ import { Logger } from "@utils/Logger"; import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; -import { WebpackInstance } from "discord-types/other"; import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; -import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from "."; +import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; const logger = new Logger("WebpackInterceptor", "#8caaee"); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); -const modulesProxyhandler: ProxyHandler = { +const modulesProxyhandler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => - [propName, (target: any, ...args: any[]) => Reflect[propName](target, ...args)] + [propName, (target: WebpackRequire["m"], ...args: any[]) => Reflect[propName](target, ...args)] )), - get: (target, p: string) => { + get: (target, p) => { const mod = Reflect.get(target, p); // If the property is not a module id, return the value of it without trying to patch @@ -48,7 +47,7 @@ const modulesProxyhandler: ProxyHandler = { Object.defineProperty(Function.prototype, "O", { configurable: true, - set(onChunksLoaded: any) { + set(onChunksLoaded: WebpackRequire["O"]) { // When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here. // This ensures we actually got the right one // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it @@ -59,14 +58,14 @@ Object.defineProperty(Function.prototype, "O", { delete (Function.prototype as any).O; const originalOnChunksLoaded = onChunksLoaded; - onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) { + onChunksLoaded = function (result, chunkIds, callback, priority) { if (callback != null && initCallbackRegex.test(callback.toString())) { Object.defineProperty(this, "O", { value: originalOnChunksLoaded, configurable: true }); - const wreq = this as WebpackInstance; + const wreq = this; const originalCallback = callback; callback = function (this: unknown) { @@ -85,7 +84,7 @@ Object.defineProperty(Function.prototype, "O", { } originalOnChunksLoaded.apply(this, arguments as any); - }; + } as WebpackRequire["O"]; onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded); } @@ -131,7 +130,7 @@ Object.defineProperty(Function.prototype, "m", { let webpackNotInitializedLogged = false; -function patchFactory(id: string, mod: (module: any, exports: any, require: WebpackInstance) => void) { +function patchFactory(id: string | number, mod: ModuleFactory) { for (const factoryListener of factoryListeners) { try { factoryListener(mod); @@ -255,7 +254,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp if (!patch.all) patches.splice(i--, 1); } - function patchedFactory(module: any, exports: any, require: WebpackInstance) { + const patchedFactory: ModuleFactory = (module, exports, require) => { if (wreq == null && IS_DEV) { if (!webpackNotInitializedLogged) { webpackNotInitializedLogged = true; @@ -312,7 +311,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); } } - } + }; patchedFactory.toString = originalMod.toString.bind(originalMod); // @ts-ignore diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 8548208518..7cd28866ec 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -20,9 +20,9 @@ import { makeLazy, proxyLazy } from "@utils/lazy"; import { LazyComponent } from "@utils/lazyReact"; import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; -import type { WebpackInstance } from "discord-types/other"; import { traceFunction } from "../debug/Tracer"; +import { ModuleExports, ModuleFactory, WebpackRequire } from "./wreq"; const logger = new Logger("Webpack"); @@ -33,8 +33,8 @@ export let _resolveReady: () => void; */ export const onceReady = new Promise(r => _resolveReady = r); -export let wreq: WebpackInstance; -export let cache: WebpackInstance["c"]; +export let wreq: WebpackRequire; +export let cache: WebpackRequire["c"]; export type FilterFn = (mod: any) => boolean; @@ -68,14 +68,14 @@ export const filters = { } }; -export type CallbackFn = (mod: any, id: string) => void; +export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; export const subscriptions = new Map(); export const moduleListeners = new Set(); -export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>(); -export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>(); +export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); +export const beforeInitListeners = new Set<(wreq: WebpackRequire) => void>(); -export function _initWebpack(webpackRequire: WebpackInstance) { +export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; cache = webpackRequire.c; } @@ -500,6 +500,7 @@ export function search(...filters: Array) { const factories = wreq.m; outer: for (const id in factories) { + // @ts-ignore const factory = factories[id].original ?? factories[id]; const str: string = factory.toString(); for (const filter of filters) { diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index c396e615da..d3d38127f1 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -4,46 +4,46 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -type AnyRecord = Record; +export type AnyRecord = Record; -type ModuleExports = any; +export type ModuleExports = any; -type Module = { +export type Module = { id: PropertyKey; loaded: boolean; exports: ModuleExports; }; /** exports ({@link ModuleExports}) can be anything, however initially it is always an empty object */ -type ModuleFactory = (module: Module, exports: AnyRecord, require: WebpackRequire) => void; +export type ModuleFactory = (module: Module, exports: AnyRecord, require: WebpackRequire) => void; -type AsyncModuleBody = ( +export type AsyncModuleBody = ( handleDependencies: (deps: Promise[]) => Promise & (() => void) ) => Promise; -type ChunkHandlers = { +export type ChunkHandlers = { /** * Ensures the js file for this chunk is loaded, or starts to load if it's not * @param chunkId The chunk id * @param promises The promises array to add the loading promise to. */ - j: (chunkId: string | number, promises: Promise) => void, + j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, /** * Ensures the css file for this chunk is loaded, or starts to load if it's not * @param chunkId The chunk id * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too. */ - css: (chunkId: string | number, promises: Promise) => void, + css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, }; -type ScriptLoadDone = (event: Event) => void; +export type ScriptLoadDone = (event: Event) => void; -type OnChunksLoaded = ((result: any, chunkIds: (string | number)[] | undefined, callback: () => any, priority: number) => any) & { +export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { /** Check if a chunk has been loaded */ - j: (chunkId: string | number) => boolean; + j: (chunkId: PropertyKey) => boolean; }; -type WebpackRequire = ((moduleId: PropertyKey) => Module) & { +export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ m: Record; /** The module cache, where all modules which have been WebpackRequire'd are stored */ @@ -52,12 +52,12 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: * @example * const fromObject = { a: 1 }; - * Object.defineProperty(to, "a", { - * get: () => fromObject.a + * Object.defineProperty(fromObject, "a", { + * get: () => fromObject["a"] * }); * @returns fromObject */ - es: (fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord; + es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord; /** * Creates an async module. The body function must be a async function. * "module.exports" will be decorated with an AsyncModulePromise. @@ -65,9 +65,9 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * To handle async dependencies correctly do this inside the body: "([a, b, c] = await handleDependencies([a, b, c]));". * If "hasAwaitAfterDependencies" is truthy, "handleDependencies()" must be called at the end of the body function. */ - a: (module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void; + a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void; /** getDefaultExport function for compatibility with non-harmony modules */ - n: (module: Module) => () => ModuleExports; + n: (this: WebpackRequire, module: Module) => () => ModuleExports; /** * Create a fake namespace object, useful for faking an __esModule with a default export. * @@ -81,7 +81,7 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * * mode & (8|1): Behave like require */ - t: (value: any, mode: number) => any; + t: (this: WebpackRequire, value: any, mode: number) => any; /** * Define property getters. For every prop in "definiton", set a getter in "exports" for the value in "definitiion", like this: * @example @@ -91,22 +91,22 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Object.defineProperty(exports, key, { get: definition[key] } * } */ - d: (exports: AnyRecord, definiton: AnyRecord) => void; + d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void; /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ f: ChunkHandlers; /** * The ensure chunk function, it ensures a chunk is loaded, or loads if needed. * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded. */ - e: (chunkId: string | number) => Promise; + e: (this: WebpackRequire, chunkId: PropertyKey) => Promise; /** Get the filename name for the css part of a chunk */ - k: (chunkId: string | number) => `${chunkId}.css`; + k: (this: WebpackRequire, chunkId: PropertyKey) => `${chunkId}.css`; /** Get the filename for the js part of a chunk */ - u: (chunkId: string | number) => string; + u: (this: WebpackRequire, chunkId: PropertyKey) => string; /** The global object, will likely always be the window */ g: Window; /** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */ - hmd: (module: Module) => any; + hmd: (this: WebpackRequire, module: Module) => any; /** Shorthand for Object.prototype.hasOwnProperty */ o: typeof Object.prototype.hasOwnProperty; /** @@ -114,11 +114,11 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * "done" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`, * so it will be called when that existing script finishes loading. */ - l: (url: string, done: ScriptLoadDone, key?: string | number, chunkId?: string | number) => void; + l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void; /** Defines __esModule on the exports, marking ES Modules compatibility as true */ - r: (exports: AnyRecord) => void; + r: (this: WebpackRequire, exports: AnyRecord) => void; /** Node.js module decorator. Decorates a module as a Node.js module */ - nmd: (module: Module) => any; + nmd: (this: WebpackRequire, module: Module) => any; /** * Register deferred code which will be executed when the passed chunks are loaded. * @@ -135,7 +135,7 @@ type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" * @returns The exports argument, but now assigned with the exports of the wasm instance */ - v: (exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; + v: (this: WebpackRequire, exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ p: string; /** Document baseURI or WebWorker location.href */ From 5ca4e58fad70fcf1fa6a13e798245d85049c9af3 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 03:36:53 -0300 Subject: [PATCH 010/125] Clean up WebpackRequire typings --- scripts/generateReport.ts | 12 ++++++------ src/webpack/patchWebpack.ts | 17 ++++++++++------- src/webpack/webpack.ts | 22 +++++++++++----------- src/webpack/wreq.d.ts | 18 ++++++++---------- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 5808b4585d..4ed44f7b45 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -41,7 +41,7 @@ const browser = await pup.launch({ const page = await browser.newPage(); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); -function maybeGetError(handle: JSHandle) { +async function maybeGetError(handle: JSHandle) { return (handle as JSHandle)?.getProperty("message") .then(m => m.jsonValue()); } @@ -383,7 +383,7 @@ async function runtime(token: string) { await Promise.all( Array.from(validChunkGroups) .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + Promise.all(chunkIds.map(id => wreq.e(id).catch(() => { }))) ) ); @@ -395,7 +395,7 @@ async function runtime(token: string) { continue; } - if (wreq.m[entryPoint]) wreq(entryPoint as any); + if (wreq.m[entryPoint]) wreq(entryPoint); } catch (err) { console.error(err); } @@ -460,7 +460,7 @@ async function runtime(token: string) { // Require deferred entry points for (const deferredRequire of deferredRequires) { - wreq(deferredRequire as any); + wreq(deferredRequire); } // All chunks Discord has mapped to asset files, even if they are not used anymore @@ -488,8 +488,8 @@ async function runtime(token: string) { // Loads and requires a chunk if (!isWasm) { - await wreq.e(id as any); - if (wreq.m[id]) wreq(id as any); + await wreq.e(id); + if (wreq.m[id]) wreq(id); } })); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a08d74c96a..b57b21aa06 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -24,6 +24,7 @@ const modulesProxyhandler: ProxyHandler = { const mod = Reflect.get(target, p); // If the property is not a module id, return the value of it without trying to patch + // @ts-ignore if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod; const patchedMod = patchFactory(p, mod); @@ -90,12 +91,14 @@ Object.defineProperty(Function.prototype, "O", { // Returns whether a chunk has been loaded Object.defineProperty(onChunksLoaded, "j", { + configurable: true, + set(v) { + // @ts-ignore delete onChunksLoaded.j; onChunksLoaded.j = v; originalOnChunksLoaded.j = v; - }, - configurable: true + } }); } @@ -113,7 +116,7 @@ Object.defineProperty(Function.prototype, "O", { Object.defineProperty(Function.prototype, "m", { configurable: true, - set(originalModules: any) { + set(originalModules: WebpackRequire["m"]) { // When using react devtools or other extensions, we may also catch their webpack here. // This ensures we actually got the right one const { stack } = new Error(); @@ -140,7 +143,7 @@ Object.defineProperty(Function.prototype, "m", { let webpackNotInitializedLogged = false; -function patchFactory(id: string | number, mod: ModuleFactory) { +function patchFactory(id: PropertyKey, mod: ModuleFactory) { for (const factoryListener of factoryListeners) { try { factoryListener(mod); @@ -192,7 +195,7 @@ function patchFactory(id: string | number, mod: ModuleFactory) { const newCode = executePatch(replacement.match, replacement.replace as string); if (newCode === code) { if (!patch.noWarn) { - logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); + logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`); if (IS_DEV) { logger.debug("Function Source:\n", code); } @@ -210,9 +213,9 @@ function patchFactory(id: string | number, mod: ModuleFactory) { } code = newCode; - mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); + mod = (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 ${id}): ${replacement.match}\n`, err); + logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); if (IS_DEV) { const changeSize = code.length - lastCode.length; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 7cd28866ec..052351b793 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -36,7 +36,7 @@ export const onceReady = new Promise(r => _resolveReady = r); export let wreq: WebpackRequire; export let cache: WebpackRequire["c"]; -export type FilterFn = (mod: any) => boolean; +export type FilterFn = (module: ModuleExports) => boolean; export const filters = { byProps: (...props: string[]): FilterFn => @@ -129,7 +129,7 @@ export function findAll(filter: FilterFn) { if (typeof filter !== "function") throw new Error("Invalid filter. Expected a function got " + typeof filter); - const ret = [] as any[]; + const ret: ModuleExports[] = []; for (const key in cache) { const mod = cache[key]; if (!mod?.exports) continue; @@ -169,7 +169,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns const filters = filterFns as Array; let found = 0; - const results = Array(length); + const results: ModuleExports[] = Array(length); outer: for (const key in cache) { @@ -496,12 +496,12 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback * @returns Mapping of found modules */ export function search(...filters: Array) { - const results = {} as Record; + const results: WebpackRequire["m"] = {}; const factories = wreq.m; outer: for (const id in factories) { // @ts-ignore - const factory = factories[id].original ?? factories[id]; + const factory = factories[id].$$vencordOriginal ?? factories[id]; const str: string = factory.toString(); for (const filter of filters) { if (typeof filter === "string" && !str.includes(filter)) continue outer; @@ -521,18 +521,18 @@ export function search(...filters: Array) { * so putting breakpoints or similar will have no effect. * @param id The id of the module to extract */ -export function extract(id: string | number) { - const mod = wreq.m[id] as Function; +export function extract(id: PropertyKey) { + const mod = wreq.m[id]; if (!mod) return null; const code = ` -// [EXTRACTED] WebpackModule${id} +// [EXTRACTED] WebpackModule${String(id)} // WARNING: This module was extracted to be more easily readable. // This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!! 0,${mod.toString()} -//# sourceURL=ExtractedWebpackModule${id} +//# sourceURL=ExtractedWebpackModule${String(id)} `; - const extracted = (0, eval)(code); - return extracted as Function; + const extracted: ModuleFactory = (0, eval)(code); + return extracted; } diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index d3d38127f1..c86fa1c49d 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -4,8 +4,6 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -export type AnyRecord = Record; - export type ModuleExports = any; export type Module = { @@ -14,8 +12,8 @@ export type Module = { exports: ModuleExports; }; -/** exports ({@link ModuleExports}) can be anything, however initially it is always an empty object */ -export type ModuleFactory = (module: Module, exports: AnyRecord, require: WebpackRequire) => void; +/** exports can be anything, however initially it is always an empty object */ +export type ModuleFactory = (module: Module, exports: ModuleExports, require: WebpackRequire) => void; export type AsyncModuleBody = ( handleDependencies: (deps: Promise[]) => Promise & (() => void) @@ -40,7 +38,7 @@ export type ScriptLoadDone = (event: Event) => void; export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { /** Check if a chunk has been loaded */ - j: (chunkId: PropertyKey) => boolean; + j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; }; export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { @@ -57,7 +55,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * }); * @returns fromObject */ - es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord; + es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; /** * Creates an async module. The body function must be a async function. * "module.exports" will be decorated with an AsyncModulePromise. @@ -91,7 +89,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Object.defineProperty(exports, key, { get: definition[key] } * } */ - d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void; + d: (this: WebpackRequire, exports: Record, definiton: Record) => void; /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ f: ChunkHandlers; /** @@ -100,7 +98,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { */ e: (this: WebpackRequire, chunkId: PropertyKey) => Promise; /** Get the filename name for the css part of a chunk */ - k: (this: WebpackRequire, chunkId: PropertyKey) => `${chunkId}.css`; + k: (this: WebpackRequire, chunkId: PropertyKey) => string; /** Get the filename for the js part of a chunk */ u: (this: WebpackRequire, chunkId: PropertyKey) => string; /** The global object, will likely always be the window */ @@ -116,7 +114,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { */ l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void; /** Defines __esModule on the exports, marking ES Modules compatibility as true */ - r: (this: WebpackRequire, exports: AnyRecord) => void; + r: (this: WebpackRequire, exports: ModuleExports) => void; /** Node.js module decorator. Decorates a module as a Node.js module */ nmd: (this: WebpackRequire, module: Module) => any; /** @@ -135,7 +133,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" * @returns The exports argument, but now assigned with the exports of the wasm instance */ - v: (this: WebpackRequire, exports: AnyRecord, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; + v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ p: string; /** Document baseURI or WebWorker location.href */ From 688449ed62980e749907a0b474b642f8e8e140fa Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 03:38:06 -0300 Subject: [PATCH 011/125] forgot this! --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index b57b21aa06..86ebbb2dd9 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -11,7 +11,7 @@ import { PatchReplacement } from "@utils/types"; import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; -import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; +import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, OnChunksLoaded, subscriptions, WebpackRequire, wreq } from "."; const logger = new Logger("WebpackInterceptor", "#8caaee"); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); @@ -93,7 +93,7 @@ Object.defineProperty(Function.prototype, "O", { Object.defineProperty(onChunksLoaded, "j", { configurable: true, - set(v) { + set(v: OnChunksLoaded["j"]) { // @ts-ignore delete onChunksLoaded.j; onChunksLoaded.j = v; From 66e1db1fdb9229b81829db7de3678459c0ce0012 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 03:39:59 -0300 Subject: [PATCH 012/125] more --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 86ebbb2dd9..874687c8ab 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -48,7 +48,7 @@ const modulesProxyhandler: ProxyHandler = { Object.defineProperty(Function.prototype, "O", { configurable: true, - set(onChunksLoaded: WebpackRequire["O"]) { + set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) { // When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here. // This ensures we actually got the right one // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it @@ -116,7 +116,7 @@ Object.defineProperty(Function.prototype, "O", { Object.defineProperty(Function.prototype, "m", { configurable: true, - set(originalModules: WebpackRequire["m"]) { + set(this: WebpackRequire, originalModules: WebpackRequire["m"]) { // When using react devtools or other extensions, we may also catch their webpack here. // This ensures we actually got the right one const { stack } = new Error(); From 01a4ac9c13955539f5741f8bfb8d6588e7167199 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 03:42:58 -0300 Subject: [PATCH 013/125] sometimes I'm stupid --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 874687c8ab..918446935d 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -18,7 +18,7 @@ const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); const modulesProxyhandler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => - [propName, (target: WebpackRequire["m"], ...args: any[]) => Reflect[propName](target, ...args)] + [propName, (...args: any[]) => Reflect[propName](...args)] )), get: (target, p) => { const mod = Reflect.get(target, p); 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 014/125] 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 4ed44f7b45..7fc435249f 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 490e6ef7f3..a96e6ca4e3 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 c0a66fdc79..c9702b4350 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 25fd563e46..37834e6a2e 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 918446935d..f5ebea7e17 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 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 (propValue == null || Number.isNaN(Number(p)) || propValue.$$vencordOriginal != null || propValue.$$vencordRequired === true) { + return propValue; + } + + // This patches factories if eagerPatches are disabled + const patchedFactory = patchFactory(p, propValue); + Reflect.set(target, p, patchedFactory); + + 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); + } - // If the property is not a module id, return the value of it without trying to patch + const existingFactory = Reflect.get(target, p); + + // Check if this factory is already patched // @ts-ignore - if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod; + if (existingFactory?.$$vencordOriginal === newValue) { + return true; + } - const patchedMod = patchFactory(p, mod); - Reflect.set(target, p, patchedMod); + 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 patchedMod; + 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"] = {}; + + 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 - for (const propName in originalModules) { - delete originalModules[propName]; + // 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; } From ac61a0377fd49317f22802d31d9653f7641f6d24 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 06:09:47 -0300 Subject: [PATCH 015/125] clean up --- src/plugins/devCompanion.dev/index.tsx | 3 +-- src/webpack/webpack.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 37834e6a2e..819360cfec 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -160,8 +160,7 @@ function initWs(isManual = false) { return reply("Expected exactly one 'find' matches, found " + keys.length); const mod = candidates[keys[0]]; - // @ts-ignore - let src = String(mod.$$vencordOriginal ?? mod).replaceAll("\n", ""); + let src = mod.toString().replaceAll("\n", ""); if (src.startsWith("function(")) { src = "0," + src; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 052351b793..a428b3b557 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -501,7 +501,7 @@ export function search(...filters: Array) { outer: for (const id in factories) { // @ts-ignore - const factory = factories[id].$$vencordOriginal ?? factories[id]; + const factory = factories[id]; const str: string = factory.toString(); for (const filter of filters) { if (typeof filter === "string" && !str.includes(filter)) continue outer; From cfb493c593b2aadb1c5681ca52c66231deb8a36a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 06:22:01 -0300 Subject: [PATCH 016/125] make reporter use eagerPatches --- scripts/generateReport.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 7fc435249f..7ef99b9db6 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -324,6 +324,9 @@ async function runtime(token: string) { }); }); + // Enable eagerPatches to make all patches apply regardless of the module being required + Vencord.Settings.eagerPatches = false; + let wreq: typeof Vencord.Webpack.wreq; const { canonicalizeMatch, Logger } = Vencord.Util; @@ -493,12 +496,6 @@ async function runtime(token: string) { } })); - // Call the getter for all the values in the modules object - // So modules that were not required get patched by our proxy - for (const id in wreq.m) { - wreq.m[id]; - } - console.log("[PUP_DEBUG]", "Finished loading all chunks!"); for (const patch of Vencord.Plugins.patches) { From 7371abbaec9a5af3e79bcba58a21c112c3c0d122 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 06:29:07 -0300 Subject: [PATCH 017/125] lmao --- scripts/generateReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 7ef99b9db6..9026ee788f 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -325,7 +325,7 @@ async function runtime(token: string) { }); // Enable eagerPatches to make all patches apply regardless of the module being required - Vencord.Settings.eagerPatches = false; + Vencord.Settings.eagerPatches = true; let wreq: typeof Vencord.Webpack.wreq; From 1e96638219c0b1d96a754e36021549d6ff48b1f5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 06:38:22 -0300 Subject: [PATCH 018/125] oops! --- src/webpack/webpack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index a428b3b557..20bd93e3bd 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -500,7 +500,6 @@ export function search(...filters: Array) { const factories = wreq.m; outer: for (const id in factories) { - // @ts-ignore const factory = factories[id]; const str: string = factory.toString(); for (const filter of filters) { From 7b7b87316d71ff3a0e3cea36b6897f9b203e14c6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 07:09:03 -0300 Subject: [PATCH 019/125] HORROR --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index f5ebea7e17..e00ae0b134 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -19,7 +19,7 @@ const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); const allProxiedModules = new Set(); -const modulesProxyhandler: ProxyHandler = { +const modulesProxyHandler: ProxyHandler = { ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => [propName, (...args: any[]) => Reflect[propName](...args)] )), @@ -176,7 +176,7 @@ Object.defineProperty(Function.prototype, "m", { // @ts-ignore originalModules.$$proxiedModules = proxiedModules; allProxiedModules.add(proxiedModules); - Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyhandler)); + Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyHandler)); } Object.defineProperty(this, "m", { From bdd4b0f14301abd0c3daa11ff664071f6e7f9aaa Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 19:53:19 -0300 Subject: [PATCH 020/125] Make eagerPatches an internal setting --- src/components/VencordSettings/VencordTab.tsx | 5 ----- src/webpack/patchWebpack.ts | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index c9702b4350..c0a66fdc79 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -66,11 +66,6 @@ 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/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index e00ae0b134..31aaa911c7 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -127,10 +127,17 @@ Object.defineProperty(Function.prototype, "O", { configurable: true, set(v: OnChunksLoaded["j"]) { - // @ts-ignore - delete onChunksLoaded.j; - onChunksLoaded.j = v; - originalOnChunksLoaded.j = v; + function setValue(target: any) { + Object.defineProperty(target, "j", { + value: v, + configurable: true, + enumerable: true, + writable: true + }); + } + + setValue(onChunksLoaded); + setValue(originalOnChunksLoaded); } }); } @@ -318,7 +325,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // @ts-ignore originalFactory.$$vencordRequired = true; for (const proxiedModules of allProxiedModules) { - proxiedModules[id] = originalFactory; + Reflect.set(proxiedModules, id, originalFactory); } if (wreq == null && IS_DEV) { From bd95cc449f7e6e39fb37449520418f2c1f1e8238 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 19:54:40 -0300 Subject: [PATCH 021/125] Exit script if a chunk failed to load --- scripts/generateReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 9026ee788f..2035ead096 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -386,7 +386,7 @@ async function runtime(token: string) { await Promise.all( Array.from(validChunkGroups) .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id).catch(() => { }))) + Promise.all(chunkIds.map(id => wreq.e(id))) ) ); From f5be78d1017afb0fbfd160ec3991515b93f6ca91 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 20:02:41 -0300 Subject: [PATCH 022/125] Patching toString -> String --- scripts/generateReport.ts | 12 ++++++------ src/components/VencordSettings/PatchHelperTab.tsx | 2 +- src/plugins/devCompanion.dev/index.tsx | 2 +- src/webpack/patchWebpack.ts | 4 ++-- src/webpack/webpack.ts | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 2035ead096..32e6a3262f 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -441,7 +441,7 @@ async function runtime(token: string) { Vencord.Webpack.factoryListeners.add(factory => { let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); chunksSearchPromises.push(() => isResolved); }); @@ -451,7 +451,7 @@ async function runtime(token: string) { setTimeout(() => { for (const factoryId in wreq.m) { let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); chunksSearchPromises.push(() => isResolved); } @@ -470,7 +470,7 @@ async function runtime(token: string) { const allChunks = [] as string[]; // Matches "id" or id: - for (const currentMatch of wreq.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + for (const currentMatch of String(wreq.u).matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { const id = currentMatch[1] ?? currentMatch[2]; if (id == null) continue; @@ -525,7 +525,7 @@ async function runtime(token: string) { const [code, matcher] = args; const module = Vencord.Webpack.findModuleFactory(...code); - if (module) result = module.toString().match(canonicalizeMatch(matcher)); + if (module) result = String(module).match(canonicalizeMatch(matcher)); } else { // @ts-ignore result = Vencord.Webpack[method](...args); @@ -534,8 +534,8 @@ async function runtime(token: string) { if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; } catch (e) { let logMessage = searchType; - if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; - else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${String(args[0]).slice(0, 147)}...)`; + else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${String(args[1])})`; else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index e09a1dbf37..f9204669b2 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -56,7 +56,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError const [compileResult, setCompileResult] = React.useState<[boolean, string]>(); const [patchedCode, matchResult, diff] = React.useMemo(() => { - const src: string = fact.toString().replaceAll("\n", ""); + const src: string = String(fact).replaceAll("\n", ""); try { new RegExp(match); diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 819360cfec..3842925f2b 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -160,7 +160,7 @@ function initWs(isManual = false) { return reply("Expected exactly one 'find' matches, found " + keys.length); const mod = candidates[keys[0]]; - let src = mod.toString().replaceAll("\n", ""); + let src = String(mod).replaceAll("\n", ""); if (src.startsWith("function(")) { src = "0," + src; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 31aaa911c7..27db709705 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -91,7 +91,7 @@ Object.defineProperty(Function.prototype, "O", { const originalOnChunksLoaded = onChunksLoaded; onChunksLoaded = function (result, chunkIds, callback, priority) { - if (callback != null && initCallbackRegex.test(callback.toString())) { + if (callback != null && initCallbackRegex.test(String(callback))) { Object.defineProperty(this, "O", { value: originalOnChunksLoaded, configurable: true, @@ -218,7 +218,7 @@ function patchFactory(id: PropertyKey, factory: 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," + factory.toString().replaceAll("\n", ""); + let code: string = "0," + String(factory).replaceAll("\n", ""); for (let i = 0; i < patches.length; i++) { const patch = patches[i]; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 20bd93e3bd..adc88ef834 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -218,7 +218,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: string[]) { outer: for (const id in wreq.m) { - const str = wreq.m[id].toString(); + const str = String(wreq.m[id]); for (const c of code) { if (!str.includes(c)) continue outer; @@ -420,7 +420,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def return; } - const match = module.toString().match(canonicalizeMatch(matcher)); + const match = String(module).match(canonicalizeMatch(matcher)); if (!match) { const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code"); logger.warn(err, "Code:", code, "Matcher:", matcher); @@ -501,7 +501,7 @@ export function search(...filters: Array) { outer: for (const id in factories) { const factory = factories[id]; - const str: string = factory.toString(); + const str: string = String(factory); for (const filter of filters) { if (typeof filter === "string" && !str.includes(filter)) continue outer; if (filter instanceof RegExp && !filter.test(str)) continue outer; @@ -529,7 +529,7 @@ export function extract(id: PropertyKey) { // WARNING: This module was extracted to be more easily readable. // This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!! -0,${mod.toString()} +0,${String(mod)} //# sourceURL=ExtractedWebpackModule${String(id)} `; const extracted: ModuleFactory = (0, eval)(code); From 8e9434cdd5bb99e8171f80126960141dd4b8f85e Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 21:57:12 -0300 Subject: [PATCH 023/125] Make reporter faster --- scripts/generateReport.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 32e6a3262f..bb5c39ac75 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -440,10 +440,13 @@ async function runtime(token: string) { wreq = webpackRequire; Vencord.Webpack.factoryListeners.add(factory => { - let isResolved = false; - searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); + // setImmediate to avoid blocking the factory patching execution while checking for lazy chunks + setTimeout(() => { + let isResolved = false; + searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); - chunksSearchPromises.push(() => isResolved); + chunksSearchPromises.push(() => isResolved); + }, 0); }); // setImmediate to only search the initial factories after Discord initialized the app From 9740d28530fda6957683cd115adfb3bc73774738 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 24 May 2024 01:05:17 +0200 Subject: [PATCH 024/125] discord why tf would u roll back to 10 days old build??? --- src/plugins/_core/settings.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/plugins/_core/settings.tsx b/src/plugins/_core/settings.tsx index b743b00666..fd221d27e4 100644 --- a/src/plugins/_core/settings.tsx +++ b/src/plugins/_core/settings.tsx @@ -56,6 +56,26 @@ export default definePlugin({ } ] }, + // Discord Stable + // FIXME: remove once change merged to stable + { + find: "Messages.ACTIVITY_SETTINGS", + replacement: { + get match() { + switch (Settings.plugins.Settings.settingsLocation) { + case "top": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.USER_SETTINGS/; + case "aboveNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.BILLING_SETTINGS/; + case "belowNitro": return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.APP_SETTINGS/; + case "belowActivity": return /(?<=\{section:(\i\.\i)\.DIVIDER},)\{section:"changelog"/; + case "bottom": return /\{section:(\i\.\i)\.CUSTOM,\s*element:.+?}/; + case "aboveActivity": + default: + return /\{section:(\i\.\i)\.HEADER,\s*label:(\i)\.\i\.Messages\.ACTIVITY_SETTINGS/; + } + }, + replace: "...$self.makeSettingsCategories($1),$&" + } + }, { find: "Messages.ACTIVITY_SETTINGS", replacement: { From a120b35f4f6d14313f1b476e8a6e5091676c2df8 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 24 May 2024 01:07:07 +0200 Subject: [PATCH 025/125] Bump to v1.8.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a99b0ad706..04b811e9c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.8.5", + "version": "1.8.6", "description": "The cutest Discord client mod", "homepage": "https://github.com/Vendicated/Vencord#readme", "bugs": { From c5541d297d37fd4a737b4d4e1fa2ecf348e1a9d5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 21:48:12 -0300 Subject: [PATCH 026/125] Optimize slowest patches --- src/plugins/_api/notices.ts | 2 +- src/plugins/alwaysAnimate/index.ts | 4 ++-- src/plugins/betterFolders/index.tsx | 4 ++-- src/plugins/betterSettings/index.tsx | 4 ++-- src/plugins/colorSighted/index.ts | 4 ++-- src/plugins/fakeNitro/index.tsx | 4 ++-- src/plugins/ignoreActivities/index.tsx | 8 ++++---- src/plugins/memberCount/index.tsx | 4 ++-- src/plugins/pinDms/index.tsx | 4 ++-- src/plugins/resurrectHome/index.tsx | 4 ++-- src/plugins/showHiddenChannels/index.tsx | 12 ++++++------ src/plugins/spotifyCrack/index.ts | 4 ++-- src/plugins/superReactionTweaks/index.ts | 4 ++-- src/plugins/viewIcons/index.tsx | 4 ++-- 14 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/plugins/_api/notices.ts b/src/plugins/_api/notices.ts index 90ae6ded9a..0c6f6e1db8 100644 --- a/src/plugins/_api/notices.ts +++ b/src/plugins/_api/notices.ts @@ -29,7 +29,7 @@ export default definePlugin({ find: '"NoticeStore"', replacement: [ { - match: /\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, + match: /(?<=!1;)\i=null;(?=.{0,80}getPremiumSubscription\(\))/g, replace: "if(Vencord.Api.Notices.currentNotice)return false;$&" }, { diff --git a/src/plugins/alwaysAnimate/index.ts b/src/plugins/alwaysAnimate/index.ts index dbec3b4e32..20cb4f974b 100644 --- a/src/plugins/alwaysAnimate/index.ts +++ b/src/plugins/alwaysAnimate/index.ts @@ -31,10 +31,10 @@ export default definePlugin({ // Some modules match the find but the replacement is returned untouched noWarn: true, replacement: { - match: /canAnimate:.+?(?=([,}].*?\)))/g, + match: /canAnimate:.+?([,}].*?\))/g, replace: (m, rest) => { const destructuringMatch = rest.match(/}=.+/); - if (destructuringMatch == null) return "canAnimate:!0"; + if (destructuringMatch == null) return `canAnimate:!0${rest}`; return m; } } diff --git a/src/plugins/betterFolders/index.tsx b/src/plugins/betterFolders/index.tsx index 795f19901b..38e1b84127 100644 --- a/src/plugins/betterFolders/index.tsx +++ b/src/plugins/betterFolders/index.tsx @@ -112,8 +112,8 @@ export default definePlugin({ replacement: [ // Create the isBetterFolders variable in the GuildsBar component { - match: /(?<=let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?)(?=}=\i,)/, - replace: ",isBetterFolders" + match: /let{disableAppDownload:\i=\i\.isPlatformEmbedded,isOverlay:.+?(?=}=\i,)/, + replace: "$&,isBetterFolders" }, // If we are rendering the Better Folders sidebar, we filter out guilds that are not in folders and unexpanded folders { diff --git a/src/plugins/betterSettings/index.tsx b/src/plugins/betterSettings/index.tsx index e90e5c82a2..e0267e4b09 100644 --- a/src/plugins/betterSettings/index.tsx +++ b/src/plugins/betterSettings/index.tsx @@ -111,8 +111,8 @@ export default definePlugin({ { // Load menu TOC eagerly find: "Messages.USER_SETTINGS_WITH_BUILD_OVERRIDE.format", replacement: { - match: /(?<=(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,)(?=\1\(this)/, - replace: "(async ()=>$2)()," + match: /(\i)\(this,"handleOpenSettingsContextMenu",.{0,100}?openContextMenuLazy.{0,100}?(await Promise\.all[^};]*?\)\)).*?,(?=\1\(this)/, + replace: "$&(async ()=>$2)()," }, predicate: () => settings.store.eagerLoad }, diff --git a/src/plugins/colorSighted/index.ts b/src/plugins/colorSighted/index.ts index d741aaae65..025cfb94e6 100644 --- a/src/plugins/colorSighted/index.ts +++ b/src/plugins/colorSighted/index.ts @@ -34,9 +34,9 @@ export default definePlugin({ { find: ".AVATAR_STATUS_MOBILE_16;", replacement: { - match: /(?<=fromIsMobile:\i=!0,.+?)status:(\i)/, + match: /(fromIsMobile:\i=!0,.+?)status:(\i)/, // Rename field to force it to always use "online" - replace: 'status_$:$1="online"' + replace: '$1status_$:$2="online"' } } ] diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 9c8af1e7ce..4ab0e18ee8 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -344,8 +344,8 @@ export default definePlugin({ { // Patch the stickers array to add fake nitro stickers predicate: () => settings.store.transformStickers, - match: /(?<=renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;)/, - replace: (_, message, stickers) => `${stickers}=$self.patchFakeNitroStickers(${stickers},${message});` + match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/, + replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});` }, { // Filter attachments to remove fake nitro stickers or emojis diff --git a/src/plugins/ignoreActivities/index.tsx b/src/plugins/ignoreActivities/index.tsx index e2262129d8..f687a0cafb 100644 --- a/src/plugins/ignoreActivities/index.tsx +++ b/src/plugins/ignoreActivities/index.tsx @@ -228,15 +228,15 @@ export default definePlugin({ { find: ".activityTitleText,variant", replacement: { - match: /(?<=\i\.activityTitleText.+?children:(\i)\.name.*?}\),)/, - replace: (_, props) => `$self.renderToggleActivityButton(${props}),` + match: /\.activityTitleText.+?children:(\i)\.name.*?}\),/, + replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),` }, }, { find: ".activityCardDetails,children", replacement: { - match: /(?<=\i\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),)/, - replace: (_, props) => `$self.renderToggleActivityButton(${props}),` + match: /\.activityCardDetails.+?children:(\i\.application)\.name.*?}\),/, + replace: (m, props) => `${m}$self.renderToggleActivityButton(${props}),` } } ], diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index 92e9a2057e..28ecb9db73 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -70,8 +70,8 @@ export default definePlugin({ { find: ".invitesDisabledTooltip", replacement: { - match: /(?<=\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100})]/, - replace: ",$self.renderTooltip(arguments[0].guild)]" + match: /\.VIEW_AS_ROLES_MENTIONS_WARNING.{0,100}(?=])/, + replace: "$&,$self.renderTooltip(arguments[0].guild)" }, predicate: () => settings.store.toolTip } diff --git a/src/plugins/pinDms/index.tsx b/src/plugins/pinDms/index.tsx index 60484561a4..033552593d 100644 --- a/src/plugins/pinDms/index.tsx +++ b/src/plugins/pinDms/index.tsx @@ -111,8 +111,8 @@ export default definePlugin({ replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)" }, { - match: /(?<=scrollToChannel\(\i\){.{1,300})this\.props\.privateChannelIds/, - replace: "[...$&,...$self.getAllUncollapsedChannels()]" + match: /(scrollToChannel\(\i\){.{1,300})(this\.props\.privateChannelIds)/, + replace: "$1[...$2,...$self.getAllUncollapsedChannels()]" }, ] diff --git a/src/plugins/resurrectHome/index.tsx b/src/plugins/resurrectHome/index.tsx index 70827e08f5..5193090ea6 100644 --- a/src/plugins/resurrectHome/index.tsx +++ b/src/plugins/resurrectHome/index.tsx @@ -134,8 +134,8 @@ export default definePlugin({ { find: '"MessageActionCreators"', replacement: { - match: /(?<=focusMessage\(\i\){.+?)(?=focus:{messageId:(\i)})/, - replace: "after:$1," + match: /focusMessage\(\i\){.+?(?=focus:{messageId:(\i)})/, + replace: "$&after:$1," } }, // Force Server Home instead of Server Guide diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index f08bc2d1d8..c120d72d8d 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -89,8 +89,8 @@ export default definePlugin({ }, // Remove permission checking for getRenderLevel function { - match: /(?<=getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/, - replace: " " + match: /(getRenderLevel\(\i\){.+?return)!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL,this\.record\)\|\|/, + replace: (_, rest) => `${rest} ` } ] }, @@ -159,8 +159,8 @@ export default definePlugin({ replacement: [ // Make the channel appear as muted if it's hidden { - match: /(?<={channel:(\i),name:\i,muted:(\i).+?;)/, - replace: (_, channel, muted) => `${muted}=$self.isHiddenChannel(${channel})?true:${muted};` + match: /{channel:(\i),name:\i,muted:(\i).+?;/, + replace: (m, channel, muted) => `${m}${muted}=$self.isHiddenChannel(${channel})?true:${muted};` }, // Add the hidden eye icon if the channel is hidden { @@ -186,8 +186,8 @@ export default definePlugin({ { // Hide unreads predicate: () => settings.store.hideUnreads === true, - match: /(?<={channel:(\i),name:\i,.+?unread:(\i).+?;)/, - replace: (_, channel, unread) => `${unread}=$self.isHiddenChannel(${channel})?false:${unread};` + match: /{channel:(\i),name:\i,.+?unread:(\i).+?;/, + replace: (m, channel, unread) => `${m}${unread}=$self.isHiddenChannel(${channel})?false:${unread};` } ] }, diff --git a/src/plugins/spotifyCrack/index.ts b/src/plugins/spotifyCrack/index.ts index 1beccad608..37504be2e3 100644 --- a/src/plugins/spotifyCrack/index.ts +++ b/src/plugins/spotifyCrack/index.ts @@ -60,8 +60,8 @@ export default definePlugin({ }, { predicate: () => settings.store.keepSpotifyActivityOnIdle, - match: /(?<=shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, - replace: "" + match: /(shouldShowActivity\(\){.{0,50})&&!\i\.\i\.isIdle\(\)/, + replace: "$1" } ] } diff --git a/src/plugins/superReactionTweaks/index.ts b/src/plugins/superReactionTweaks/index.ts index 89197b4c3a..7878ba630c 100644 --- a/src/plugins/superReactionTweaks/index.ts +++ b/src/plugins/superReactionTweaks/index.ts @@ -42,8 +42,8 @@ export default definePlugin({ { find: ",BURST_REACTION_EFFECT_PLAY", replacement: { - match: /(?<=BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/, - replace: "!$self.shouldPlayBurstReaction($1)" + match: /(BURST_REACTION_EFFECT_PLAY:\i=>{.{50,100})(\i\(\i,\i\))>=\d+/, + replace: "$1!$self.shouldPlayBurstReaction($2)" } }, { diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx index 359365ee4d..09254d511d 100644 --- a/src/plugins/viewIcons/index.tsx +++ b/src/plugins/viewIcons/index.tsx @@ -206,8 +206,8 @@ export default definePlugin({ { find: ".avatarPositionPanel", replacement: { - match: /(?<=avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/, - replace: "style:($1)?{cursor:\"pointer\"}:{},onClick:$1?()=>{$self.openImage($2)}" + match: /(avatarWrapperNonUserBot.{0,50})onClick:(\i\|\|\i)\?void 0(?<=,avatarSrc:(\i).+?)/, + replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}" } }, // Group DMs top small & large icon From 5e762ddd044afb5d770f47b21044926e557e827f Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 23:18:43 -0300 Subject: [PATCH 027/125] Add names to Modules objects --- src/webpack/patchWebpack.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 27db709705..51d0b9a938 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -167,6 +167,8 @@ Object.defineProperty(Function.prototype, "m", { // The new object which will contain the factories const proxiedModules: WebpackRequire["m"] = {}; + // @ts-ignore + proxiedModules[Symbol.toStringTag] = "ProxiedModules"; for (const id in originalModules) { // If we have eagerPatches enabled we have to patch the pre-populated factories @@ -180,9 +182,10 @@ Object.defineProperty(Function.prototype, "m", { delete originalModules[id]; } - // @ts-ignore - originalModules.$$proxiedModules = proxiedModules; allProxiedModules.add(proxiedModules); + + // @ts-ignore + originalModules[Symbol.toStringTag] = "OriginalModules"; Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyHandler)); } From d8c65599672a0fb4f67b63b3e7f4226a99a8ed86 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 23 May 2024 23:56:29 -0300 Subject: [PATCH 028/125] I forgot this --- src/webpack/patchWebpack.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 51d0b9a938..4aa468fe8a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -24,7 +24,7 @@ const modulesProxyHandler: ProxyHandler = { [propName, (...args: any[]) => Reflect[propName](...args)] )), get: (target, p) => { - const propValue = Reflect.get(target, p); + const propValue = Reflect.get(target, p, target); // 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 @@ -36,7 +36,7 @@ const modulesProxyHandler: ProxyHandler = { // This patches factories if eagerPatches are disabled const patchedFactory = patchFactory(p, propValue); - Reflect.set(target, p, patchedFactory); + Reflect.set(target, p, patchedFactory, target); return patchedFactory; }, @@ -44,10 +44,10 @@ const modulesProxyHandler: ProxyHandler = { // $$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); + return Reflect.set(target, p, newValue, target); } - const existingFactory = Reflect.get(target, p); + const existingFactory = Reflect.get(target, p, target); // Check if this factory is already patched // @ts-ignore @@ -59,7 +59,7 @@ const modulesProxyHandler: ProxyHandler = { // 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); + Reflect.set(proxiedModules, p, patchedFactory, proxiedModules); } return true; @@ -328,7 +328,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // @ts-ignore originalFactory.$$vencordRequired = true; for (const proxiedModules of allProxiedModules) { - Reflect.set(proxiedModules, id, originalFactory); + Reflect.set(proxiedModules, id, originalFactory, proxiedModules); } if (wreq == null && IS_DEV) { From b38ab066d9a8109b3fa5631b1a6c05ac4a728e5e Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 02:02:28 -0300 Subject: [PATCH 029/125] Preserve original modules prototype --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 4aa468fe8a..d27d450b31 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -166,7 +166,7 @@ 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 proxiedModules: WebpackRequire["m"] = {}; + const proxiedModules: WebpackRequire["m"] = Object.create(Object.getPrototypeOf(originalModules)); // @ts-ignore proxiedModules[Symbol.toStringTag] = "ProxiedModules"; From 9bbec66ea5e76dc46fc63d2346262ddf176edb35 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:08:35 -0300 Subject: [PATCH 030/125] Take a different approach which requires less cursed code --- src/webpack/patchWebpack.ts | 104 +++++++++++++++++------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d27d450b31..be9dfc9740 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -6,7 +6,6 @@ import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; -import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc"; import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; @@ -17,34 +16,36 @@ import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, mod const logger = new Logger("WebpackInterceptor", "#8caaee"); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); -const allProxiedModules = new Set(); +/** A set with all the module factories objects */ +const allModuleFactories = new Set(); -const modulesProxyHandler: ProxyHandler = { - ...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName => - [propName, (...args: any[]) => Reflect[propName](...args)] - )), - get: (target, p) => { - const propValue = Reflect.get(target, p, target); +function defineModuleFactoryGetter(modules: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { + Object.defineProperty(modules, id, { + get: () => { + // $$vencordOriginal means the factory is already patched + // @ts-ignore + if (factory.$$vencordOriginal != null) { + return factory; + } + // This patches factories if eagerPatches are disabled + return (factory = patchFactory(id, factory)); + }, + configurable: true, + enumerable: true + }); +} + +const moduleFactoriesHandler: ProxyHandler = { + set: (target, p, newValue, receiver) => { // 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 (propValue == null || Number.isNaN(Number(p)) || propValue.$$vencordOriginal != null || propValue.$$vencordRequired === true) { - return propValue; + if (Number.isNaN(Number(p))) { + return Reflect.set(target, p, newValue, receiver); } - // This patches factories if eagerPatches are disabled - const patchedFactory = patchFactory(p, propValue); - Reflect.set(target, p, patchedFactory, target); - - 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, target); + if (!Settings.eagerPatches) { + defineModuleFactoryGetter(target, p, newValue); + return true; } const existingFactory = Reflect.get(target, p, target); @@ -58,18 +59,16 @@ const modulesProxyHandler: ProxyHandler = { 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, proxiedModules); + for (const moduleFactories of allModuleFactories) { + Object.defineProperty(moduleFactories, p, { + value: patchedFactory, + configurable: true, + enumerable: true, + writable: true + }); } return true; - }, - ownKeys: target => { - const keys = Reflect.ownKeys(target); - for (const key of UNCONFIGURABLE_PROPERTIES) { - if (!keys.includes(key)) keys.push(key); - } - return keys; } }; @@ -154,43 +153,35 @@ Object.defineProperty(Function.prototype, "O", { // wreq.m is the webpack object containing module factories. // 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 +// We replace wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them Object.defineProperty(Function.prototype, "m", { configurable: true, - set(this: WebpackRequire, originalModules: WebpackRequire["m"]) { + set(this: WebpackRequire, moduleFactories: WebpackRequire["m"]) { // When using react devtools or other extensions, we may also catch their webpack here. // This ensures we actually got the right one const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(originalModules)) { + if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); - // The new object which will contain the factories - const proxiedModules: WebpackRequire["m"] = Object.create(Object.getPrototypeOf(originalModules)); - // @ts-ignore - proxiedModules[Symbol.toStringTag] = "ProxiedModules"; - - for (const id in originalModules) { + for (const id in moduleFactories) { // If we have eagerPatches enabled we have to patch the pre-populated factories if (Settings.eagerPatches) { - proxiedModules[id] = patchFactory(id, originalModules[id]); + moduleFactories[id] = patchFactory(id, moduleFactories[id]); } else { - proxiedModules[id] = originalModules[id]; + defineModuleFactoryGetter(moduleFactories, id, moduleFactories[id]); } - - // Clear the original object so pre-populated factories are patched if eagerPatches are disabled - delete originalModules[id]; } - allProxiedModules.add(proxiedModules); + allModuleFactories.add(moduleFactories); // @ts-ignore - originalModules[Symbol.toStringTag] = "OriginalModules"; - Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyHandler)); + moduleFactories[Symbol.toStringTag] = "ModuleFactories"; + moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); } Object.defineProperty(this, "m", { - value: originalModules, + value: moduleFactories, configurable: true, enumerable: true, writable: true @@ -325,10 +316,13 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { } const patchedFactory: ModuleFactory = (module, exports, require) => { - // @ts-ignore - originalFactory.$$vencordRequired = true; - for (const proxiedModules of allProxiedModules) { - Reflect.set(proxiedModules, id, originalFactory, proxiedModules); + for (const moduleFactories of allModuleFactories) { + Object.defineProperty(moduleFactories, id, { + value: originalFactory, + configurable: true, + enumerable: true, + writable: true + }); } if (wreq == null && IS_DEV) { From f727d101fccc7032b41b4652367fe5a9fdc6165c Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:09:25 -0300 Subject: [PATCH 031/125] I can never not forget something --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index be9dfc9740..b8f1f32a51 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -19,8 +19,8 @@ const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); /** A set with all the module factories objects */ const allModuleFactories = new Set(); -function defineModuleFactoryGetter(modules: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { - Object.defineProperty(modules, id, { +function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { + Object.defineProperty(modulesFactories, id, { get: () => { // $$vencordOriginal means the factory is already patched // @ts-ignore From f64b228cb9506f47426ba9b9f9004bf5aa714feb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:13:41 -0300 Subject: [PATCH 032/125] better log --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index b8f1f32a51..af27e2294a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -162,7 +162,7 @@ Object.defineProperty(Function.prototype, "m", { // This ensures we actually got the right one const { stack } = new Error(); if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); + logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); for (const id in moduleFactories) { // If we have eagerPatches enabled we have to patch the pre-populated factories From 488f133a90500947f26b6311b0c4e2fbc8e2d398 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:44:40 -0300 Subject: [PATCH 033/125] Handle if for some reason modules are set again --- src/webpack/patchWebpack.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index af27e2294a..e0f8ce12c3 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -21,7 +21,7 @@ const allModuleFactories = new Set(); function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { Object.defineProperty(modulesFactories, id, { - get: () => { + get() { // $$vencordOriginal means the factory is already patched // @ts-ignore if (factory.$$vencordOriginal != null) { @@ -31,6 +31,15 @@ function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: Pr // This patches factories if eagerPatches are disabled return (factory = patchFactory(id, factory)); }, + set(v: ModuleFactory) { + // @ts-ignore + if (factory.$$vencordOriginal != null) { + // @ts-ignore + factory.$$vencordOriginal = v; + } else { + factory = v; + } + }, configurable: true, enumerable: true }); @@ -43,16 +52,24 @@ const moduleFactoriesHandler: ProxyHandler = { return Reflect.set(target, p, newValue, receiver); } + const existingFactory = Reflect.get(target, p, target); + if (!Settings.eagerPatches) { + // If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required + // so call Reflect.set with the new original and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) + if (existingFactory != null) { + return Reflect.set(target, p, newValue, receiver); + } + defineModuleFactoryGetter(target, p, newValue); return true; } - const existingFactory = Reflect.get(target, p, target); - // Check if this factory is already patched // @ts-ignore - if (existingFactory?.$$vencordOriginal === newValue) { + if (existingFactory.$$vencordOriginal != null) { + // @ts-ignore + existingFactory.$$vencordOriginal = newValue; return true; } @@ -318,7 +335,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const patchedFactory: ModuleFactory = (module, exports, require) => { for (const moduleFactories of allModuleFactories) { Object.defineProperty(moduleFactories, id, { - value: originalFactory, + // @ts-ignore + value: patchFactory.$$vencordOriginal, configurable: true, enumerable: true, writable: true From 86f69e84c17848ff16b071bf2e3b1125df066f62 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:45:46 -0300 Subject: [PATCH 034/125] I'm stupid --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index e0f8ce12c3..9b1fcdb831 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -67,7 +67,7 @@ const moduleFactoriesHandler: ProxyHandler = { // Check if this factory is already patched // @ts-ignore - if (existingFactory.$$vencordOriginal != null) { + if (existingFactory?.$$vencordOriginal != null) { // @ts-ignore existingFactory.$$vencordOriginal = newValue; return true; From d6b5bc58ce0f6123fd401e6b6fbe63425fa0a552 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:49:14 -0300 Subject: [PATCH 035/125] how many times --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 9b1fcdb831..8768a4718f 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -336,7 +336,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { for (const moduleFactories of allModuleFactories) { Object.defineProperty(moduleFactories, id, { // @ts-ignore - value: patchFactory.$$vencordOriginal, + value: patchedFactory.$$vencordOriginal, configurable: true, enumerable: true, writable: true From bacf021a281cb6d65af9b8dc9a5d9025d64b2e44 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 03:57:54 -0300 Subject: [PATCH 036/125] aaaaa --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 8768a4718f..2f1deafb04 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -170,7 +170,7 @@ Object.defineProperty(Function.prototype, "O", { // wreq.m is the webpack object containing module factories. // 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 wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them +// We wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them Object.defineProperty(Function.prototype, "m", { configurable: true, From 3296ee1c4ba22dbe8aaa8a599951469043a92986 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 05:14:27 -0300 Subject: [PATCH 037/125] Remove onChunksLoaded patch --- scripts/generateReport.ts | 81 +++++++++++---------- src/webpack/patchWebpack.ts | 140 +++++++++++------------------------- src/webpack/webpack.ts | 1 - 3 files changed, 86 insertions(+), 136 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index bb5c39ac75..89632addf1 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -327,8 +327,51 @@ async function runtime(token: string) { // Enable eagerPatches to make all patches apply regardless of the module being required Vencord.Settings.eagerPatches = true; - let wreq: typeof Vencord.Webpack.wreq; + // The main patch for starting the reporter chunk loading + Vencord.Plugins.patches.push({ + plugin: "Vencord Reporter", + find: '"Could not find app-mount"', + replacement: [{ + match: /(?<="use strict";)/, + replace: "Vencord.Webpack._initReporter();" + }] + }); + + Vencord.Webpack.waitFor( + "loginToken", + m => { + console.log("[PUP_DEBUG]", "Logging in with token..."); + m.loginToken(token); + } + ); + + // @ts-ignore + Vencord.Webpack._initReporter = function () { + // initReporter is called in the patched entry point of Discord + // setImmediate to only start searching for lazy chunks after Discord initialized the app + setTimeout(() => { + console.log("[PUP_DEBUG]", "Loading all chunks..."); + + Vencord.Webpack.factoryListeners.add(factory => { + // setImmediate to avoid blocking the factory patching execution while checking for lazy chunks + setTimeout(() => { + let isResolved = false; + searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); + chunksSearchPromises.push(() => isResolved); + }, 0); + }); + + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + }, 0); + }; + + const wreq = Vencord.Util.proxyLazy(() => Vencord.Webpack.wreq); const { canonicalizeMatch, Logger } = Vencord.Util; const validChunks = new Set(); @@ -426,43 +469,7 @@ async function runtime(token: string) { }, 0); } - Vencord.Webpack.waitFor( - "loginToken", - m => { - console.log("[PUP_DEBUG]", "Logging in with token..."); - m.loginToken(token); - } - ); - - Vencord.Webpack.beforeInitListeners.add(async webpackRequire => { - console.log("[PUP_DEBUG]", "Loading all chunks..."); - - wreq = webpackRequire; - - Vencord.Webpack.factoryListeners.add(factory => { - // setImmediate to avoid blocking the factory patching execution while checking for lazy chunks - setTimeout(() => { - let isResolved = false; - searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - }, 0); - }); - - // setImmediate to only search the initial factories after Discord initialized the app - // our beforeInitListeners are called before Discord initializes the app - setTimeout(() => { - for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - } - }, 0); - }); - await chunksSearchingDone; - wreq = wreq!; // Require deferred entry points for (const deferredRequire of deferredRequires) { diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 2f1deafb04..2e53a544dc 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -6,24 +6,33 @@ import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; -import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; +import { canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; -import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, OnChunksLoaded, subscriptions, WebpackRequire, wreq } from "."; +import { _initWebpack, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; + +type PatchedModuleFactory = ModuleFactory & { + $$vencordOriginal?: ModuleFactory; +}; + +type PatchedModuleFactories = Record & { + [Symbol.toStringTag]?: "ModuleFactories"; +}; const logger = new Logger("WebpackInterceptor", "#8caaee"); -const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); /** A set with all the module factories objects */ -const allModuleFactories = new Set(); +const allModuleFactories = new Set(); -function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { +function defineModuleFactoryGetter(modulesFactories: PatchedModuleFactories, id: PropertyKey, factory: PatchedModuleFactory) { Object.defineProperty(modulesFactories, id, { + configurable: true, + enumerable: true, + get() { // $$vencordOriginal means the factory is already patched - // @ts-ignore if (factory.$$vencordOriginal != null) { return factory; } @@ -32,20 +41,16 @@ function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: Pr return (factory = patchFactory(id, factory)); }, set(v: ModuleFactory) { - // @ts-ignore if (factory.$$vencordOriginal != null) { - // @ts-ignore factory.$$vencordOriginal = v; } else { factory = v; } - }, - configurable: true, - enumerable: true + } }); } -const moduleFactoriesHandler: ProxyHandler = { +const moduleFactoriesHandler: ProxyHandler = { set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory if (Number.isNaN(Number(p))) { @@ -66,9 +71,7 @@ const moduleFactoriesHandler: ProxyHandler = { } // Check if this factory is already patched - // @ts-ignore if (existingFactory?.$$vencordOriginal != null) { - // @ts-ignore existingFactory.$$vencordOriginal = newValue; return true; } @@ -89,97 +92,41 @@ const moduleFactoriesHandler: ProxyHandler = { } }; -// wreq.O is the webpack onChunksLoaded function -// Discord uses it to await for all the chunks to be loaded before initializing the app -// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it -Object.defineProperty(Function.prototype, "O", { +// wreq.m is the webpack object containing module factories. +// 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 wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them +Object.defineProperty(Function.prototype, "m", { configurable: true, - set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) { - // When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here. - // This ensures we actually got the right one - // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it + set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { + // When using React DevTools or other extensions, we may also catch their Webpack here. + // This ensures we actually got the right ones const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && String(this.e).includes("Promise.all")) { - logger.info("Found main WebpackRequire.onChunksLoaded"); + if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { + logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); - delete (Function.prototype as any).O; + // setImmediate to clear this property setter if this is not the main Webpack + // If this is the main Webpack, wreq.m will always be set before the timeout runs + const setterTimeout = setTimeout(() => delete (this as Partial).p, 0); + Object.defineProperty(this, "p", { + configurable: true, - const originalOnChunksLoaded = onChunksLoaded; - onChunksLoaded = function (result, chunkIds, callback, priority) { - if (callback != null && initCallbackRegex.test(String(callback))) { - Object.defineProperty(this, "O", { - value: originalOnChunksLoaded, + set(this: WebpackRequire, v: WebpackRequire["p"]) { + if (v !== "/assets/") return; + + logger.info("Main Webpack found, initializing internal references to WebpackRequire "); + _initWebpack(this); + clearTimeout(setterTimeout); + + Object.defineProperty(this, "p", { + value: v, configurable: true, enumerable: true, writable: true }); - - const wreq = this; - - const originalCallback = callback; - callback = function (this: unknown) { - logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners"); - _initWebpack(wreq); - - for (const beforeInitListener of beforeInitListeners) { - beforeInitListener(wreq); - } - - originalCallback.apply(this, arguments as any); - }; - - callback.toString = originalCallback.toString.bind(originalCallback); - arguments[2] = callback; - } - - originalOnChunksLoaded.apply(this, arguments as any); - } as WebpackRequire["O"]; - - onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded); - - // Returns whether a chunk has been loaded - Object.defineProperty(onChunksLoaded, "j", { - configurable: true, - - set(v: OnChunksLoaded["j"]) { - function setValue(target: any) { - Object.defineProperty(target, "j", { - value: v, - configurable: true, - enumerable: true, - writable: true - }); - } - - setValue(onChunksLoaded); - setValue(originalOnChunksLoaded); } }); - } - - Object.defineProperty(this, "O", { - value: onChunksLoaded, - configurable: true, - enumerable: true, - writable: true - }); - } -}); - -// wreq.m is the webpack object containing module factories. -// 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 wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them -Object.defineProperty(Function.prototype, "m", { - configurable: true, - - set(this: WebpackRequire, moduleFactories: WebpackRequire["m"]) { - // When using react devtools or other extensions, we may also catch their webpack here. - // This ensures we actually got the right one - const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); for (const id in moduleFactories) { // If we have eagerPatches enabled we have to patch the pre-populated factories @@ -192,7 +139,6 @@ Object.defineProperty(Function.prototype, "m", { allModuleFactories.add(moduleFactories); - // @ts-ignore moduleFactories[Symbol.toStringTag] = "ModuleFactories"; moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); } @@ -332,10 +278,9 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (!patch.all) patches.splice(i--, 1); } - const patchedFactory: ModuleFactory = (module, exports, require) => { + const patchedFactory: PatchedModuleFactory = (module, exports, require) => { for (const moduleFactories of allModuleFactories) { Object.defineProperty(moduleFactories, id, { - // @ts-ignore value: patchedFactory.$$vencordOriginal, configurable: true, enumerable: true, @@ -402,7 +347,6 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { }; patchedFactory.toString = originalFactory.toString.bind(originalFactory); - // @ts-ignore patchedFactory.$$vencordOriginal = originalFactory; return patchedFactory; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index adc88ef834..3003c6bd83 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -73,7 +73,6 @@ export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; export const subscriptions = new Map(); export const moduleListeners = new Set(); export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); -export const beforeInitListeners = new Set<(wreq: WebpackRequire) => void>(); export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; From 3793acd9bb7f857b17cca63eb5aedbefa889dd65 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 05:19:12 -0300 Subject: [PATCH 038/125] forgot the comment --- src/webpack/patchWebpack.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 2e53a544dc..bd3c45b76b 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -92,10 +92,11 @@ const moduleFactoriesHandler: ProxyHandler = { } }; -// wreq.m is the webpack object containing module factories. +// wreq.m is the Webpack object containing module factories. // 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 +// The sentry module also has their own Webpack with a pre-populated module factories object, so this also targets that // We wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them +// If this is the main Webpack, we also set up the internal references to WebpackRequire Object.defineProperty(Function.prototype, "m", { configurable: true, From 5fbabb0b70df8d61e01b0d963707c56f02ae0da5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 05:24:37 -0300 Subject: [PATCH 039/125] sob --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index bd3c45b76b..4f05578179 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -116,7 +116,7 @@ Object.defineProperty(Function.prototype, "m", { set(this: WebpackRequire, v: WebpackRequire["p"]) { if (v !== "/assets/") return; - logger.info("Main Webpack found, initializing internal references to WebpackRequire "); + logger.info("Main Webpack found, initializing internal references to WebpackRequire"); _initWebpack(this); clearTimeout(setterTimeout); From 8dde496757b6d5072d38d5689516cbfa88179b7c Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 07:40:36 -0300 Subject: [PATCH 040/125] one more name cuz why not --- src/webpack/patchWebpack.ts | 2 +- src/webpack/webpack.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 4f05578179..a37169258a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -18,7 +18,7 @@ type PatchedModuleFactory = ModuleFactory & { }; type PatchedModuleFactories = Record & { - [Symbol.toStringTag]?: "ModuleFactories"; + [Symbol.toStringTag]?: string; }; const logger = new Logger("WebpackInterceptor", "#8caaee"); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 3003c6bd83..ab9ded1e00 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -77,6 +77,9 @@ export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; cache = webpackRequire.c; + + // @ts-ignore + webpackRequire.c[Symbol.toStringTag] = "ModuleCache"; } let devToolsOpen = false; From 2f16cdf77b5ac8ee1ccd1886f48bd33268d734c5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 07:59:56 -0300 Subject: [PATCH 041/125] Make toStringTag non enumerable --- src/webpack/patchWebpack.ts | 10 ++++++---- src/webpack/webpack.ts | 7 +++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a37169258a..7aaa4e329a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -17,9 +17,7 @@ type PatchedModuleFactory = ModuleFactory & { $$vencordOriginal?: ModuleFactory; }; -type PatchedModuleFactories = Record & { - [Symbol.toStringTag]?: string; -}; +type PatchedModuleFactories = Record; const logger = new Logger("WebpackInterceptor", "#8caaee"); @@ -140,7 +138,11 @@ Object.defineProperty(Function.prototype, "m", { allModuleFactories.add(moduleFactories); - moduleFactories[Symbol.toStringTag] = "ModuleFactories"; + Object.defineProperty(moduleFactories, Symbol.toStringTag, { + value: "ModuleFactories", + configurable: true, + writable: true + }); moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index ab9ded1e00..480ccb078f 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -78,8 +78,11 @@ export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; cache = webpackRequire.c; - // @ts-ignore - webpackRequire.c[Symbol.toStringTag] = "ModuleCache"; + Object.defineProperty(webpackRequire.c, Symbol.toStringTag, { + value: "ModuleCache", + configurable: true, + writable: true + }); } let devToolsOpen = false; From daa1a35fc3a658c2e5bc49174e6ea73f6361c6ed Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 08:23:41 -0300 Subject: [PATCH 042/125] Factories are called with a `this` --- src/webpack/patchWebpack.ts | 8 ++++---- src/webpack/wreq.d.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 7aaa4e329a..075a5a6248 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -281,7 +281,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (!patch.all) patches.splice(i--, 1); } - const patchedFactory: PatchedModuleFactory = (module, exports, require) => { + const patchedFactory: PatchedModuleFactory = function (module, exports, require) { for (const moduleFactories of allModuleFactories) { Object.defineProperty(moduleFactories, id, { value: patchedFactory.$$vencordOriginal, @@ -297,17 +297,17 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { logger.error("WebpackRequire was not initialized, running modules without patches instead."); } - return void originalFactory(module, exports, require); + return void originalFactory.call(this, module, exports, require); } try { - factory(module, exports, require); + factory.call(this, module, exports, require); } catch (err) { // Just rethrow Discord errors if (factory === originalFactory) throw err; logger.error("Error in patched module", err); - return void originalFactory(module, exports, require); + return void originalFactory.call(this, module, exports, require); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index c86fa1c49d..c810c5a568 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -13,7 +13,7 @@ export type Module = { }; /** exports can be anything, however initially it is always an empty object */ -export type ModuleFactory = (module: Module, exports: ModuleExports, require: WebpackRequire) => void; +export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void; export type AsyncModuleBody = ( handleDependencies: (deps: Promise[]) => Promise & (() => void) From 1e55ae5327d3a29337fc0589fbb831a860bc3fec Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 24 May 2024 18:46:36 -0300 Subject: [PATCH 043/125] this is more clean --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 075a5a6248..723271f980 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -107,7 +107,7 @@ Object.defineProperty(Function.prototype, "m", { // setImmediate to clear this property setter if this is not the main Webpack // If this is the main Webpack, wreq.m will always be set before the timeout runs - const setterTimeout = setTimeout(() => delete (this as Partial).p, 0); + const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); Object.defineProperty(this, "p", { configurable: true, From 9af63b362d7ecbc9bef2b9bca5fe897e7980a7ac Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 00:27:13 -0300 Subject: [PATCH 044/125] more future proof --- src/webpack/patchWebpack.ts | 109 +++++++++++++++++++----------------- src/webpack/webpack.ts | 7 ++- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 723271f980..a35da91776 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -24,28 +24,30 @@ const logger = new Logger("WebpackInterceptor", "#8caaee"); /** A set with all the module factories objects */ const allModuleFactories = new Set(); -function defineModuleFactoryGetter(modulesFactories: PatchedModuleFactories, id: PropertyKey, factory: PatchedModuleFactory) { - Object.defineProperty(modulesFactories, id, { - configurable: true, - enumerable: true, - - get() { - // $$vencordOriginal means the factory is already patched - if (factory.$$vencordOriginal != null) { - return factory; - } +function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { + for (const moduleFactories of allModuleFactories) { + Reflect.defineProperty(moduleFactories, id, { + configurable: true, + enumerable: true, + + get() { + // $$vencordOriginal means the factory is already patched + if (factory.$$vencordOriginal != null) { + return factory; + } - // This patches factories if eagerPatches are disabled - return (factory = patchFactory(id, factory)); - }, - set(v: ModuleFactory) { - if (factory.$$vencordOriginal != null) { - factory.$$vencordOriginal = v; - } else { - factory = v; + // This patches factories if eagerPatches are disabled + return (factory = patchFactory(id, factory)); + }, + set(v: ModuleFactory) { + if (factory.$$vencordOriginal != null) { + factory.$$vencordOriginal = v; + } else { + factory = v; + } } - } - }); + }); + } } const moduleFactoriesHandler: ProxyHandler = { @@ -64,7 +66,7 @@ const moduleFactoriesHandler: ProxyHandler = { return Reflect.set(target, p, newValue, receiver); } - defineModuleFactoryGetter(target, p, newValue); + defineModulesFactoryGetter(p, newValue); return true; } @@ -78,7 +80,7 @@ const moduleFactoriesHandler: ProxyHandler = { // Modules are only patched once, so we need to set the patched factory on all the modules for (const moduleFactories of allModuleFactories) { - Object.defineProperty(moduleFactories, p, { + Reflect.defineProperty(moduleFactories, p, { value: patchedFactory, configurable: true, enumerable: true, @@ -95,7 +97,7 @@ const moduleFactoriesHandler: ProxyHandler = { // The sentry module also has their own Webpack with a pre-populated module factories object, so this also targets that // We wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them // If this is the main Webpack, we also set up the internal references to WebpackRequire -Object.defineProperty(Function.prototype, "m", { +Reflect.defineProperty(Function.prototype, "m", { configurable: true, set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { @@ -103,23 +105,24 @@ Object.defineProperty(Function.prototype, "m", { // This ensures we actually got the right ones const { stack } = new Error(); if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); + const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; + logger.info("Found Webpack module factories in", fileName); // setImmediate to clear this property setter if this is not the main Webpack // If this is the main Webpack, wreq.m will always be set before the timeout runs const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); - Object.defineProperty(this, "p", { + Reflect.defineProperty(this, "p", { configurable: true, - set(this: WebpackRequire, v: WebpackRequire["p"]) { - if (v !== "/assets/") return; + set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + if (bundlePath !== "/assets/") return; - logger.info("Main Webpack found, initializing internal references to WebpackRequire"); + logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); _initWebpack(this); clearTimeout(setterTimeout); - Object.defineProperty(this, "p", { - value: v, + Reflect.defineProperty(this, "p", { + value: bundlePath, configurable: true, enumerable: true, writable: true @@ -127,26 +130,28 @@ Object.defineProperty(Function.prototype, "m", { } }); + // This needs to be added before the loop below + allModuleFactories.add(moduleFactories); + for (const id in moduleFactories) { // If we have eagerPatches enabled we have to patch the pre-populated factories if (Settings.eagerPatches) { moduleFactories[id] = patchFactory(id, moduleFactories[id]); } else { - defineModuleFactoryGetter(moduleFactories, id, moduleFactories[id]); + defineModulesFactoryGetter(id, moduleFactories[id]); } } - allModuleFactories.add(moduleFactories); - - Object.defineProperty(moduleFactories, Symbol.toStringTag, { + Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { value: "ModuleFactories", configurable: true, - writable: true + writable: true, + enumerable: false }); moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); } - Object.defineProperty(this, "m", { + Reflect.defineProperty(this, "m", { value: moduleFactories, configurable: true, enumerable: true, @@ -158,15 +163,16 @@ Object.defineProperty(Function.prototype, "m", { let webpackNotInitializedLogged = false; function patchFactory(id: PropertyKey, factory: ModuleFactory) { + const originalFactory = factory; + for (const factoryListener of factoryListeners) { try { - factoryListener(factory); + factoryListener(originalFactory); } catch (err) { logger.error("Error in Webpack factory listener:\n", err, factoryListener); } } - const originalFactory = factory; const patchedBy = new Set(); // Discords Webpack chunks for some ungodly reason contain random @@ -193,15 +199,15 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { patchedBy.add(patch.plugin); const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); - const previousFactory = factory; const previousCode = code; + const previousFactory = factory; // 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 lastFactory = factory; const lastCode = code; + const lastFactory = factory; canonicalizeReplacement(replacement, patch.plugin); @@ -217,8 +223,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); - factory = previousFactory; code = previousCode; + factory = previousFactory; patchedBy.delete(patch.plugin); break; } @@ -268,13 +274,13 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); - factory = previousFactory; code = previousCode; + factory = previousFactory; break; } - factory = lastFactory; code = lastCode; + factory = lastFactory; } } @@ -283,7 +289,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const patchedFactory: PatchedModuleFactory = function (module, exports, require) { for (const moduleFactories of allModuleFactories) { - Object.defineProperty(moduleFactories, id, { + Reflect.defineProperty(moduleFactories, id, { value: patchedFactory.$$vencordOriginal, configurable: true, enumerable: true, @@ -297,33 +303,34 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { logger.error("WebpackRequire was not initialized, running modules without patches instead."); } - return void originalFactory.call(this, module, exports, require); + return originalFactory.call(this, module, exports, require); } + let factoryReturn: unknown; try { - factory.call(this, module, exports, require); + factoryReturn = factory.call(this, module, exports, require); } catch (err) { // Just rethrow Discord errors if (factory === originalFactory) throw err; logger.error("Error in patched module", err); - return void originalFactory.call(this, module, exports, require); + return originalFactory.call(this, module, exports, require); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it exports = module.exports; - if (exports == null) return; + if (exports == null) return factoryReturn; // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance if (exports === window && require.c) { - Object.defineProperty(require.c, id, { + Reflect.defineProperty(require.c, id, { value: require.c[id], configurable: true, enumerable: false, writable: true }); - return; + return factoryReturn; } for (const callback of moduleListeners) { @@ -347,6 +354,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); } } + + return factoryReturn; }; patchedFactory.toString = originalFactory.toString.bind(originalFactory); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 480ccb078f..a46a7de459 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -78,10 +78,11 @@ export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; cache = webpackRequire.c; - Object.defineProperty(webpackRequire.c, Symbol.toStringTag, { + Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, { value: "ModuleCache", configurable: true, - writable: true + writable: true, + enumerable: false }); } @@ -506,7 +507,7 @@ export function search(...filters: Array) { outer: for (const id in factories) { const factory = factories[id]; - const str: string = String(factory); + const str = String(factory); for (const filter of filters) { if (typeof filter === "string" && !str.includes(filter)) continue outer; if (filter instanceof RegExp && !filter.test(str)) continue outer; From affd527bc13503aa906286c0d23f99bc64fe5550 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 05:19:52 -0300 Subject: [PATCH 045/125] Make factory wrapper a little more future proof --- src/utils/misc.tsx | 5 ++++ src/webpack/patchWebpack.ts | 47 ++++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 5bf2c23984..089bd541c9 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -102,3 +102,8 @@ export function pluralise(amount: number, singular: string, plural = singular + /** Unconfigurable properties for proxies */ export const UNCONFIGURABLE_PROPERTIES = ["arguments", "caller", "prototype"]; + +export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) { + if (args.some(arg => arg == null)) return ""; + return strings.reduce((acc, str, i) => `${acc}${str}${args[i] ?? ""}`, ""); +} diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a35da91776..6aeee236f9 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -6,6 +6,7 @@ import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; +import { interpolateIfDefined } from "@utils/misc"; import { canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; @@ -105,8 +106,8 @@ Reflect.defineProperty(Function.prototype, "m", { // This ensures we actually got the right ones const { stack } = new Error(); if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; - logger.info("Found Webpack module factories in", fileName); + const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); // setImmediate to clear this property setter if this is not the main Webpack // If this is the main Webpack, wreq.m will always be set before the timeout runs @@ -117,7 +118,7 @@ Reflect.defineProperty(Function.prototype, "m", { set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { if (bundlePath !== "/assets/") return; - logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); clearTimeout(setterTimeout); @@ -160,8 +161,6 @@ Reflect.defineProperty(Function.prototype, "m", { } }); -let webpackNotInitializedLogged = false; - function patchFactory(id: PropertyKey, factory: ModuleFactory) { const originalFactory = factory; @@ -287,7 +286,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (!patch.all) patches.splice(i--, 1); } - const patchedFactory: PatchedModuleFactory = function (module, exports, require) { + const patchedFactory: PatchedModuleFactory = function (...args: Parameters) { for (const moduleFactories of allModuleFactories) { Reflect.defineProperty(moduleFactories, id, { value: patchedFactory.$$vencordOriginal, @@ -297,38 +296,44 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { }); } - if (wreq == null && IS_DEV) { - if (!webpackNotInitializedLogged) { - webpackNotInitializedLogged = true; - logger.error("WebpackRequire was not initialized, running modules without patches instead."); - } - - return originalFactory.call(this, module, exports, require); + // eslint-disable-next-line prefer-const + let [module, exports, require] = args; + + // Make sure the require argument is actually the WebpackRequire functioin + if (wreq == null && String(require).includes("exports:{}")) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + + ")" + ); + _initWebpack(require); } let factoryReturn: unknown; try { - factoryReturn = factory.call(this, module, exports, require); + factoryReturn = factory.apply(this, args); } catch (err) { - // Just rethrow Discord errors + // Just re-throw Discord errors if (factory === originalFactory) throw err; - logger.error("Error in patched module", err); - return originalFactory.call(this, module, exports, require); + logger.error("Error in patched module factory", err); + return originalFactory.apply(this, args); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it - exports = module.exports; + exports = module?.exports; if (exports == null) return factoryReturn; // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance - if (exports === window && require.c) { + if (exports === window && require?.c) { Reflect.defineProperty(require.c, id, { value: require.c[id], configurable: true, - enumerable: false, - writable: true + writable: true, + enumerable: false }); return factoryReturn; } From 265cf12d392c8a0bfbf585f77bf76a3acab34c55 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 05:24:03 -0300 Subject: [PATCH 046/125] I love --- src/webpack/patchWebpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 6aeee236f9..e756d3d0b1 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -299,8 +299,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // eslint-disable-next-line prefer-const let [module, exports, require] = args; - // Make sure the require argument is actually the WebpackRequire functioin - if (wreq == null && String(require).includes("exports:{}")) { + // Make sure the require argument is actually the WebpackRequire function + if (wreq == null && typeof require === "function" && require.m != null) { const { stack } = new Error(); const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; logger.warn( @@ -328,7 +328,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance - if (exports === window && require?.c) { + if (exports === window && typeof require === "function" && require.c != null) { Reflect.defineProperty(require.c, id, { value: require.c[id], configurable: true, From a8fa685cfac63ceff4d3905d47d4d6683f9dbac6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 05:28:34 -0300 Subject: [PATCH 047/125] Add back running modules without patches --- src/webpack/patchWebpack.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index e756d3d0b1..480234dbdd 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -161,6 +161,8 @@ Reflect.defineProperty(Function.prototype, "m", { } }); +let wreqNotInitializedLogged = false; + function patchFactory(id: PropertyKey, factory: ModuleFactory) { const originalFactory = factory; @@ -299,16 +301,25 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // eslint-disable-next-line prefer-const let [module, exports, require] = args; - // Make sure the require argument is actually the WebpackRequire function - if (wreq == null && typeof require === "function" && require.m != null) { - const { stack } = new Error(); - const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.warn( - "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + - `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + - ")" - ); - _initWebpack(require); + if (wreq == null) { + // Make sure the require argument is actually the WebpackRequire function + if (typeof require === "function" && require.m != null) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + + ")" + ); + _initWebpack(require); + } else if (IS_DEV) { + if (!wreqNotInitializedLogged) { + wreqNotInitializedLogged = true; + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + } + + return originalFactory.apply(this, args); + } } let factoryReturn: unknown; From 1ed956114f82721e905a33ee3c39ca417ecc0a24 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 05:46:53 -0300 Subject: [PATCH 048/125] fix logic --- src/webpack/patchWebpack.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 480234dbdd..efebaf6bbe 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -161,7 +161,7 @@ Reflect.defineProperty(Function.prototype, "m", { } }); -let wreqNotInitializedLogged = false; +let wreqFallbackApplied = false; function patchFactory(id: PropertyKey, factory: ModuleFactory) { const originalFactory = factory; @@ -302,22 +302,25 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { let [module, exports, require] = args; if (wreq == null) { - // Make sure the require argument is actually the WebpackRequire function - if (typeof require === "function" && require.m != null) { - const { stack } = new Error(); - const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.warn( - "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + - `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + - ")" - ); - _initWebpack(require); - } else if (IS_DEV) { - if (!wreqNotInitializedLogged) { - wreqNotInitializedLogged = true; + if (!wreqFallbackApplied) { + wreqFallbackApplied = true; + + // Make sure the require argument is actually the WebpackRequire function + if (typeof require === "function" && require.m != null) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + + ")" + ); + _initWebpack(require); + } else if (IS_DEV) { logger.error("WebpackRequire was not initialized, running modules without patches instead."); } + } + if (IS_DEV) { return originalFactory.apply(this, args); } } From 3e3201ad0d29b8fc367ce30be5b63f87ac6c8716 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 07:00:03 -0300 Subject: [PATCH 049/125] improve wreq docs --- src/webpack/wreq.d.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index c810c5a568..19800e7014 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -50,8 +50,13 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: * @example * const fromObject = { a: 1 }; - * Object.defineProperty(fromObject, "a", { - * get: () => fromObject["a"] + * Object.keys(fromObject).forEach(key => { + * if (key !== "default" && !(key in toObject)) { + * Object.defineProperty(toObject, key, { + * get: () => fromObject[key], + * enumerable: true + * }); + * } * }); * @returns fromObject */ @@ -81,14 +86,20 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { */ t: (this: WebpackRequire, value: any, mode: number) => any; /** - * Define property getters. For every prop in "definiton", set a getter in "exports" for the value in "definitiion", like this: + * Define getter functions for harmony exports. For every prop in "definiton" (the module exports), set a getter in "exports" for the getter function in the "definition", like this: * @example * const exports = {}; - * const definition = { a: 1 }; + * const definition = { exportName: () => someExportedValue }; * for (const key in definition) { - * Object.defineProperty(exports, key, { get: definition[key] } + * if (key in definition && !(key in exports)) { + * Object.defineProperty(exports, key, { + * get: definition[key], + * enumerable: true + * }); + * } * } - */ + * // exports is now { exportName: someExportedValue } (but each value is actually a getter) + */ d: (this: WebpackRequire, exports: Record, definiton: Record) => void; /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ f: ChunkHandlers; From 440cb1f29cb01bc8a5c36a927053e75eaec1d371 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:24:59 -0300 Subject: [PATCH 050/125] Properly document wreq.a --- src/webpack/wreq.d.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 19800e7014..2e8ece8f19 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -16,7 +16,9 @@ export type Module = { export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void; export type AsyncModuleBody = ( - handleDependencies: (deps: Promise[]) => Promise & (() => void) + handleAsyncDependencies: (deps: Promise[]) => + Promise<() => any[]> | (() => any[]), + asyncResult: (error?: any) => void ) => Promise; export type ChunkHandlers = { @@ -62,11 +64,29 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { */ es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; /** - * Creates an async module. The body function must be a async function. - * "module.exports" will be decorated with an AsyncModulePromise. - * The body function will be called. - * To handle async dependencies correctly do this inside the body: "([a, b, c] = await handleDependencies([a, b, c]));". - * If "hasAwaitAfterDependencies" is truthy, "handleDependencies()" must be called at the end of the body function. + * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. + * The body function must be an async function. "module.exports" will become a Promise. + * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example to handle async depedencies: + * @example + * const factory = (module, exports, wreq) => { + * wreq.a(module, async (handleAsyncDependencies, asyncResult) => { + * try { + * const asyncRequireA = wreq(...); + * + * const asyncDependencies = handleAsyncDependencies([asyncRequire]); + * const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies; + * + * // Use the required module + * console.log(requireAResult); + * + * // Mark this async module as resolved + * asyncResult(); + * } catch(error) { + * // Mark this async module as rejected with an error + * asyncResult(error); + * } + * }, false); // false because our module does not have an await after dealing with the async requires + * } */ a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void; /** getDefaultExport function for compatibility with non-harmony modules */ From 18142ecccb62b1ced32299fbd797a19fae477cd3 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:26:26 -0300 Subject: [PATCH 051/125] fix doc --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 2e8ece8f19..ab1f43c53e 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -65,7 +65,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; /** * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. - * The body function must be an async function. "module.exports" will become a Promise. + * The body function must be an async function. "module.exports" will become a AsyncModulePromise. * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example to handle async depedencies: * @example * const factory = (module, exports, wreq) => { From c2047e5f3b99371a9a85bc430c60f520d61d2be5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:27:52 -0300 Subject: [PATCH 052/125] fix typos --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index ab1f43c53e..4ac85a1cce 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -66,7 +66,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { /** * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. * The body function must be an async function. "module.exports" will become a AsyncModulePromise. - * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example to handle async depedencies: + * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies: * @example * const factory = (module, exports, wreq) => { * wreq.a(module, async (handleAsyncDependencies, asyncResult) => { From 4d27643d39b89b4d981098226815cf8d52fc5ef1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:37:47 -0300 Subject: [PATCH 053/125] type more --- src/webpack/wreq.d.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 4ac85a1cce..38038907a1 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -15,9 +15,19 @@ export type Module = { /** exports can be anything, however initially it is always an empty object */ export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void; +export type WebpackQueues = unique symbol; +export type WebpackExports = unique symbol; +export type WebpackError = unique symbol; + +type AsyncModulePromise = Promise & { + [WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any; + [WebpackExports]: ModuleExports; + [WebpackError]?: any; +}; + export type AsyncModuleBody = ( - handleAsyncDependencies: (deps: Promise[]) => - Promise<() => any[]> | (() => any[]), + handleAsyncDependencies: (deps: AsyncModulePromise[]) => + Promise<() => ModuleExports[]> | (() => ModuleExports[]), asyncResult: (error?: any) => void ) => Promise; From dae3841f106680b77e4e6455b901d30a7b5744b7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:38:25 -0300 Subject: [PATCH 054/125] ughh --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 38038907a1..85f2c9ab77 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -19,7 +19,7 @@ export type WebpackQueues = unique symbol; export type WebpackExports = unique symbol; export type WebpackError = unique symbol; -type AsyncModulePromise = Promise & { +export type AsyncModulePromise = Promise & { [WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any; [WebpackExports]: ModuleExports; [WebpackError]?: any; From 32a2c90761212da2d497fa0d302a01625fdd86f1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:40:33 -0300 Subject: [PATCH 055/125] fix typing of the global --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 85f2c9ab77..e155f20cd8 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -143,7 +143,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { /** Get the filename for the js part of a chunk */ u: (this: WebpackRequire, chunkId: PropertyKey) => string; /** The global object, will likely always be the window */ - g: Window; + g: typeof globalThis; /** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */ hmd: (this: WebpackRequire, module: Module) => any; /** Shorthand for Object.prototype.hasOwnProperty */ From c1e78b439750a94559724a89e25155a4acddfa52 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:50:16 -0300 Subject: [PATCH 056/125] hasOwn != in --- src/webpack/wreq.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index e155f20cd8..a491fc4af6 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -63,7 +63,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * @example * const fromObject = { a: 1 }; * Object.keys(fromObject).forEach(key => { - * if (key !== "default" && !(key in toObject)) { + * if (key !== "default" && !Object.hasOwn(toObject, key) { * Object.defineProperty(toObject, key, { * get: () => fromObject[key], * enumerable: true @@ -121,7 +121,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * const exports = {}; * const definition = { exportName: () => someExportedValue }; * for (const key in definition) { - * if (key in definition && !(key in exports)) { + * if (Objeect.hasOwn(definition, key) && !Object.hasOwn(exports, key) { * Object.defineProperty(exports, key, { * get: definition[key], * enumerable: true From 8fd22f6deb90285f594c23fe73a169e61a587f72 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:50:47 -0300 Subject: [PATCH 057/125] e --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index a491fc4af6..6457fcc815 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -121,7 +121,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * const exports = {}; * const definition = { exportName: () => someExportedValue }; * for (const key in definition) { - * if (Objeect.hasOwn(definition, key) && !Object.hasOwn(exports, key) { + * if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key) { * Object.defineProperty(exports, key, { * get: definition[key], * enumerable: true From 9ada9bc1a950c9f761e2e66dd342f0a69c1d63b6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 19:51:34 -0300 Subject: [PATCH 058/125] e part 2 --- src/webpack/wreq.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 6457fcc815..3dccb629d2 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -63,7 +63,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * @example * const fromObject = { a: 1 }; * Object.keys(fromObject).forEach(key => { - * if (key !== "default" && !Object.hasOwn(toObject, key) { + * if (key !== "default" && !Object.hasOwn(toObject, key)) { * Object.defineProperty(toObject, key, { * get: () => fromObject[key], * enumerable: true @@ -121,7 +121,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * const exports = {}; * const definition = { exportName: () => someExportedValue }; * for (const key in definition) { - * if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key) { + * if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) { * Object.defineProperty(exports, key, { * get: definition[key], * enumerable: true From 8c5f2c897eaa8f92d5aa13f737f61c36352554fd Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 20:22:03 -0300 Subject: [PATCH 059/125] Document patchWebpack better --- src/webpack/patchWebpack.ts | 177 +++++++++++++++++++++--------------- src/webpack/wreq.d.ts | 10 +- 2 files changed, 109 insertions(+), 78 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index efebaf6bbe..719cd40207 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -24,8 +24,95 @@ const logger = new Logger("WebpackInterceptor", "#8caaee"); /** A set with all the module factories objects */ const allModuleFactories = new Set(); +/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ +let wreqFallbackApplied = false; + +// wreq.m is the Webpack object containing module factories. +// We wrap it with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. +// If this is the main Webpack, we also set up the internal references to WebpackRequire. +// wreq.m 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 wreq.m, so this also patches the sentry module factories. +Reflect.defineProperty(Function.prototype, "m", { + configurable: true, + + set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { + // When using React DevTools or other extensions, we may also catch their Webpack here. + // This ensures we actually got the right ones. + const { stack } = new Error(); + if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { + const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); + + // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. + // So if the setter is called, this means we can initialize the internal references to WebpackRequire. + Reflect.defineProperty(this, "p", { + configurable: true, + + set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + if (bundlePath !== "/assets/") return; + + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); + _initWebpack(this); + clearTimeout(setterTimeout); + + Reflect.defineProperty(this, "p", { + value: bundlePath, + configurable: true, + enumerable: true, + writable: true + }); + } + }); + // setImmediate to clear this property setter if this is not the main Webpack. + // If this is the main Webpack, wreq.m will always be set before the timeout runs. + const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); + // This needs to be added before the loop below + allModuleFactories.add(moduleFactories); + + // Patch the pre-populated factories + for (const id in moduleFactories) { + if (Settings.eagerPatches) { + // Patches the factory directly + moduleFactories[id] = patchFactory(id, moduleFactories[id]); + } else { + // Define a getter for the patched version + defineModulesFactoryGetter(id, moduleFactories[id]); + } + } + + Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { + value: "ModuleFactories", + configurable: true, + writable: true, + enumerable: false + }); + + // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions + moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + } + + Reflect.defineProperty(this, "m", { + value: moduleFactories, + configurable: true, + enumerable: true, + writable: true + }); + } +}); + +/** + * Define the getter for returning the patched version of the module factory. This only executes and patches the factory when its accessed for the first time. + * + * It is what patches factories when eagerPatches are disabled. + * + * The factory argument will become the patched version of the factory once it is accessed. + * @param id The id of the module + * @param factory The original or patched module factory + */ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { + // Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object + // have the the patched version for (const moduleFactories of allModuleFactories) { Reflect.defineProperty(moduleFactories, id, { configurable: true, @@ -37,7 +124,6 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto return factory; } - // This patches factories if eagerPatches are disabled return (factory = patchFactory(id, factory)); }, set(v: ModuleFactory) { @@ -52,6 +138,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto } const moduleFactoriesHandler: ProxyHandler = { + // The set trap for patching or defining getters for the module factories when new module factories are loaded set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory if (Number.isNaN(Number(p))) { @@ -67,6 +154,7 @@ const moduleFactoriesHandler: ProxyHandler = { return Reflect.set(target, p, newValue, receiver); } + // eagerPatches are disabled, so set up the getter for the patched version defineModulesFactoryGetter(p, newValue); return true; } @@ -79,7 +167,9 @@ const moduleFactoriesHandler: ProxyHandler = { const patchedFactory = patchFactory(p, newValue); - // Modules are only patched once, so we need to set the patched factory on all the modules + // If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects. + // Because patches are only executed once, we need to set the patched version in all of them, to avoid the Webpack instance + // that uses the factory to contain the original factory instead of the patched, in case it was set first in another instance for (const moduleFactories of allModuleFactories) { Reflect.defineProperty(moduleFactories, p, { value: patchedFactory, @@ -92,77 +182,14 @@ const moduleFactoriesHandler: ProxyHandler = { return true; } }; - -// wreq.m is the Webpack object containing module factories. -// 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 wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them -// If this is the main Webpack, we also set up the internal references to WebpackRequire -Reflect.defineProperty(Function.prototype, "m", { - configurable: true, - - set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { - // When using React DevTools or other extensions, we may also catch their Webpack here. - // This ensures we actually got the right ones - const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - - // setImmediate to clear this property setter if this is not the main Webpack - // If this is the main Webpack, wreq.m will always be set before the timeout runs - const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); - Reflect.defineProperty(this, "p", { - configurable: true, - - set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { - if (bundlePath !== "/assets/") return; - - logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); - _initWebpack(this); - clearTimeout(setterTimeout); - - Reflect.defineProperty(this, "p", { - value: bundlePath, - configurable: true, - enumerable: true, - writable: true - }); - } - }); - - // This needs to be added before the loop below - allModuleFactories.add(moduleFactories); - - for (const id in moduleFactories) { - // If we have eagerPatches enabled we have to patch the pre-populated factories - if (Settings.eagerPatches) { - moduleFactories[id] = patchFactory(id, moduleFactories[id]); - } else { - defineModulesFactoryGetter(id, moduleFactories[id]); - } - } - - Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { - value: "ModuleFactories", - configurable: true, - writable: true, - enumerable: false - }); - moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); - } - - Reflect.defineProperty(this, "m", { - value: moduleFactories, - configurable: true, - enumerable: true, - writable: true - }); - } -}); - -let wreqFallbackApplied = false; - +/** + * Patches a module factory. + * + * The factory argument will become the patched version of the factory. + * @param id The id of the module + * @param factory The original or patched module factory + * @returns The wrapper for the patched module factory + */ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const originalFactory = factory; @@ -288,7 +315,10 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (!patch.all) patches.splice(i--, 1); } + // The patched factory wrapper const patchedFactory: PatchedModuleFactory = function (...args: Parameters) { + // Restore the original factory in all the module factories objects, + // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance for (const moduleFactories of allModuleFactories) { Reflect.defineProperty(moduleFactories, id, { value: patchedFactory.$$vencordOriginal, @@ -327,6 +357,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { let factoryReturn: unknown; try { + // Call the patched factory factoryReturn = factory.apply(this, args); } catch (err) { // Just re-throw Discord errors diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 3dccb629d2..de392668b7 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -33,15 +33,15 @@ export type AsyncModuleBody = ( export type ChunkHandlers = { /** - * Ensures the js file for this chunk is loaded, or starts to load if it's not + * Ensures the js file for this chunk is loaded, or starts to load if it's not. * @param chunkId The chunk id - * @param promises The promises array to add the loading promise to. + * @param promises The promises array to add the loading promise to */ j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, /** - * Ensures the css file for this chunk is loaded, or starts to load if it's not + * Ensures the css file for this chunk is loaded, or starts to load if it's not. * @param chunkId The chunk id - * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too. + * @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too */ css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, }; @@ -171,7 +171,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { */ O: OnChunksLoaded; /** - * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports" + * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports". * @returns The exports argument, but now assigned with the exports of the wasm instance */ v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; From 1f99162b5a4829e1e42aa61c243e101a3842a42b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 26 May 2024 20:38:57 -0300 Subject: [PATCH 060/125] update patchWebpack license --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 719cd40207..b29452a565 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -1,6 +1,6 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2024 Vendicated, Nuckyz, and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ From 513eea8d1498806b116d5919a4f60377497c46c0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 May 2024 18:27:21 -0300 Subject: [PATCH 061/125] Prepare for in case modules object is accessed directly in the future --- src/webpack/patchWebpack.ts | 45 ++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index b29452a565..570e410970 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -72,13 +72,7 @@ Reflect.defineProperty(Function.prototype, "m", { // Patch the pre-populated factories for (const id in moduleFactories) { - if (Settings.eagerPatches) { - // Patches the factory directly - moduleFactories[id] = patchFactory(id, moduleFactories[id]); - } else { - // Define a getter for the patched version - defineModulesFactoryGetter(id, moduleFactories[id]); - } + defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); } Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { @@ -90,6 +84,11 @@ Reflect.defineProperty(Function.prototype, "m", { // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + /* + If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype + Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); + */ + } Reflect.defineProperty(this, "m", { @@ -102,17 +101,17 @@ Reflect.defineProperty(Function.prototype, "m", { }); /** - * Define the getter for returning the patched version of the module factory. This only executes and patches the factory when its accessed for the first time. + * Define the getter for returning the patched version of the module factory. * - * It is what patches factories when eagerPatches are disabled. + * If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original + * and only be patched when accessed for the first time. * - * The factory argument will become the patched version of the factory once it is accessed. * @param id The id of the module * @param factory The original or patched module factory */ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { // Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object - // have the the patched version + // have the patched version for (const moduleFactories of allModuleFactories) { Reflect.defineProperty(moduleFactories, id, { configurable: true, @@ -138,6 +137,17 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto } const moduleFactoriesHandler: ProxyHandler = { + /* + If Discord 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 + and that requires defining additional traps for keeping the object working + + // Proxies on the prototype dont intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined, + // to avoid Reflect.get having no effect and causing a stack overflow + get: (target, p, receiver) => { + return undefined; + }, + */ + // The set trap for patching or defining getters for the module factories when new module factories are loaded set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory @@ -145,7 +155,7 @@ const moduleFactoriesHandler: ProxyHandler = { return Reflect.set(target, p, newValue, receiver); } - const existingFactory = Reflect.get(target, p, target); + const existingFactory = Reflect.get(target, p, receiver); if (!Settings.eagerPatches) { // If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required @@ -154,7 +164,7 @@ const moduleFactoriesHandler: ProxyHandler = { return Reflect.set(target, p, newValue, receiver); } - // eagerPatches are disabled, so set up the getter for the patched version + // eagerPatches are disabled, so the factory argument should be the original defineModulesFactoryGetter(p, newValue); return true; } @@ -170,14 +180,7 @@ const moduleFactoriesHandler: ProxyHandler = { // If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects. // Because patches are only executed once, we need to set the patched version in all of them, to avoid the Webpack instance // that uses the factory to contain the original factory instead of the patched, in case it was set first in another instance - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, p, { - value: patchedFactory, - configurable: true, - enumerable: true, - writable: true - }); - } + defineModulesFactoryGetter(p, patchedFactory); return true; } From a1542bcec5c5f92fe3432c6f517a31c5695e3a1c Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 May 2024 23:21:51 -0300 Subject: [PATCH 062/125] okay codium agent --- scripts/generateReport.ts | 12 ++++++++---- src/webpack/webpack.ts | 8 +++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 89632addf1..cd37674049 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -42,8 +42,8 @@ const page = await browser.newPage(); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); async function maybeGetError(handle: JSHandle) { - return (handle as JSHandle)?.getProperty("message") - .then(m => m.jsonValue()); + return (handle as JSHandle).getProperty("message") + .then(m => m?.jsonValue() ?? "Unknown Error"); } const report = { @@ -356,7 +356,9 @@ async function runtime(token: string) { // setImmediate to avoid blocking the factory patching execution while checking for lazy chunks setTimeout(() => { let isResolved = false; - searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); + searchAndLoadLazyChunks(String(factory)) + .then(() => isResolved = true) + .catch(() => isResolved = true); chunksSearchPromises.push(() => isResolved); }, 0); @@ -364,7 +366,9 @@ async function runtime(token: string) { for (const factoryId in wreq.m) { let isResolved = false; - searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); + searchAndLoadLazyChunks(String(wreq.m[factoryId])) + .then(() => isResolved = true) + .catch(() => isResolved = true); chunksSearchPromises.push(() => isResolved); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index a46a7de459..d9301508de 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -76,6 +76,8 @@ export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; + + if (webpackRequire.c == null) return; cache = webpackRequire.c; Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, { @@ -507,10 +509,10 @@ export function search(...filters: Array) { outer: for (const id in factories) { const factory = factories[id]; - const str = String(factory); + const factoryStr = String(factory); for (const filter of filters) { - if (typeof filter === "string" && !str.includes(filter)) continue outer; - if (filter instanceof RegExp && !filter.test(str)) continue outer; + if (typeof filter === "string" && !factoryStr.includes(factoryStr)) continue outer; + if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer; } results[id] = factory; } From e96458fafa4d1a3587b7299932b91b8ad6a48608 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 May 2024 23:31:08 -0300 Subject: [PATCH 063/125] fixes for if we use prototype proxy in the future --- src/webpack/patchWebpack.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 570e410970..968a2ea0ce 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -88,7 +88,6 @@ Reflect.defineProperty(Function.prototype, "m", { If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); */ - } Reflect.defineProperty(this, "m", { @@ -146,13 +145,23 @@ const moduleFactoriesHandler: ProxyHandler = { get: (target, p, receiver) => { return undefined; }, + // Same thing as get + has: (target, p) => { + return false; + } */ // The set trap for patching or defining getters for the module factories when new module factories are loaded set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory if (Number.isNaN(Number(p))) { - return Reflect.set(target, p, newValue, receiver); + Reflect.defineProperty(target, p, { + value: newValue, + configurable: true, + enumerable: true, + writable: true + }); + return true; } const existingFactory = Reflect.get(target, p, receiver); From 539b70cd52016276c599e9c03c26817b9b921fb6 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Mon, 27 May 2024 23:32:40 -0300 Subject: [PATCH 064/125] lmao oops --- src/webpack/webpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index d9301508de..bde2697760 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -511,7 +511,7 @@ export function search(...filters: Array) { const factory = factories[id]; const factoryStr = String(factory); for (const filter of filters) { - if (typeof filter === "string" && !factoryStr.includes(factoryStr)) continue outer; + if (typeof filter === "string" && !factoryStr.includes(filter)) continue outer; if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer; } results[id] = factory; From 513d9f4209ad6e9de696a8dcc228cffa2355affd Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 03:34:40 -0300 Subject: [PATCH 065/125] I knew I was forgetting something --- src/webpack/wreq.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index de392668b7..fe393a4c14 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -75,7 +75,9 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; /** * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. - * The body function must be an async function. "module.exports" will become a AsyncModulePromise. + * + * The body function must be an async function. "module.exports" will become a {@link AsyncModulePromise}. + * * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies: * @example * const factory = (module, exports, wreq) => { From c9c09b95a3f8faa99f29ac3b0725df57bcb4d539 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 03:35:38 -0300 Subject: [PATCH 066/125] fix typo --- src/webpack/wreq.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index fe393a4c14..246682f643 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -76,7 +76,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { /** * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. * - * The body function must be an async function. "module.exports" will become a {@link AsyncModulePromise}. + * The body function must be an async function. "module.exports" will become an {@link AsyncModulePromise}. * * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies: * @example @@ -84,7 +84,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { * wreq.a(module, async (handleAsyncDependencies, asyncResult) => { * try { * const asyncRequireA = wreq(...); - * + * * const asyncDependencies = handleAsyncDependencies([asyncRequire]); * const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies; * From acbc932542daa1104e3a8a58b6e52f16b8cd80c8 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 03:57:56 -0300 Subject: [PATCH 067/125] give name to PatchedFactory --- src/webpack/patchWebpack.ts | 160 ++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 968a2ea0ce..d7e3630657 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -327,98 +327,100 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { if (!patch.all) patches.splice(i--, 1); } - // The patched factory wrapper - const patchedFactory: PatchedModuleFactory = function (...args: Parameters) { - // Restore the original factory in all the module factories objects, - // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, id, { - value: patchedFactory.$$vencordOriginal, - configurable: true, - enumerable: true, - writable: true - }); - } + // The patched factory wrapper, define it in an object to preserve the name after minification + const patchedFactory: PatchedModuleFactory = { + PatchedFactory(...args: Parameters) { + // Restore the original factory in all the module factories objects, + // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance + for (const moduleFactories of allModuleFactories) { + Reflect.defineProperty(moduleFactories, id, { + value: patchedFactory.$$vencordOriginal, + configurable: true, + enumerable: true, + writable: true + }); + } - // eslint-disable-next-line prefer-const - let [module, exports, require] = args; - - if (wreq == null) { - if (!wreqFallbackApplied) { - wreqFallbackApplied = true; - - // Make sure the require argument is actually the WebpackRequire function - if (typeof require === "function" && require.m != null) { - const { stack } = new Error(); - const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.warn( - "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + - `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + - ")" - ); - _initWebpack(require); - } else if (IS_DEV) { - logger.error("WebpackRequire was not initialized, running modules without patches instead."); + // eslint-disable-next-line prefer-const + let [module, exports, require] = args; + + if (wreq == null) { + if (!wreqFallbackApplied) { + wreqFallbackApplied = true; + + // Make sure the require argument is actually the WebpackRequire function + if (typeof require === "function" && require.m != null) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + + ")" + ); + _initWebpack(require); + } else if (IS_DEV) { + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + } } - } - if (IS_DEV) { - return originalFactory.apply(this, args); + if (IS_DEV) { + return originalFactory.apply(this, args); + } } - } - - let factoryReturn: unknown; - try { - // Call the patched factory - factoryReturn = factory.apply(this, args); - } catch (err) { - // Just re-throw Discord errors - if (factory === originalFactory) throw err; - logger.error("Error in patched module factory", err); - return originalFactory.apply(this, args); - } - - // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it - exports = module?.exports; - if (exports == null) return factoryReturn; - - // There are (at the time of writing) 11 modules exporting the window - // Make these non enumerable to improve webpack search performance - if (exports === window && typeof require === "function" && require.c != null) { - Reflect.defineProperty(require.c, id, { - value: require.c[id], - configurable: true, - writable: true, - enumerable: false - }); - return factoryReturn; - } - - for (const callback of moduleListeners) { + let factoryReturn: unknown; try { - callback(exports, id); + // Call the patched factory + factoryReturn = factory.apply(this, args); } catch (err) { - logger.error("Error in Webpack module listener:\n", err, callback); + // Just re-throw Discord errors + if (factory === originalFactory) throw err; + + logger.error("Error in patched module factory", err); + return originalFactory.apply(this, args); } - } - for (const [filter, callback] of subscriptions) { - try { - if (filter(exports)) { - subscriptions.delete(filter); + // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it + exports = module?.exports; + if (exports == null) return factoryReturn; + + // There are (at the time of writing) 11 modules exporting the window + // Make these non enumerable to improve webpack search performance + if (exports === window && typeof require === "function" && require.c != null) { + Reflect.defineProperty(require.c, id, { + value: require.c[id], + configurable: true, + writable: true, + enumerable: false + }); + return factoryReturn; + } + + for (const callback of moduleListeners) { + try { callback(exports, id); - } else if (exports.default && filter(exports.default)) { - subscriptions.delete(filter); - callback(exports.default, id); + } catch (err) { + logger.error("Error in Webpack module listener:\n", err, callback); } - } catch (err) { - logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); } - } - return factoryReturn; - }; + for (const [filter, callback] of subscriptions) { + try { + if (filter(exports)) { + subscriptions.delete(filter); + callback(exports, id); + } else if (exports.default && filter(exports.default)) { + subscriptions.delete(filter); + callback(exports.default, id); + } + } catch (err) { + logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); + } + } + + return factoryReturn; + } + }.PatchedFactory; patchedFactory.toString = originalFactory.toString.bind(originalFactory); patchedFactory.$$vencordOriginal = originalFactory; From 9c2545ab16b31b6d1d0fcdf56f2aebcfd44a5799 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 04:27:36 -0300 Subject: [PATCH 068/125] undo explosion --- scripts/generateReport.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index cd37674049..ef7ce04a33 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -41,9 +41,9 @@ const browser = await pup.launch({ const page = await browser.newPage(); await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"); -async function maybeGetError(handle: JSHandle) { - return (handle as JSHandle).getProperty("message") - .then(m => m?.jsonValue() ?? "Unknown Error"); +async function maybeGetError(handle: JSHandle): Promise { + return await (handle as JSHandle)?.getProperty("message") + .then(m => m?.jsonValue()); } const report = { @@ -236,7 +236,7 @@ page.on("console", async e => { const [, name] = failedToStartMatch; report.badStarts.push({ plugin: name, - error: cause + error: cause ?? "Unknown error" }); break; From 1eeadbcd97aeb3caa2fcfd80b99a3a3b94dfddf2 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:11:17 -0300 Subject: [PATCH 069/125] export all webpack instances --- src/Vencord.ts | 2 +- src/webpack/patchWebpack.ts | 146 +++++++++++++++++------------------- 2 files changed, 69 insertions(+), 79 deletions(-) diff --git a/src/Vencord.ts b/src/Vencord.ts index 72541148e2..ea769c7895 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -23,10 +23,10 @@ export * as Util from "./utils"; export * as QuickCss from "./utils/quickCss"; export * as Updater from "./utils/updater"; export * as Webpack from "./webpack"; +export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; import "./utils/quickCss"; -import "./webpack/patchWebpack"; import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { StartAt } from "@utils/types"; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d7e3630657..25d0d20f13 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -14,6 +14,8 @@ import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; import { _initWebpack, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; +type AnyWebpackRequire = Partial & Pick; + type PatchedModuleFactory = ModuleFactory & { $$vencordOriginal?: ModuleFactory; }; @@ -22,83 +24,87 @@ type PatchedModuleFactories = Record; const logger = new Logger("WebpackInterceptor", "#8caaee"); -/** A set with all the module factories objects */ -const allModuleFactories = new Set(); +/** A set with all the Webpack instances */ +export const allWebpackInstances = new Set(); /** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ let wreqFallbackApplied = false; +type Define = typeof Reflect.defineProperty; +const define: Define = (target, p, attributes) => { + if (Object.hasOwn(attributes, "value")) { + attributes.writable = true; + } + + return Reflect.defineProperty(target, p, { + configurable: true, + enumerable: true, + ...attributes + }); +}; + // wreq.m is the Webpack object containing module factories. // We wrap it with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. // If this is the main Webpack, we also set up the internal references to WebpackRequire. // wreq.m 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 wreq.m, so this also patches the sentry module factories. -Reflect.defineProperty(Function.prototype, "m", { - configurable: true, +define(Function.prototype, "m", { + enumerable: false, set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { // When using React DevTools or other extensions, we may also catch their Webpack here. // This ensures we actually got the right ones. const { stack } = new Error(); - if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - - // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. - // So if the setter is called, this means we can initialize the internal references to WebpackRequire. - Reflect.defineProperty(this, "p", { - configurable: true, - - set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { - if (bundlePath !== "/assets/") return; - - logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); - _initWebpack(this); - clearTimeout(setterTimeout); - - Reflect.defineProperty(this, "p", { - value: bundlePath, - configurable: true, - enumerable: true, - writable: true - }); - } - }); - // setImmediate to clear this property setter if this is not the main Webpack. - // If this is the main Webpack, wreq.m will always be set before the timeout runs. - const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) && Array.isArray(moduleFactories)) { + define(this, "m", { value: moduleFactories }); + return; + } - // This needs to be added before the loop below - allModuleFactories.add(moduleFactories); + const fileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - // Patch the pre-populated factories - for (const id in moduleFactories) { - defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); - } + allWebpackInstances.add(this); - Reflect.defineProperty(moduleFactories, Symbol.toStringTag, { - value: "ModuleFactories", - configurable: true, - writable: true, - enumerable: false - }); - - // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); - /* - If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype - Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); - */ - } + // Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. + // So if the setter is called, this means we can initialize the internal references to WebpackRequire. + define(this, "p", { + enumerable: false, + + set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + if (bundlePath !== "/assets/") return; + + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); + _initWebpack(this); + clearTimeout(setterTimeout); - Reflect.defineProperty(this, "m", { - value: moduleFactories, - configurable: true, - enumerable: true, - writable: true + define(this, "p", { value: bundlePath }); + } + }); + // setImmediate to clear this property setter if this is not the main Webpack. + // If this is the main Webpack, wreq.m will always be set before the timeout runs. + const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); + + define(moduleFactories, Symbol.toStringTag, { + value: "ModuleFactories", + enumerable: false }); + + // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions + moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + /* + If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype + Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); + */ + + define(this, "m", { value: moduleFactories }); + + // Patch the pre-populated factories + for (const id in moduleFactories) { + defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); + } } }); + /** * Define the getter for returning the patched version of the module factory. * @@ -111,11 +117,8 @@ Reflect.defineProperty(Function.prototype, "m", { function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { // 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 moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, id, { - configurable: true, - enumerable: true, - + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { get() { // $$vencordOriginal means the factory is already patched if (factory.$$vencordOriginal != null) { @@ -155,13 +158,7 @@ const moduleFactoriesHandler: ProxyHandler = { set: (target, p, newValue, receiver) => { // If the property is not a number, we are not dealing with a module factory if (Number.isNaN(Number(p))) { - Reflect.defineProperty(target, p, { - value: newValue, - configurable: true, - enumerable: true, - writable: true - }); - return true; + return define(target, p, { value: newValue }); } const existingFactory = Reflect.get(target, p, receiver); @@ -332,13 +329,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { PatchedFactory(...args: Parameters) { // Restore the original factory in all the module factories objects, // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, id, { - value: patchedFactory.$$vencordOriginal, - configurable: true, - enumerable: true, - writable: true - }); + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { value: patchedFactory.$$vencordOriginal }); } // eslint-disable-next-line prefer-const @@ -387,10 +379,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance if (exports === window && typeof require === "function" && require.c != null) { - Reflect.defineProperty(require.c, id, { + define(require.c, id, { value: require.c[id], - configurable: true, - writable: true, enumerable: false }); return factoryReturn; From 6b648f3d381dbfc9027d8f85839a6fd26f4c7830 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:12:36 -0300 Subject: [PATCH 070/125] oops! --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 25d0d20f13..40c428e650 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -54,7 +54,7 @@ define(Function.prototype, "m", { // When using React DevTools or other extensions, we may also catch their Webpack here. // This ensures we actually got the right ones. const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) && Array.isArray(moduleFactories)) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(moduleFactories)) { define(this, "m", { value: moduleFactories }); return; } From 64262001e8a4d6203864fcf76906238980a31df1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:14:35 -0300 Subject: [PATCH 071/125] boop --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 40c428e650..6cbcd2a464 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -89,13 +89,13 @@ define(Function.prototype, "m", { }); // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + const proxiedModuleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); */ - define(this, "m", { value: moduleFactories }); + define(this, "m", { value: proxiedModuleFactories }); // Patch the pre-populated factories for (const id in moduleFactories) { From d78f1c80004e0558dd5d520316aaa0d423dec420 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:15:25 -0300 Subject: [PATCH 072/125] where did this come from? --- src/webpack/patchWebpack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 6cbcd2a464..b19b16b9ae 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -104,7 +104,6 @@ define(Function.prototype, "m", { } }); - /** * Define the getter for returning the patched version of the module factory. * From 84b9e3fec173e2fd2bba33a2f900ba02b0ee1077 Mon Sep 17 00:00:00 2001 From: vee Date: Tue, 28 May 2024 22:31:58 +0200 Subject: [PATCH 073/125] ConsoleShortcuts: Fix autocomplete on lazies, add more utils (#2519) --- src/plugins/consoleShortcuts/index.ts | 260 +++++++++++++++---------- src/plugins/consoleShortcuts/native.ts | 16 ++ src/utils/lazy.ts | 29 +-- 3 files changed, 192 insertions(+), 113 deletions(-) create mode 100644 src/plugins/consoleShortcuts/native.ts diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index b0efe8a08f..ee86b5fcfd 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -17,138 +17,198 @@ */ import { Devs } from "@utils/constants"; +import { getCurrentChannel, getCurrentGuild } from "@utils/discord"; +import { SYM_LAZY_CACHED, SYM_LAZY_GET } from "@utils/lazy"; import { relaunch } from "@utils/native"; import { canonicalizeMatch, canonicalizeReplace, canonicalizeReplacement } from "@utils/patches"; -import definePlugin, { StartAt } from "@utils/types"; +import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack"; import * as Common from "@webpack/common"; import type { ComponentType } from "react"; -const WEB_ONLY = (f: string) => () => { +const DESKTOP_ONLY = (f: string) => () => { throw new Error(`'${f}' is Discord Desktop only.`); }; +const define: typeof Object.defineProperty = + (obj, prop, desc) => { + if (Object.hasOwn(desc, "value")) + desc.writable = true; + + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: true, + ...desc + }); + }; + +function makeShortcuts() { + function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { + const cache = new Map(); + + return function (...filterProps: unknown[]) { + const cacheKey = String(filterProps); + if (cache.has(cacheKey)) return cache.get(cacheKey); + + const matches = findAll(filterFactory(...filterProps)); + + const result = (() => { + switch (matches.length) { + case 0: return null; + case 1: return matches[0]; + default: + const uniqueMatches = [...new Set(matches)]; + if (uniqueMatches.length > 1) + console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); + + return matches[0]; + } + })(); + if (result && cacheKey) cache.set(cacheKey, result); + return result; + }; + } + + let fakeRenderWin: WeakRef | undefined; + const find = newFindWrapper(f => f); + const findByProps = newFindWrapper(filters.byProps); + + return { + ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])), + wp: Webpack, + wpc: { getter: () => Webpack.cache }, + wreq: { getter: () => Webpack.wreq }, + wpsearch: search, + wpex: extract, + wpexs: (code: string) => extract(findModuleId(code)!), + find, + findAll: findAll, + findByProps, + findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), + findByCode: newFindWrapper(filters.byCode), + findAllByCode: (code: string) => findAll(filters.byCode(code)), + findComponentByCode: newFindWrapper(filters.componentByCode), + findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), + findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], + findStore: newFindWrapper(filters.byStoreName), + PluginsApi: { getter: () => Vencord.Plugins }, + plugins: { getter: () => Vencord.Plugins.plugins }, + Settings: { getter: () => Vencord.Settings }, + Api: { getter: () => Vencord.Api }, + Util: { getter: () => Vencord.Util }, + reload: () => location.reload(), + restart: IS_WEB ? DESKTOP_ONLY("restart") : relaunch, + canonicalizeMatch, + canonicalizeReplace, + canonicalizeReplacement, + fakeRender: (component: ComponentType, props: any) => { + const prevWin = fakeRenderWin?.deref(); + const win = prevWin?.closed === false + ? prevWin + : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; + fakeRenderWin = new WeakRef(win); + win.focus(); + + const doc = win.document; + doc.body.style.margin = "1em"; + + if (!win.prepared) { + win.prepared = true; + + [...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => { + const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement; + + if (s.parentElement?.tagName === "HEAD") + doc.head.append(n); + else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-")) + doc.documentElement.append(n); + else + doc.body.append(n); + }); + } + + Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); + }, + + preEnable: (plugin: string) => (Vencord.Settings.plugins[plugin] ??= { enabled: true }).enabled = true, + + channel: { getter: () => getCurrentChannel(), preload: false }, + channelId: { getter: () => Common.SelectedChannelStore.getChannelId(), preload: false }, + guild: { getter: () => getCurrentGuild(), preload: false }, + guildId: { getter: () => Common.SelectedGuildStore.getGuildId(), preload: false }, + me: { getter: () => Common.UserStore.getCurrentUser(), preload: false }, + meId: { getter: () => Common.UserStore.getCurrentUser().id, preload: false }, + messages: { getter: () => Common.MessageStore.getMessages(Common.SelectedChannelStore.getChannelId()), preload: false } + }; +} + +function loadAndCacheShortcut(key: string, val: any, forceLoad: boolean) { + const currentVal = val.getter(); + if (!currentVal || val.preload === false) return currentVal; + + const value = currentVal[SYM_LAZY_GET] + ? forceLoad ? currentVal[SYM_LAZY_GET]() : currentVal[SYM_LAZY_CACHED] + : currentVal; + + if (value) define(window.shortcutList, key, { value }); + + return value; +} + export default definePlugin({ name: "ConsoleShortcuts", description: "Adds shorter Aliases for many things on the window. Run `shortcutList` for a list.", authors: [Devs.Ven], - getShortcuts(): Record { - function newFindWrapper(filterFactory: (...props: any[]) => Webpack.FilterFn) { - const cache = new Map(); - - return function (...filterProps: unknown[]) { - const cacheKey = String(filterProps); - if (cache.has(cacheKey)) return cache.get(cacheKey); - - const matches = findAll(filterFactory(...filterProps)); - - const result = (() => { - switch (matches.length) { - case 0: return null; - case 1: return matches[0]; - default: - const uniqueMatches = [...new Set(matches)]; - if (uniqueMatches.length > 1) - console.warn(`Warning: This filter matches ${matches.length} modules. Make it more specific!\n`, uniqueMatches); - - return matches[0]; - } - })(); - if (result && cacheKey) cache.set(cacheKey, result); - return result; - }; - } - - let fakeRenderWin: WeakRef | undefined; - const find = newFindWrapper(f => f); - const findByProps = newFindWrapper(filters.byProps); - - return { - ...Object.fromEntries(Object.keys(Common).map(key => [key, { getter: () => Common[key] }])), - wp: Webpack, - wpc: { getter: () => Webpack.cache }, - wreq: { getter: () => Webpack.wreq }, - wpsearch: search, - wpex: extract, - wpexs: (code: string) => extract(findModuleId(code)!), - find, - findAll: findAll, - findByProps, - findAllByProps: (...props: string[]) => findAll(filters.byProps(...props)), - findByCode: newFindWrapper(filters.byCode), - findAllByCode: (code: string) => findAll(filters.byCode(code)), - findComponentByCode: newFindWrapper(filters.componentByCode), - findAllComponentsByCode: (...code: string[]) => findAll(filters.componentByCode(...code)), - findExportedComponent: (...props: string[]) => findByProps(...props)[props[0]], - findStore: newFindWrapper(filters.byStoreName), - PluginsApi: { getter: () => Vencord.Plugins }, - plugins: { getter: () => Vencord.Plugins.plugins }, - Settings: { getter: () => Vencord.Settings }, - Api: { getter: () => Vencord.Api }, - reload: () => location.reload(), - restart: IS_WEB ? WEB_ONLY("restart") : relaunch, - canonicalizeMatch, - canonicalizeReplace, - canonicalizeReplacement, - fakeRender: (component: ComponentType, props: any) => { - const prevWin = fakeRenderWin?.deref(); - const win = prevWin?.closed === false ? prevWin : window.open("about:blank", "Fake Render", "popup,width=500,height=500")!; - fakeRenderWin = new WeakRef(win); - win.focus(); - - const doc = win.document; - doc.body.style.margin = "1em"; - - if (!win.prepared) { - win.prepared = true; - - [...document.querySelectorAll("style"), ...document.querySelectorAll("link[rel=stylesheet]")].forEach(s => { - const n = s.cloneNode(true) as HTMLStyleElement | HTMLLinkElement; - - if (s.parentElement?.tagName === "HEAD") - doc.head.append(n); - else if (n.id?.startsWith("vencord-") || n.id?.startsWith("vcd-")) - doc.documentElement.append(n); - else - doc.body.append(n); - }); - } - - Common.ReactDOM.render(Common.React.createElement(component, props), doc.body.appendChild(document.createElement("div"))); - } - }; - }, - startAt: StartAt.Init, start() { - const shortcuts = this.getShortcuts(); + const shortcuts = makeShortcuts(); window.shortcutList = {}; for (const [key, val] of Object.entries(shortcuts)) { - if (val.getter != null) { - Object.defineProperty(window.shortcutList, key, { - get: val.getter, - configurable: true, - enumerable: true + if ("getter" in val) { + define(window.shortcutList, key, { + get: () => loadAndCacheShortcut(key, val, true) }); - Object.defineProperty(window, key, { - get: () => window.shortcutList[key], - configurable: true, - enumerable: true + define(window, key, { + get: () => window.shortcutList[key] }); } else { window.shortcutList[key] = val; window[key] = val; } } + + // unproxy loaded modules + Webpack.onceReady.then(() => { + setTimeout(() => this.eagerLoad(false), 1000); + + if (!IS_WEB) { + const Native = VencordNative.pluginHelpers.ConsoleShortcuts as PluginNative; + Native.initDevtoolsOpenEagerLoad(); + } + }); + }, + + async eagerLoad(forceLoad: boolean) { + await Webpack.onceReady; + + const shortcuts = makeShortcuts(); + + for (const [key, val] of Object.entries(shortcuts)) { + if (!Object.hasOwn(val, "getter") || (val as any).preload === false) continue; + + try { + loadAndCacheShortcut(key, val, forceLoad); + } catch { } // swallow not found errors in DEV + } }, stop() { delete window.shortcutList; - for (const key in this.getShortcuts()) { + for (const key in makeShortcuts()) { delete window[key]; } } diff --git a/src/plugins/consoleShortcuts/native.ts b/src/plugins/consoleShortcuts/native.ts new file mode 100644 index 0000000000..763b239a47 --- /dev/null +++ b/src/plugins/consoleShortcuts/native.ts @@ -0,0 +1,16 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { IpcMainInvokeEvent } from "electron"; + +export function initDevtoolsOpenEagerLoad(e: IpcMainInvokeEvent) { + const handleDevtoolsOpened = () => e.sender.executeJavaScript("Vencord.Plugins.plugins.ConsoleShortcuts.eagerLoad(true)"); + + if (e.sender.isDevToolsOpened()) + handleDevtoolsOpened(); + else + e.sender.once("devtools-opened", () => handleDevtoolsOpened()); +} diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index b05855fa09..b86533d4d0 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -33,8 +33,8 @@ export function makeLazy(factory: () => T, attempts = 5): () => T { const handler: ProxyHandler = {}; -const kGET = Symbol.for("vencord.lazy.get"); -const kCACHE = Symbol.for("vencord.lazy.cached"); +export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get"); +export const SYM_LAZY_CACHED = Symbol.for("vencord.lazy.cached"); for (const method of [ "apply", @@ -51,11 +51,11 @@ for (const method of [ "setPrototypeOf" ]) { handler[method] = - (target: any, ...args: any[]) => Reflect[method](target[kGET](), ...args); + (target: any, ...args: any[]) => Reflect[method](target[SYM_LAZY_GET](), ...args); } handler.ownKeys = target => { - const v = target[kGET](); + const v = target[SYM_LAZY_GET](); const keys = Reflect.ownKeys(v); for (const key of UNCONFIGURABLE_PROPERTIES) { if (!keys.includes(key)) keys.push(key); @@ -67,7 +67,7 @@ handler.getOwnPropertyDescriptor = (target, p) => { if (typeof p === "string" && UNCONFIGURABLE_PROPERTIES.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); - const descriptor = Reflect.getOwnPropertyDescriptor(target[kGET](), p); + const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p); if (descriptor) Object.defineProperty(target, p, descriptor); return descriptor; @@ -90,31 +90,34 @@ export function proxyLazy(factory: () => T, attempts = 5, isChild = false): T let tries = 0; const proxyDummy = Object.assign(function () { }, { - [kCACHE]: void 0 as T | undefined, - [kGET]() { - if (!proxyDummy[kCACHE] && attempts > tries++) { - proxyDummy[kCACHE] = factory(); - if (!proxyDummy[kCACHE] && attempts === tries) + [SYM_LAZY_CACHED]: void 0 as T | undefined, + [SYM_LAZY_GET]() { + if (!proxyDummy[SYM_LAZY_CACHED] && attempts > tries++) { + proxyDummy[SYM_LAZY_CACHED] = factory(); + if (!proxyDummy[SYM_LAZY_CACHED] && attempts === tries) console.error("Lazy factory failed:", factory); } - return proxyDummy[kCACHE]; + return proxyDummy[SYM_LAZY_CACHED]; } }); return new Proxy(proxyDummy, { ...handler, get(target, p, receiver) { + if (p === SYM_LAZY_CACHED || p === SYM_LAZY_GET) + return Reflect.get(target, p, receiver); + // if we're still in the same tick, it means the lazy was immediately used. // thus, we lazy proxy the get access to make things like destructuring work as expected // meow here will also be a lazy // `const { meow } = findByPropsLazy("meow");` if (!isChild && isSameTick) return proxyLazy( - () => Reflect.get(target[kGET](), p, receiver), + () => Reflect.get(target[SYM_LAZY_GET](), p, receiver), attempts, true ); - const lazyTarget = target[kGET](); + const lazyTarget = target[SYM_LAZY_GET](); if (typeof lazyTarget === "object" || typeof lazyTarget === "function") { return Reflect.get(lazyTarget, p, receiver); } From a4073703fd628622e2b22f59a2b9252cce4db9d7 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:44:34 -0300 Subject: [PATCH 074/125] explain UNCONFIGURABLE_PROPERTIES better --- src/utils/misc.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 089bd541c9..424386a269 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -100,7 +100,7 @@ export function pluralise(amount: number, singular: string, plural = singular + return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; } -/** Unconfigurable properties for proxies */ +/** Proxies which have an internal target but use a function as the main target require these properties to be unconfigurable */ export const UNCONFIGURABLE_PROPERTIES = ["arguments", "caller", "prototype"]; export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) { From 73503945fba7732fae634e754bdfc9658890ddcf Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Tue, 28 May 2024 17:58:54 -0300 Subject: [PATCH 075/125] Add WebpackInstances shortcut --- src/plugins/consoleShortcuts/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index ee86b5fcfd..3d66258a70 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -79,6 +79,7 @@ function makeShortcuts() { wp: Webpack, wpc: { getter: () => Webpack.cache }, wreq: { getter: () => Webpack.wreq }, + WebpackInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances }, wpsearch: search, wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), From 4a872236e1efcb04d0b895ba8d177f75462764b5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 29 May 2024 06:03:31 -0300 Subject: [PATCH 076/125] Fix reporter breaking because of ConsoleShortcuts --- scripts/generateReport.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 8bb87d812c..8d64451430 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -204,8 +204,13 @@ page.on("console", async e => { report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string); } - if (isVencord) { - const args = await Promise.all(e.args().map(a => a.jsonValue())); + if (isVencord && level !== "error") { + let args: unknown[] = []; + try { + args = await Promise.all(e.args().map(a => a.jsonValue())); + } catch { + return; + } const [, tag, message] = args as Array; const cause = await maybeGetError(e.args()[3]); From c8602ef52be0a1171be9506f23ba6208f0be4436 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 29 May 2024 06:45:44 -0300 Subject: [PATCH 077/125] Fix reporter breaking because of ConsoleShortcuts --- scripts/generateReport.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 8bb87d812c..200aa3f834 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -205,7 +205,12 @@ page.on("console", async e => { } if (isVencord) { - const args = await Promise.all(e.args().map(a => a.jsonValue())); + let args: unknown[] = []; + try { + args = await Promise.all(e.args().map(a => a.jsonValue())); + } catch { + return; + } const [, tag, message] = args as Array; const cause = await maybeGetError(e.args()[3]); From 892de53603a1af1bf9f4dbd331868f12fec2edc0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 May 2024 00:51:23 -0300 Subject: [PATCH 078/125] Fix extractAndLoadChunks issue with 2 match groups; Improve testing of lazy extractAndLoadChunks --- scripts/generateReport.ts | 11 +++++------ src/webpack/webpack.ts | 37 ++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 200aa3f834..d3068492f8 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -343,7 +343,7 @@ async function runtime(token: string) { // True if resolved, false otherwise const chunksSearchPromises = [] as Array<() => boolean>; - const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); + const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); async function searchAndLoadLazyChunks(factoryCode: string) { const lazyChunks = factoryCode.matchAll(LazyChunkRegex); @@ -353,8 +353,7 @@ async function runtime(token: string) { // the chunk containing the component const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); - await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => { - const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle; + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : []; if (chunkIds.length === 0) { @@ -525,14 +524,14 @@ async function runtime(token: string) { } else if (method === "extractAndLoadChunks") { const [code, matcher] = args; - const module = Vencord.Webpack.findModuleFactory(...code); - if (module) result = module.toString().match(canonicalizeMatch(matcher)); + result = await Vencord.Webpack.extractAndLoadChunks(code, matcher); + if (result === false) result = null; } else { // @ts-ignore result = Vencord.Webpack[method](...args); } - if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; + if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; } catch (e) { let logMessage = searchType; if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index f2e6e58a81..9e0c0d000e 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -402,14 +402,14 @@ export function findExportedComponentLazy(...props: stri }); } -export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/; -export const ChunkIdsRegex = /\("(.+?)"\)/g; +export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/; +export const ChunkIdsRegex = /\("([^"]+?)"\)/g; /** * Extract and load chunks using their entry point * @param code An array of all the code the module factory containing the lazy chunk loading must include - * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory - * @returns A promise that resolves when the chunks were loaded + * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory + * @returns A promise that resolves with a boolean whether the chunks were loaded */ export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { const module = findModuleFactory(...code); @@ -417,7 +417,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def const err = new Error("extractAndLoadChunks: Couldn't find module factory"); logger.warn(err, "Code:", code, "Matcher:", matcher); - return; + // Strict behaviour in DevBuilds to fail early and make sure the issue is found + if (IS_DEV && !devToolsOpen) + throw err; + + return false; } const match = module.toString().match(canonicalizeMatch(matcher)); @@ -429,10 +433,10 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def if (IS_DEV && !devToolsOpen) throw err; - return; + return false; } - const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match; + const [, rawChunkIds, entryPointId] = match; if (Number.isNaN(Number(entryPointId))) { const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number"); logger.warn(err, "Code:", code, "Matcher:", matcher); @@ -441,16 +445,27 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def if (IS_DEV && !devToolsOpen) throw err; - return; + return false; } - const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle; if (rawChunkIds) { const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]); await Promise.all(chunkIds.map(id => wreq.e(id))); } + if (wreq.m[entryPointId] == null) { + const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load"); + logger.warn(err, "Code:", code, "Matcher:", matcher); + + // Strict behaviour in DevBuilds to fail early and make sure the issue is found + if (IS_DEV && !devToolsOpen) + throw err; + + return false; + } + wreq(entryPointId); + return true; } /** @@ -458,8 +473,8 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def * * Extract and load chunks using their entry point * @param code An array of all the code the module factory containing the lazy chunk loading must include - * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory - * @returns A function that returns a promise that resolves when the chunks were loaded, on first call + * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory + * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call */ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); From 9a9c1b04871599000557d068ab7d00c5b495bfa5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 May 2024 04:44:13 -0300 Subject: [PATCH 079/125] Reporter: Properly implement reporter build of Vencord; Test more plugins; Fix running in wrong pages --- .github/workflows/reportBrokenPlugins.yml | 4 +- package.json | 2 + scripts/build/build.mjs | 22 ++--- scripts/build/buildWeb.mjs | 52 ++++++----- scripts/build/common.mjs | 30 ++++--- scripts/generateReport.ts | 47 ++-------- src/debug/Tracer.ts | 8 +- src/globals.d.ts | 3 +- src/plugins/arRPC.web/index.tsx | 3 +- src/plugins/devCompanion.dev/index.tsx | 3 +- src/plugins/index.ts | 94 ++++++++++++++------ src/plugins/invisibleChat.desktop/index.tsx | 6 +- src/plugins/shikiCodeblocks.desktop/index.ts | 6 +- src/plugins/vcNarrator/index.tsx | 3 +- src/plugins/xsOverlay.desktop/index.ts | 4 +- src/utils/Logger.ts | 5 ++ src/utils/types.ts | 11 +++ src/webpack/common/internal.tsx | 4 +- src/webpack/webpack.ts | 22 ++--- 19 files changed, 184 insertions(+), 145 deletions(-) diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index d3a882fa35..a669c1a276 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -37,8 +37,8 @@ jobs: with: chrome-version: stable - - name: Build web - run: pnpm buildWeb --standalone --dev + - name: Build Vencord Reporter Version + run: pnpm buildReporter - name: Create Report timeout-minutes: 10 diff --git a/package.json b/package.json index 29b1506e2a..881e42a287 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "buildStandalone": "pnpm build --standalone", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", + "buildReporter": "pnpm buildWeb --standalone --reporter --skip-extension", "watch": "pnpm build --watch", + "watchWeb": "pnpm buildWeb --watch", "generatePluginJson": "tsx scripts/generatePluginList.ts", "generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types", "inject": "node scripts/runInstaller.mjs", diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 0c2a930a05..fcf56f66c6 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -21,19 +21,21 @@ import esbuild from "esbuild"; import { readdir } from "fs/promises"; import { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs"; const defines = { - IS_STANDALONE: isStandalone, - IS_DEV: JSON.stringify(isDev), - IS_UPDATER_DISABLED: updaterDisabled, + IS_STANDALONE, + IS_DEV, + IS_REPORTER, + IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, VERSION: JSON.stringify(VERSION), - BUILD_TIMESTAMP, + BUILD_TIMESTAMP }; -if (defines.IS_STANDALONE === "false") - // If this is a local build (not standalone), optimise + +if (defines.IS_STANDALONE === false) + // If this is a local build (not standalone), optimize // for the specific platform we're on defines["process.platform"] = JSON.stringify(process.platform); @@ -46,7 +48,7 @@ const nodeCommonOpts = { platform: "node", target: ["esnext"], external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external], - define: defines, + define: defines }; const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`; @@ -73,13 +75,13 @@ const globNativesPlugin = { let i = 0; for (const dir of pluginDirs) { const dirPath = join("src", dir); - if (!await existsAsync(dirPath)) continue; + if (!await exists(dirPath)) continue; const plugins = await readdir(dirPath); for (const p of plugins) { const nativePath = join(dirPath, p, "native.ts"); const indexNativePath = join(dirPath, p, "native/index.ts"); - if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath))) + if (!(await exists(nativePath)) && !(await exists(indexNativePath))) continue; const nameParts = p.split("."); diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index b4c726064c..04ff674fc8 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises import { join } from "path"; import Zip from "zip-local"; -import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs"; /** * @type {esbuild.BuildOptions} @@ -40,15 +40,16 @@ const commonOptions = { ], target: ["esnext"], define: { - IS_WEB: "true", - IS_EXTENSION: "false", - IS_STANDALONE: "true", - IS_DEV: JSON.stringify(isDev), - IS_DISCORD_DESKTOP: "false", - IS_VESKTOP: "false", - IS_UPDATER_DISABLED: "true", + IS_WEB: true, + IS_EXTENSION: false, + IS_STANDALONE: true, + IS_DEV, + IS_REPORTER, + IS_DISCORD_DESKTOP: false, + IS_VESKTOP: false, + IS_UPDATER_DISABLED: true, VERSION: JSON.stringify(VERSION), - BUILD_TIMESTAMP, + BUILD_TIMESTAMP } }; @@ -87,16 +88,16 @@ await Promise.all( esbuild.build({ ...commonOptions, outfile: "dist/browser.js", - footer: { js: "//# sourceURL=VencordWeb" }, + footer: { js: "//# sourceURL=VencordWeb" } }), esbuild.build({ ...commonOptions, outfile: "dist/extension.js", define: { ...commonOptions?.define, - IS_EXTENSION: "true", + IS_EXTENSION: true, }, - footer: { js: "//# sourceURL=VencordWeb" }, + footer: { js: "//# sourceURL=VencordWeb" } }), esbuild.build({ ...commonOptions, @@ -112,7 +113,7 @@ await Promise.all( footer: { // UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});" - }, + } }) ] ); @@ -165,7 +166,7 @@ async function buildExtension(target, files) { f.startsWith("manifest") ? "manifest.json" : f, content ]; - }))), + }))) }; await rm(target, { recursive: true, force: true }); @@ -192,14 +193,19 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content return appendFile("dist/Vencord.user.js", cssRuntime); }); -await Promise.all([ - appendCssRuntime, - buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), - buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]), -]); +if (!process.argv.includes("--skip-extension")) { + await Promise.all([ + appendCssRuntime, + buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]), + buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]), + ]); -Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip"); -console.info("Packed Chromium Extension written to dist/extension-chrome.zip"); + Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip"); + console.info("Packed Chromium Extension written to dist/extension-chrome.zip"); -Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip"); -console.info("Packed Firefox Extension written to dist/extension-firefox.zip"); + Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip"); + console.info("Packed Firefox Extension written to dist/extension-firefox.zip"); + +} else { + await appendCssRuntime; +} diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index 3b1473e1c8..cdbb26eec8 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -35,24 +35,26 @@ const PackageJSON = JSON.parse(readFileSync("package.json")); export const VERSION = PackageJSON.version; // https://reproducible-builds.org/docs/source-date-epoch/ export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now(); + export const watch = process.argv.includes("--watch"); -export const isDev = watch || process.argv.includes("--dev"); -export const isStandalone = JSON.stringify(process.argv.includes("--standalone")); -export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater")); +export const IS_DEV = watch || process.argv.includes("--dev"); +export const IS_REPORTER = process.argv.includes("--reporter"); +export const IS_STANDALONE = process.argv.includes("--standalone"); + +export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater"); export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); + export const banner = { js: ` // Vencord ${gitHash} -// Standalone: ${isStandalone} -// Platform: ${isStandalone === "false" ? process.platform : "Universal"} -// Updater disabled: ${updaterDisabled} +// Standalone: ${IS_STANDALONE} +// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"} +// Updater Disabled: ${IS_UPDATER_DISABLED} `.trim() }; -const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs")); - -export function existsAsync(path) { - return access(path, FsConstants.F_OK) +export async function exists(path) { + return await access(path, FsConstants.F_OK) .then(() => true) .catch(() => false); } @@ -66,7 +68,7 @@ export const makeAllPackagesExternalPlugin = { setup(build) { const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../" build.onResolve({ filter }, args => ({ path: args.path, external: true })); - }, + } }; /** @@ -89,14 +91,14 @@ export const globPlugins = kind => ({ let plugins = "\n"; let i = 0; for (const dir of pluginDirs) { - if (!await existsAsync(`./src/${dir}`)) continue; + if (!await exists(`./src/${dir}`)) continue; const files = await readdir(`./src/${dir}`); for (const file of files) { if (file.startsWith("_") || file.startsWith(".")) continue; if (file === "index.ts") continue; const target = getPluginTarget(file); - if (target) { + if (target && !IS_REPORTER) { if (target === "dev" && !watch) continue; if (target === "web" && kind === "discordDesktop") continue; if (target === "desktop" && kind === "web") continue; @@ -178,7 +180,7 @@ export const fileUrlPlugin = { build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => { const { searchParams } = new URL(uri); const base64 = searchParams.has("base64"); - const minify = isStandalone === "true" && searchParams.has("minify"); + const minify = IS_STANDALONE === true && searchParams.has("minify"); const noTrim = searchParams.get("trim") === "false"; const encoding = base64 ? "base64" : "utf-8"; diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index d3068492f8..8233f3e5d6 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -282,7 +282,7 @@ page.on("pageerror", e => console.error("[Page Error]", e)); await page.setBypassCSP(true); -async function runtime(token: string) { +async function reporterRuntime(token: string) { console.log("[PUP_DEBUG]", "Starting test..."); try { @@ -290,43 +290,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 - // @ts-ignore - const originalLog = Vencord.Util.Logger.prototype._log; - // @ts-ignore - Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { - if (level === "warn" || level === "error") - return console[level]("[Vencord]", this.name + ":", ...args); - - return originalLog.call(this, level, levelColor, args); - }; - - // Force enable all plugins and patches - Vencord.Plugins.patches.length = 0; - Object.values(Vencord.Plugins.plugins).forEach(p => { - // Needs native server to run - if (p.name === "WebRichPresence (arRPC)") return; - - Vencord.Settings.plugins[p.name].enabled = true; - p.patches?.forEach(patch => { - patch.plugin = p.name; - delete patch.predicate; - delete patch.group; - - Vencord.Util.canonicalizeFind(patch); - if (!Array.isArray(patch.replacement)) { - patch.replacement = [patch.replacement]; - } - - patch.replacement.forEach(r => { - delete r.predicate; - }); - - Vencord.Plugins.patches.push(patch); - }); + } }); let wreq: typeof Vencord.Webpack.wreq; @@ -549,9 +513,10 @@ async function runtime(token: string) { } await page.evaluateOnNewDocument(` - ${readFileSync("./dist/browser.js", "utf-8")} - - ;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + if (location.host.endsWith("discord.com")) { + ${readFileSync("./dist/browser.js", "utf-8")}; + (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + } `); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); diff --git a/src/debug/Tracer.ts b/src/debug/Tracer.ts index 4337e00193..7d80f425ce 100644 --- a/src/debug/Tracer.ts +++ b/src/debug/Tracer.ts @@ -18,14 +18,14 @@ import { Logger } from "@utils/Logger"; -if (IS_DEV) { +if (IS_DEV || IS_REPORTER) { var traces = {} as Record; var logger = new Logger("Tracer", "#FFD166"); } const noop = function () { }; -export const beginTrace = !IS_DEV ? noop : +export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop : function beginTrace(name: string, ...args: any[]) { if (name in traces) throw new Error(`Trace ${name} already exists!`); @@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop : traces[name] = [performance.now(), args]; }; -export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) { +export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) { const end = performance.now(); const [start, args] = traces[name]; @@ -48,7 +48,7 @@ type TraceNameMapper = (...args: Parameters) => string; const noopTracer = (name: string, f: F, mapper?: TraceNameMapper) => f; -export const traceFunction = !IS_DEV +export const traceFunction = !(IS_DEV || IS_REPORTER) ? noopTracer : function traceFunction(name: string, f: F, mapper?: TraceNameMapper): F { return function (this: any, ...args: Parameters) { diff --git a/src/globals.d.ts b/src/globals.d.ts index 94b5f15e85..e20ca4b71a 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -34,9 +34,10 @@ declare global { */ export var IS_WEB: boolean; export var IS_EXTENSION: boolean; - export var IS_DEV: boolean; export var IS_STANDALONE: boolean; export var IS_UPDATER_DISABLED: boolean; + export var IS_DEV: boolean; + export var IS_REPORTER: boolean; export var IS_DISCORD_DESKTOP: boolean; export var IS_VESKTOP: boolean; export var VERSION: string; diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx index 423dce9b52..e41e8675ed 100644 --- a/src/plugins/arRPC.web/index.tsx +++ b/src/plugins/arRPC.web/index.tsx @@ -19,7 +19,7 @@ import { popNotice, showNotice } from "@api/Notices"; import { Link } from "@components/Link"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { ReporterTestable } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common"; @@ -41,6 +41,7 @@ export default definePlugin({ name: "WebRichPresence (arRPC)", description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", authors: [Devs.Ducko], + reporterTestable: ReporterTestable.None, settingsAboutComponent: () => ( <> diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index 25fd563e46..a495907b2d 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { filters, findAll, search } from "@webpack"; const PORT = 8485; @@ -243,6 +243,7 @@ export default definePlugin({ name: "DevCompanion", description: "Dev Companion Plugin", authors: [Devs.Ven], + reporterTestable: ReporterTestable.None, settings, toolboxActions: { diff --git a/src/plugins/index.ts b/src/plugins/index.ts index a434b4a6fe..53ab7983a5 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -21,7 +21,7 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu"; import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; import { canonicalizeFind } from "@utils/patches"; -import { Patch, Plugin, StartAt } from "@utils/types"; +import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; import { FluxEvents } from "@webpack/types"; @@ -39,35 +39,68 @@ export const patches = [] as Patch[]; let enabledPluginsSubscribedFlux = false; const subscribedFluxEventsPlugins = new Set(); +const pluginsValues = Object.values(Plugins); const settings = Settings.plugins; export function isPluginEnabled(p: string) { return ( + IS_REPORTER || Plugins[p]?.required || Plugins[p]?.isDependency || settings[p]?.enabled ) ?? false; } -const pluginsValues = Object.values(Plugins); +export function addPatch(newPatch: Omit, pluginName: string) { + const patch = newPatch as Patch; + patch.plugin = pluginName; + + if (IS_REPORTER) { + delete patch.predicate; + delete patch.group; + } + + canonicalizeFind(patch); + if (!Array.isArray(patch.replacement)) { + patch.replacement = [patch.replacement]; + } + + if (IS_REPORTER) { + patch.replacement.forEach(r => { + delete r.predicate; + }); + } -// First roundtrip to mark and force enable dependencies (only for enabled plugins) + patches.push(patch); +} + +function isReporterTestable(p: Plugin, part: ReporterTestable) { + return p.reporterTestable == null + ? true + : (p.reporterTestable & part) === part; +} + +// First round-trip to mark and force enable dependencies // // FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only // goes for the top level and their children, but for now this works okay with the current API plugins -for (const p of pluginsValues) if (settings[p.name]?.enabled) { +for (const p of pluginsValues) if (isPluginEnabled(p.name)) { p.dependencies?.forEach(d => { const dep = Plugins[d]; - if (dep) { - settings[d].enabled = true; - dep.isDependency = true; - } - else { + + if (!dep) { const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`); - if (IS_DEV) + + if (IS_DEV) { throw error; + } + logger.warn(error); + return; } + + settings[d].enabled = true; + dep.isDependency = true; }); } @@ -82,23 +115,18 @@ for (const p of pluginsValues) { } if (p.patches && isPluginEnabled(p.name)) { - for (const patch of p.patches) { - patch.plugin = p.name; - - canonicalizeFind(patch); - if (!Array.isArray(patch.replacement)) { - patch.replacement = [patch.replacement]; + if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) { + for (const patch of p.patches) { + addPatch(patch, p.name); } - - patches.push(patch); } } } export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) { logger.info(`Starting plugins (stage ${target})`); - for (const name in Plugins) - if (isPluginEnabled(name)) { + for (const name in Plugins) { + if (isPluginEnabled(name) && (!IS_REPORTER || isReporterTestable(Plugins[name], ReporterTestable.Start))) { const p = Plugins[name]; const startAt = p.startAt ?? StartAt.WebpackReady; @@ -106,30 +134,38 @@ export const startAllPlugins = traceFunction("startAllPlugins", function startAl startPlugin(Plugins[name]); } + } }); export function startDependenciesRecursive(p: Plugin) { let restartNeeded = false; const failures: string[] = []; - p.dependencies?.forEach(dep => { - if (!Settings.plugins[dep].enabled) { - startDependenciesRecursive(Plugins[dep]); + + p.dependencies?.forEach(d => { + if (!settings[d].enabled) { + const dep = Plugins[d]; + startDependenciesRecursive(dep); + // If the plugin has patches, don't start the plugin, just enable it. - Settings.plugins[dep].enabled = true; - if (Plugins[dep].patches) { - logger.warn(`Enabling dependency ${dep} requires restart.`); + settings[d].enabled = true; + dep.isDependency = true; + + if (dep.patches) { + logger.warn(`Enabling dependency ${d} requires restart.`); restartNeeded = true; return; } - const result = startPlugin(Plugins[dep]); - if (!result) failures.push(dep); + + const result = startPlugin(dep); + if (!result) failures.push(d); } }); + return { restartNeeded, failures }; } export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) { - if (p.flux && !subscribedFluxEventsPlugins.has(p.name)) { + if (p.flux && !subscribedFluxEventsPlugins.has(p.name) && (!IS_REPORTER || isReporterTestable(p, ReporterTestable.FluxEvents))) { subscribedFluxEventsPlugins.add(p.name); logger.debug("Subscribing to flux events of plugin", p.name); diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx index 884ffafe37..01199d9996 100644 --- a/src/plugins/invisibleChat.desktop/index.tsx +++ b/src/plugins/invisibleChat.desktop/index.tsx @@ -23,7 +23,7 @@ import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import { getStegCloak } from "@utils/dependencies"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { ChannelStore, Constants, RestAPI, Tooltip } from "@webpack/common"; import { Message } from "discord-types/general"; @@ -105,6 +105,9 @@ export default definePlugin({ description: "Encrypt your Messages in a non-suspicious way!", authors: [Devs.SammCheese], dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"], + reporterTestable: ReporterTestable.Patches, + settings, + patches: [ { // Indicator @@ -121,7 +124,6 @@ export default definePlugin({ URL_REGEX: new RegExp( /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/, ), - settings, async start() { addButton("InvisibleChat", message => { return this.INV_REGEX.test(message?.content) diff --git a/src/plugins/shikiCodeblocks.desktop/index.ts b/src/plugins/shikiCodeblocks.desktop/index.ts index 27463195d2..e6676a866c 100644 --- a/src/plugins/shikiCodeblocks.desktop/index.ts +++ b/src/plugins/shikiCodeblocks.desktop/index.ts @@ -20,7 +20,7 @@ import "./shiki.css"; import { enableStyle } from "@api/Styles"; import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; +import definePlugin, { ReporterTestable } from "@utils/types"; import previewExampleText from "file://previewExample.tsx"; import { shiki } from "./api/shiki"; @@ -34,6 +34,9 @@ export default definePlugin({ name: "ShikiCodeblocks", description: "Brings vscode-style codeblocks into Discord, powered by Shiki", authors: [Devs.Vap], + reporterTestable: ReporterTestable.Patches, + settings, + patches: [ { find: "codeBlock:{react(", @@ -66,7 +69,6 @@ export default definePlugin({ isPreview: true, tempSettings, }), - settings, // exports shiki, diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx index ac629e749a..6e8e4bbf5e 100644 --- a/src/plugins/vcNarrator/index.tsx +++ b/src/plugins/vcNarrator/index.tsx @@ -22,7 +22,7 @@ import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { wordsToTitle } from "@utils/text"; -import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types"; +import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common"; @@ -155,6 +155,7 @@ export default definePlugin({ name: "VcNarrator", description: "Announces when users join, leave, or move voice channels via narrator", authors: [Devs.Ven], + reporterTestable: ReporterTestable.None, flux: { VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts index 5251959f2f..caa44a40c4 100644 --- a/src/plugins/xsOverlay.desktop/index.ts +++ b/src/plugins/xsOverlay.desktop/index.ts @@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import definePlugin, { OptionType, PluginNative } from "@utils/types"; +import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { ChannelStore, GuildStore, UserStore } from "@webpack/common"; import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general"; @@ -143,7 +143,9 @@ export default definePlugin({ description: "Forwards discord notifications to XSOverlay, for easy viewing in VR", authors: [Devs.Nyako], tags: ["vr", "notify"], + reporterTestable: ReporterTestable.None, settings, + flux: { CALL_UPDATE({ call }: { call: Call; }) { if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 1ae4762d7b..e222d71fbe 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -32,6 +32,11 @@ export class Logger { constructor(public name: string, public color: string = "white") { } private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { + if (IS_REPORTER && (level === "warn" || level === "error")) { + console[level]("[Vencord]", this.name + ":", ...args); + return; + } + console[level]( `%c Vencord %c %c ${this.name} ${customFmt}`, `background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`, diff --git a/src/utils/types.ts b/src/utils/types.ts index 6e15241967..fe19a10936 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -94,6 +94,10 @@ export interface PluginDef { * @default StartAt.WebpackReady */ startAt?: StartAt, + /** + * Which parts of the plugin can be tested by the reporter. Defaults to all parts + */ + reporterTestable?: number; /** * Optionally provide settings that the user can configure in the Plugins tab of settings. * @deprecated Use `settings` instead @@ -144,6 +148,13 @@ export const enum StartAt { WebpackReady = "WebpackReady" } +export const enum ReporterTestable { + None = 1 << 1, + Start = 1 << 2, + Patches = 1 << 3, + FluxEvents = 1 << 4 +} + export const enum OptionType { STRING, NUMBER, diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx index 9a89af362e..8957c254b9 100644 --- a/src/webpack/common/internal.tsx +++ b/src/webpack/common/internal.tsx @@ -22,7 +22,7 @@ import { LazyComponent } from "@utils/react"; import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack"; export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T { - if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]); let myValue: T = function () { throw new Error(`Vencord could not find the ${name} Component`); @@ -38,7 +38,7 @@ export function waitForComponent = React.Comp } export function waitForStore(name: string, cb: (v: any) => void) { - if (IS_DEV) lazyWebpackSearchHistory.push(["waitForStore", [name]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForStore", [name]]); waitFor(filters.byStoreName(name), cb, { isIndirect: true }); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 9e0c0d000e..ec7218c649 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -264,7 +264,7 @@ export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "f * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); */ export function proxyLazyWebpack(factory: () => any, attempts?: number) { - if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); return proxyLazy(factory, attempts); } @@ -278,7 +278,7 @@ export function proxyLazyWebpack(factory: () => any, attempts?: number) * @returns Result of factory function */ export function LazyComponentWebpack(factory: () => any, attempts?: number) { - if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]); return LazyComponent(factory, attempts); } @@ -287,7 +287,7 @@ export function LazyComponentWebpack(factory: () => any, * Find the first module that matches the filter, lazily */ export function findLazy(filter: FilterFn) { - if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["find", [filter]]); return proxyLazy(() => find(filter)); } @@ -306,7 +306,7 @@ export function findByProps(...props: string[]) { * Find the first module that has the specified properties, lazily */ export function findByPropsLazy(...props: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]); return proxyLazy(() => findByProps(...props)); } @@ -325,7 +325,7 @@ export function findByCode(...code: string[]) { * Find the first function that includes all the given code, lazily */ export function findByCodeLazy(...code: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]); return proxyLazy(() => findByCode(...code)); } @@ -344,7 +344,7 @@ export function findStore(name: string) { * Find a store by its displayName, lazily */ export function findStoreLazy(name: string) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]); return proxyLazy(() => findStore(name)); } @@ -363,7 +363,7 @@ export function findComponentByCode(...code: string[]) { * Finds the first component that matches the filter, lazily. */ export function findComponentLazy(filter: FilterFn) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponent", [filter]]); return LazyComponent(() => { @@ -378,7 +378,7 @@ export function findComponentLazy(filter: FilterFn) { * Finds the first component that includes all the given code, lazily */ export function findComponentByCodeLazy(...code: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]); return LazyComponent(() => { const res = find(filters.componentByCode(...code), { isIndirect: true }); @@ -392,7 +392,7 @@ export function findComponentByCodeLazy(...code: string[ * Finds the first component that is exported by the first prop name, lazily */ export function findExportedComponentLazy(...props: string[]) { - if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]); return LazyComponent(() => { const res = find(filters.byProps(...props), { isIndirect: true }); @@ -477,7 +477,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call */ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { - if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); + if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); return makeLazy(() => extractAndLoadChunks(code, matcher)); } @@ -487,7 +487,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac * then call the callback with the module as the first argument */ export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { - if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); + if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); if (typeof filter === "string") filter = filters.byProps(filter); From 3c62e393a3cc09d51cf3c1be854dae79d2d5adad Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 May 2024 13:38:22 -0300 Subject: [PATCH 080/125] edit another license header --- src/webpack/wreq.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 246682f643..6611680bdd 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -1,6 +1,6 @@ /* * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors + * Copyright (c) 2024 Vendicated, Nuckyz and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ From 3a25da5f1443b3caf6ea0b410011b5319226f58c Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 May 2024 18:03:00 -0300 Subject: [PATCH 081/125] Fix wrong external files and clean up build script; Remove non used stuff --- .github/workflows/build.yml | 2 +- .github/workflows/publish.yml | 2 +- package.json | 3 ++- scripts/build/buildWeb.mjs | 2 +- src/utils/dependencies.ts | 10 ---------- 5 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7a2f24e89..ba22b12301 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWebStandalone - name: Build run: pnpm build --standalone diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8407e08e2d..190e3069c4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Build web - run: pnpm buildWeb --standalone + run: pnpm buildWebStandalone - name: Publish extension run: | diff --git a/package.json b/package.json index 881e42a287..43ac363043 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs", "buildStandalone": "pnpm build --standalone", "buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs", - "buildReporter": "pnpm buildWeb --standalone --reporter --skip-extension", + "buildWebStandalone": "pnpm buildWeb --standalone", + "buildReporter": "pnpm buildWebStandalone --reporter --skip-extension", "watch": "pnpm build --watch", "watchWeb": "pnpm buildWeb --watch", "generatePluginJson": "tsx scripts/generatePluginList.ts", diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs index 04ff674fc8..bc15cccedc 100644 --- a/scripts/build/buildWeb.mjs +++ b/scripts/build/buildWeb.mjs @@ -33,7 +33,7 @@ const commonOptions = { entryPoints: ["browser/Vencord.ts"], globalName: "Vencord", format: "iife", - external: ["plugins", "git-hash", "/assets/*"], + external: ["~plugins", "~git-hash", "/assets/*"], plugins: [ globPlugins("web"), ...commonOpts.plugins, diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index f05900e146..d8c361e884 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -17,7 +17,6 @@ */ import { makeLazy } from "./lazy"; -import { EXTENSION_BASE_URL } from "./web-metadata"; /* Add dynamically loaded dependencies for plugins here. @@ -67,15 +66,6 @@ export interface ApngFrameData { playTime: number; } -// On web (extensions), use extension uri as basepath (load files from extension) -// On desktop (electron), load from cdn -export const rnnoiseDist = IS_EXTENSION - ? new URL("/third-party/rnnoise", EXTENSION_BASE_URL).toString() - : "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.3/dist"; -export const rnnoiseWasmSrc = (simd = false) => `${rnnoiseDist}/rnnoise${simd ? "_simd" : ""}.wasm`; -export const rnnoiseWorkletSrc = `${rnnoiseDist}/rnnoise/workletProcessor.js`; - - // The below code is only used on the Desktop (electron) build of Vencord. // Browser (extension) builds do not contain these remote imports. From 47eec049fc98548846554115016ca7a220f9af6b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 00:21:15 -0300 Subject: [PATCH 082/125] Not needed anymore and breaks interpolated strings with newlines --- src/webpack/patchWebpack.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index ffc4100e0c..d728e5a2f4 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -211,16 +211,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const patchedBy = new Set(); - // Discords Webpack chunks for some ungodly reason contain random - // newlines. Cyn recommended this workaround and it seems to work fine, - // however this could potentially break code, so if anything goes weird, - // this is probably why. - // Additionally, `[actual newline]` is one less char than "\n", so if Discord - // ever targets newer browsers, the minifier could potentially use this trick and - // cause issues. - // - // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, - let code: string = "0," + String(factory).replaceAll("\n", ""); + // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, + let code: string = "0," + String(factory); for (let i = 0; i < patches.length; i++) { const patch = patches[i]; From 545d69aa6df8ea6c2d0712f3c9d02af21e310b90 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 05:19:34 -0300 Subject: [PATCH 083/125] Use indexOf instead of includes; reset global find regex state --- src/webpack/patchWebpack.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d728e5a2f4..b0f735d2e3 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -218,9 +218,11 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const patch = patches[i]; if (patch.predicate && !patch.predicate()) continue; + // indexOf is faster than includes because it doesn't check if searchString is a RegExp + // https://github.com/moonlight-mod/moonlight/blob/53ae39d4010277f49f3b70bebbd27b9cbcdb1c8b/packages/core/src/patch.ts#L61 const moduleMatches = typeof patch.find === "string" - ? code.includes(patch.find) - : patch.find.test(code); + ? code.indexOf(patch.find) !== -1 + : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code)); if (!moduleMatches) continue; From f4cc95da5a0edf7491e58aca6c0ac7c78bbd7acb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 05:29:01 -0300 Subject: [PATCH 084/125] Use indexOf in more places --- src/webpack/webpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index f318558702..af57a324eb 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -48,7 +48,7 @@ export const filters = { if (typeof m !== "function") return false; const s = Function.prototype.toString.call(m); for (const c of code) { - if (!s.includes(c)) return false; + if (s.indexOf(c) === -1) return false; } return true; }, @@ -229,7 +229,7 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( const str = String(wreq.m[id]); for (const c of code) { - if (!str.includes(c)) continue outer; + if (str.indexOf(c) === -1) continue outer; } return id; } @@ -526,7 +526,7 @@ export function search(...filters: Array) { const factory = factories[id]; const factoryStr = String(factory); for (const filter of filters) { - if (typeof filter === "string" && !factoryStr.includes(filter)) continue outer; + if (typeof filter === "string" && factoryStr.indexOf(filter) === -1) continue outer; if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer; } results[id] = factory; From db63fffe18c1a4c526d32cd0033842645c8b1dfe Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 06:16:01 -0300 Subject: [PATCH 085/125] I should still be defining this --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index b0f735d2e3..4a609666fc 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -70,13 +70,13 @@ define(Function.prototype, "m", { enumerable: false, set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + define(this, "p", { value: bundlePath }); + if (bundlePath !== "/assets/") return; logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); clearTimeout(setterTimeout); - - define(this, "p", { value: bundlePath }); } }); // setImmediate to clear this property setter if this is not the main Webpack. From e21a6be4b640a20e6d72e88419eb3e63f05f4b6b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 06:19:57 -0300 Subject: [PATCH 086/125] sfdsfdsfdsf --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 4a609666fc..8c974f28eb 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -71,12 +71,12 @@ define(Function.prototype, "m", { set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { define(this, "p", { value: bundlePath }); + clearTimeout(setterTimeout); if (bundlePath !== "/assets/") return; logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); - clearTimeout(setterTimeout); } }); // setImmediate to clear this property setter if this is not the main Webpack. From 4187932aa71b0d7113adb50120afe46d99fe26a9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 06:27:02 -0300 Subject: [PATCH 087/125] \n --- src/webpack/patchWebpack.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 8c974f28eb..a2264c5419 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -190,6 +190,7 @@ const moduleFactoriesHandler: ProxyHandler = { return true; } }; + /** * Patches a module factory. * From fbb7ee50dd636900467cbae44e3bdd88273f62eb Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 17:34:32 -0300 Subject: [PATCH 088/125] Revert indexOf change --- src/webpack/patchWebpack.ts | 4 +--- src/webpack/webpack.ts | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a2264c5419..abfabb4384 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -219,10 +219,8 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { const patch = patches[i]; if (patch.predicate && !patch.predicate()) continue; - // indexOf is faster than includes because it doesn't check if searchString is a RegExp - // https://github.com/moonlight-mod/moonlight/blob/53ae39d4010277f49f3b70bebbd27b9cbcdb1c8b/packages/core/src/patch.ts#L61 const moduleMatches = typeof patch.find === "string" - ? code.indexOf(patch.find) !== -1 + ? code.includes(patch.find) : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code)); if (!moduleMatches) continue; diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index af57a324eb..f318558702 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -48,7 +48,7 @@ export const filters = { if (typeof m !== "function") return false; const s = Function.prototype.toString.call(m); for (const c of code) { - if (s.indexOf(c) === -1) return false; + if (!s.includes(c)) return false; } return true; }, @@ -229,7 +229,7 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( const str = String(wreq.m[id]); for (const c of code) { - if (str.indexOf(c) === -1) continue outer; + if (!str.includes(c)) continue outer; } return id; } @@ -526,7 +526,7 @@ export function search(...filters: Array) { const factory = factories[id]; const factoryStr = String(factory); for (const filter of filters) { - if (typeof filter === "string" && factoryStr.indexOf(filter) === -1) continue outer; + if (typeof filter === "string" && !factoryStr.includes(filter)) continue outer; if (filter instanceof RegExp && !filter.test(factoryStr)) continue outer; } results[id] = factory; From cbd9a9312cc1bc4acb362f526051caf1d08b9d84 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 31 May 2024 19:58:26 -0300 Subject: [PATCH 089/125] don't depend in the value of bundlePath --- src/webpack/patchWebpack.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index abfabb4384..3734331661 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -73,8 +73,6 @@ define(Function.prototype, "m", { define(this, "p", { value: bundlePath }); clearTimeout(setterTimeout); - if (bundlePath !== "/assets/") return; - logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); } From 01fdeb7292d583bce2ccdf7e8c00d3fa8b9902d9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 30 May 2024 17:32:54 -0300 Subject: [PATCH 090/125] cya webpackChunkdiscord_app part 2 --- src/globals.d.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/globals.d.ts b/src/globals.d.ts index e20ca4b71a..4456564ccb 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -64,13 +64,8 @@ declare global { export var Vesktop: any; export var VesktopNative: any; - interface Window { - webpackChunkdiscord_app: { - push(chunk: any): any; - pop(): any; - }; + interface Window extends Record { _: LoDashStatic; - [k: string]: any; } } From 0aff3c29bc7ef885fc85cc7a2a8d82d7542075b4 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 00:11:00 -0300 Subject: [PATCH 091/125] Fix reporter --- src/debug/runReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 002fbae708..7b625b01b9 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -30,7 +30,7 @@ async function runReporter() { }, "Vencord Reporter"); // @ts-ignore - Vencord.Webpack._initReporter = function () { + Webpack._initReporter = function () { // initReporter is called in the patched entry point of Discord // setImmediate to only start searching for lazy chunks after Discord initialized the app setTimeout(() => { From dd4d80872efbddba326ecd4c22740ecd496a5949 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 00:17:15 -0300 Subject: [PATCH 092/125] fix reporter part 2 --- src/debug/runReporter.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 7b625b01b9..b8bfc759e2 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -30,7 +30,7 @@ async function runReporter() { }, "Vencord Reporter"); // @ts-ignore - Webpack._initReporter = function () { + Vencord.Webpack._initReporter = function () { // initReporter is called in the patched entry point of Discord // setImmediate to only start searching for lazy chunks after Discord initialized the app setTimeout(() => { @@ -242,4 +242,5 @@ async function runReporter() { } } -runReporter(); +// Run after the Vencord object has been created +setTimeout(runReporter, 0); From db5397f41a880399a5af00357fd4489bdf9c8581 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 00:22:17 -0300 Subject: [PATCH 093/125] sdfdsf --- src/debug/runReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index b8bfc759e2..05ecf719d9 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -157,7 +157,7 @@ async function runReporter() { // Require deferred entry points for (const deferredRequire of deferredRequires) { - wreq(deferredRequire as any); + wreq(deferredRequire); } // All chunks Discord has mapped to asset files, even if they are not used anymore From 4d8c56689cc1647b2c9810906625de4b25119641 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 01:40:33 -0300 Subject: [PATCH 094/125] Future proof against array modules --- src/webpack/patchWebpack.ts | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 3734331661..944d83e863 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -42,20 +42,29 @@ const define: Define = (target, p, attributes) => { }); }; -// wreq.m is the Webpack object containing module factories. -// We wrap it with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. -// If this is the main Webpack, we also set up the internal references to WebpackRequire. +// wreq.O is the Webpack onChunksLoaded function. +// It is pretty likely that all important Discord Webpack instances will have this property set, +// because Discord bundled code is chunked. +// As of the time of writing, only the main and sentry Webpack instances have this property, and they are the only ones we care about. + +// We use this setter to intercept when wreq.O is defined, and apply the patching in the modules factories (wreq.m). // wreq.m 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 wreq.m, so this also patches the sentry module factories. -define(Function.prototype, "m", { +// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. + +// If this is the main Webpack, we also set up the internal references to WebpackRequire. +define(Function.prototype, "O", { enumerable: false, - set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) { - // When using React DevTools or other extensions, we may also catch their Webpack here. - // This ensures we actually got the right ones. + set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) { + define(this, "O", { value: onChunksLoaded }); + const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(moduleFactories)) { - define(this, "m", { value: moduleFactories }); + if (!stack?.includes("discord.com") && !stack?.includes("discordapp.com")) { + return; + } + + if (this.m == null) { return; } @@ -81,24 +90,24 @@ define(Function.prototype, "m", { // If this is the main Webpack, wreq.m will always be set before the timeout runs. const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); - define(moduleFactories, Symbol.toStringTag, { + // Patch the pre-populated factories + for (const id in this.m) { + defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, this.m[id]) : this.m[id]); + } + + define(this.m, Symbol.toStringTag, { value: "ModuleFactories", enumerable: false }); // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - const proxiedModuleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); + const proxiedModuleFactories = new Proxy(this.m, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); */ define(this, "m", { value: proxiedModuleFactories }); - - // Patch the pre-populated factories - for (const id in moduleFactories) { - defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, moduleFactories[id]) : moduleFactories[id]); - } } }); From 8580332c8222f68804141718038487a769250628 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 01:44:18 -0300 Subject: [PATCH 095/125] cleanup --- src/webpack/patchWebpack.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 944d83e863..d3d722b7ab 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -60,11 +60,7 @@ define(Function.prototype, "O", { define(this, "O", { value: onChunksLoaded }); const { stack } = new Error(); - if (!stack?.includes("discord.com") && !stack?.includes("discordapp.com")) { - return; - } - - if (this.m == null) { + if (this.m == null || !(stack?.includes("discord.com") || stack?.includes("discordapp.com"))) { return; } From 33d5753ca7f7105dc689e3f59994a3c1ae289757 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 05:03:47 -0300 Subject: [PATCH 096/125] fix grammar issues --- src/webpack/patchWebpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d3d722b7ab..bf4efa5f17 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -47,9 +47,9 @@ const define: Define = (target, p, attributes) => { // because Discord bundled code is chunked. // As of the time of writing, only the main and sentry Webpack instances have this property, and they are the only ones we care about. -// We use this setter to intercept when wreq.O is defined, and apply the patching in the modules factories (wreq.m). +// We use this setter to intercept when wreq.O is defined, so we can patch the modules factories (wreq.m). // wreq.m 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 wreq.m, so this also patches the sentry module factories. +// The sentry module also has their own Webpack with a pre-populated wreq.m, so this also patches those. // We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. // If this is the main Webpack, we also set up the internal references to WebpackRequire. @@ -83,7 +83,7 @@ define(Function.prototype, "O", { } }); // setImmediate to clear this property setter if this is not the main Webpack. - // If this is the main Webpack, wreq.m will always be set before the timeout runs. + // If this is the main Webpack, wreq.p will always be set before the timeout runs. const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); // Patch the pre-populated factories From 78fd37a4c682ccc54085e5eb91f51b03a6b952ef Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 1 Jun 2024 19:13:27 +0200 Subject: [PATCH 097/125] fix(css): brand-experiment is now brand-500 --- src/api/Notifications/NotificationComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx index caa4b64efe..d07143c450 100644 --- a/src/api/Notifications/NotificationComponent.tsx +++ b/src/api/Notifications/NotificationComponent.tsx @@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({ {timeout !== 0 && !permanent && (
)} From c4c92ed366b6fb0e320c1d1eafe326e54b7fcf66 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 17:51:50 -0300 Subject: [PATCH 098/125] Add shortcut for lazy loading chunks --- scripts/generateReport.ts | 11 +- src/debug/loadLazyChunks.ts | 167 ++++++++++++++++++++++++++ src/debug/runReporter.ts | 164 ++----------------------- src/plugins/consoleShortcuts/index.ts | 2 + src/plugins/partyMode/index.ts | 3 +- src/utils/Logger.ts | 2 +- 6 files changed, 189 insertions(+), 160 deletions(-) create mode 100644 src/debug/loadLazyChunks.ts diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0fde486375..d5f9a02531 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -242,16 +242,23 @@ page.on("console", async e => { }); break; + case "LazyChunkLoader:": + console.error(await getText()); + + switch (message) { + case "A fatal error occurred:": + process.exit(1); + } case "Reporter:": console.error(await getText()); switch (message) { + case "A fatal error occurred:": + process.exit(1); case "Webpack Find Fail:": process.exitCode = 1; report.badWebpackFinds.push(otherMessage); break; - case "A fatal error occurred:": - process.exit(1); case "Finished test": await browser.close(); await printReport(); diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts new file mode 100644 index 0000000000..d8f84335c3 --- /dev/null +++ b/src/debug/loadLazyChunks.ts @@ -0,0 +1,167 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { canonicalizeMatch } from "@utils/patches"; +import * as Webpack from "@webpack"; +import { wreq } from "@webpack"; + +const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); + +export async function loadLazyChunks() { + try { + LazyChunkLoaderLogger.log("Loading all chunks..."); + + const validChunks = new Set(); + const invalidChunks = new Set(); + const deferredRequires = new Set(); + + let chunksSearchingResolve: (value: void | PromiseLike) => void; + const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); + + // True if resolved, false otherwise + const chunksSearchPromises = [] as Array<() => boolean>; + + const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); + + async function searchAndLoadLazyChunks(factoryCode: string) { + const lazyChunks = factoryCode.matchAll(LazyChunkRegex); + const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); + + // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before + // the chunk containing the component + const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); + + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { + const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; + + if (chunkIds.length === 0) { + return; + } + + let invalidChunkGroup = false; + + for (const id of chunkIds) { + if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + if (isWasm && IS_WEB) { + invalidChunks.add(id); + invalidChunkGroup = true; + continue; + } + + validChunks.add(id); + } + + if (!invalidChunkGroup) { + validChunkGroups.add([chunkIds, entryPoint]); + } + })); + + // Loads all found valid chunk groups + await Promise.all( + Array.from(validChunkGroups) + .map(([chunkIds]) => + Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + ) + ); + + // Requires the entry points for all valid chunk groups + for (const [, entryPoint] of validChunkGroups) { + try { + if (shouldForceDefer) { + deferredRequires.add(entryPoint); + continue; + } + + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + // setImmediate to only check if all chunks were loaded after this function resolves + // We check if all chunks were loaded every time a factory is loaded + // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved + // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + setTimeout(() => { + let allResolved = true; + + for (let i = 0; i < chunksSearchPromises.length; i++) { + const isResolved = chunksSearchPromises[i](); + + if (isResolved) { + // Remove finished promises to avoid having to iterate through a huge array everytime + chunksSearchPromises.splice(i--, 1); + } else { + allResolved = false; + } + } + + if (allResolved) chunksSearchingResolve(); + }, 0); + } + + Webpack.factoryListeners.add(factory => { + let isResolved = false; + searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + }); + + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + + await chunksSearchingDone; + + // Require deferred entry points + for (const deferredRequire of deferredRequires) { + wreq!(deferredRequire as any); + } + + // All chunks Discord has mapped to asset files, even if they are not used anymore + const allChunks = [] as string[]; + + // Matches "id" or id: + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + const id = currentMatch[1] ?? currentMatch[2]; + if (id == null) continue; + + allChunks.push(id); + } + + if (allChunks.length === 0) throw new Error("Failed to get all chunks"); + + // Chunks that are not loaded (not used) by Discord code anymore + const chunksLeft = allChunks.filter(id => { + return !(validChunks.has(id) || invalidChunks.has(id)); + }); + + await Promise.all(chunksLeft.map(async id => { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + // Loads and requires a chunk + if (!isWasm) { + await wreq.e(id as any); + if (wreq.m[id]) wreq(id as any); + } + })); + + LazyChunkLoaderLogger.log("Finished loading all chunks!"); + } catch (e) { + LazyChunkLoaderLogger.log("A fatal error occurred:", e); + } +} diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 61c9f162b9..95cda51892 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -5,171 +5,23 @@ */ import { Logger } from "@utils/Logger"; -import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; -import { wreq } from "@webpack"; import { patches } from "plugins"; +import { loadLazyChunks } from "./loadLazyChunks"; + + const ReporterLogger = new Logger("Reporter"); async function runReporter() { - ReporterLogger.log("Starting test..."); - try { - const validChunks = new Set(); - const invalidChunks = new Set(); - const deferredRequires = new Set(); - - let chunksSearchingResolve: (value: void | PromiseLike) => void; - const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); - - // True if resolved, false otherwise - const chunksSearchPromises = [] as Array<() => boolean>; - - const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); - - async function searchAndLoadLazyChunks(factoryCode: string) { - const lazyChunks = factoryCode.matchAll(LazyChunkRegex); - const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); - - // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before - // the chunk containing the component - const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); - - await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { - const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; - - if (chunkIds.length === 0) { - return; - } - - let invalidChunkGroup = false; - - for (const id of chunkIds) { - if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; - - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - if (isWasm && IS_WEB) { - invalidChunks.add(id); - invalidChunkGroup = true; - continue; - } - - validChunks.add(id); - } - - if (!invalidChunkGroup) { - validChunkGroups.add([chunkIds, entryPoint]); - } - })); - - // Loads all found valid chunk groups - await Promise.all( - Array.from(validChunkGroups) - .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) - ) - ); - - // Requires the entry points for all valid chunk groups - for (const [, entryPoint] of validChunkGroups) { - try { - if (shouldForceDefer) { - deferredRequires.add(entryPoint); - continue; - } - - if (wreq.m[entryPoint]) wreq(entryPoint as any); - } catch (err) { - console.error(err); - } - } + ReporterLogger.log("Starting test..."); - // setImmediate to only check if all chunks were loaded after this function resolves - // We check if all chunks were loaded every time a factory is loaded - // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved - // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them - setTimeout(() => { - let allResolved = true; - - for (let i = 0; i < chunksSearchPromises.length; i++) { - const isResolved = chunksSearchPromises[i](); - - if (isResolved) { - // Remove finished promises to avoid having to iterate through a huge array everytime - chunksSearchPromises.splice(i--, 1); - } else { - allResolved = false; - } - } - - if (allResolved) chunksSearchingResolve(); - }, 0); - } - - Webpack.beforeInitListeners.add(async () => { - ReporterLogger.log("Loading all chunks..."); - - Webpack.factoryListeners.add(factory => { - let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - }); - - // setImmediate to only search the initial factories after Discord initialized the app - // our beforeInitListeners are called before Discord initializes the app - setTimeout(() => { - for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - } - }, 0); - }); - - await chunksSearchingDone; - - // Require deferred entry points - for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); - } - - // All chunks Discord has mapped to asset files, even if they are not used anymore - const allChunks = [] as string[]; - - // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { - const id = currentMatch[1] ?? currentMatch[2]; - if (id == null) continue; - - allChunks.push(id); - } - - if (allChunks.length === 0) throw new Error("Failed to get all chunks"); - - // Chunks that are not loaded (not used) by Discord code anymore - const chunksLeft = allChunks.filter(id => { - return !(validChunks.has(id) || invalidChunks.has(id)); - }); - - await Promise.all(chunksLeft.map(async id => { - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - // Loads and requires a chunk - if (!isWasm) { - await wreq.e(id as any); - if (wreq.m[id]) wreq(id as any); - } - })); + let loadLazyChunksResolve: (value: void | PromiseLike) => void; + const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r); - ReporterLogger.log("Finished loading all chunks!"); + Webpack.beforeInitListeners.add(() => loadLazyChunks().then(() => loadLazyChunksResolve())); + await loadLazyChunksDone; for (const patch of patches) { if (!patch.all) { diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index ee86b5fcfd..0a1323e755 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack"; import * as Common from "@webpack/common"; +import { loadLazyChunks } from "debug/loadLazyChunks"; import type { ComponentType } from "react"; const DESKTOP_ONLY = (f: string) => () => { @@ -82,6 +83,7 @@ function makeShortcuts() { wpsearch: search, wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), + loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); }, find, findAll: findAll, findByProps, diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts index 56c19c02c6..c40f2e3c75 100644 --- a/src/plugins/partyMode/index.ts +++ b/src/plugins/partyMode/index.ts @@ -18,7 +18,7 @@ import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; const enum Intensity { @@ -46,6 +46,7 @@ export default definePlugin({ name: "PartyMode", description: "Allows you to use party mode cause the party never ends ✨", authors: [Devs.UwUDev], + reporterTestable: ReporterTestable.None, settings, start() { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 5296184d4b..22a3813600 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -32,7 +32,7 @@ export class Logger { constructor(public name: string, public color: string = "white") { } private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { - if (IS_REPORTER) { + if (IS_REPORTER && IS_WEB) { console[level]("[Vencord]", this.name + ":", ...args); return; } From c50208b0193c072959e7f59dfd990c6871011f62 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 18:10:45 -0300 Subject: [PATCH 099/125] Add shortcut for lazy loading chunks --- scripts/generateReport.ts | 11 +- src/api/Settings.ts | 2 +- src/debug/loadLazyChunks.ts | 167 ++++++++++++++++++++++++++ src/debug/runReporter.ts | 163 ++----------------------- src/plugins/consoleShortcuts/index.ts | 2 + src/plugins/index.ts | 1 - src/plugins/partyMode/index.ts | 3 +- src/utils/Logger.ts | 2 +- 8 files changed, 189 insertions(+), 162 deletions(-) create mode 100644 src/debug/loadLazyChunks.ts diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0fde486375..d5f9a02531 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -242,16 +242,23 @@ page.on("console", async e => { }); break; + case "LazyChunkLoader:": + console.error(await getText()); + + switch (message) { + case "A fatal error occurred:": + process.exit(1); + } case "Reporter:": console.error(await getText()); switch (message) { + case "A fatal error occurred:": + process.exit(1); case "Webpack Find Fail:": process.exitCode = 1; report.badWebpackFinds.push(otherMessage); break; - case "A fatal error occurred:": - process.exit(1); case "Finished test": await browser.close(); await printReport(); diff --git a/src/api/Settings.ts b/src/api/Settings.ts index b94e6a3fd9..70ba0bd4a0 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, { if (path === "plugins" && key in plugins) return target[key] = { - enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false + enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false }; // Since the property is not set, check if this is a plugin's setting and if so, try to resolve diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts new file mode 100644 index 0000000000..d8f84335c3 --- /dev/null +++ b/src/debug/loadLazyChunks.ts @@ -0,0 +1,167 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { canonicalizeMatch } from "@utils/patches"; +import * as Webpack from "@webpack"; +import { wreq } from "@webpack"; + +const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); + +export async function loadLazyChunks() { + try { + LazyChunkLoaderLogger.log("Loading all chunks..."); + + const validChunks = new Set(); + const invalidChunks = new Set(); + const deferredRequires = new Set(); + + let chunksSearchingResolve: (value: void | PromiseLike) => void; + const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); + + // True if resolved, false otherwise + const chunksSearchPromises = [] as Array<() => boolean>; + + const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); + + async function searchAndLoadLazyChunks(factoryCode: string) { + const lazyChunks = factoryCode.matchAll(LazyChunkRegex); + const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); + + // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before + // the chunk containing the component + const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); + + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { + const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; + + if (chunkIds.length === 0) { + return; + } + + let invalidChunkGroup = false; + + for (const id of chunkIds) { + if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + if (isWasm && IS_WEB) { + invalidChunks.add(id); + invalidChunkGroup = true; + continue; + } + + validChunks.add(id); + } + + if (!invalidChunkGroup) { + validChunkGroups.add([chunkIds, entryPoint]); + } + })); + + // Loads all found valid chunk groups + await Promise.all( + Array.from(validChunkGroups) + .map(([chunkIds]) => + Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + ) + ); + + // Requires the entry points for all valid chunk groups + for (const [, entryPoint] of validChunkGroups) { + try { + if (shouldForceDefer) { + deferredRequires.add(entryPoint); + continue; + } + + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + // setImmediate to only check if all chunks were loaded after this function resolves + // We check if all chunks were loaded every time a factory is loaded + // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved + // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + setTimeout(() => { + let allResolved = true; + + for (let i = 0; i < chunksSearchPromises.length; i++) { + const isResolved = chunksSearchPromises[i](); + + if (isResolved) { + // Remove finished promises to avoid having to iterate through a huge array everytime + chunksSearchPromises.splice(i--, 1); + } else { + allResolved = false; + } + } + + if (allResolved) chunksSearchingResolve(); + }, 0); + } + + Webpack.factoryListeners.add(factory => { + let isResolved = false; + searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + }); + + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + + await chunksSearchingDone; + + // Require deferred entry points + for (const deferredRequire of deferredRequires) { + wreq!(deferredRequire as any); + } + + // All chunks Discord has mapped to asset files, even if they are not used anymore + const allChunks = [] as string[]; + + // Matches "id" or id: + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + const id = currentMatch[1] ?? currentMatch[2]; + if (id == null) continue; + + allChunks.push(id); + } + + if (allChunks.length === 0) throw new Error("Failed to get all chunks"); + + // Chunks that are not loaded (not used) by Discord code anymore + const chunksLeft = allChunks.filter(id => { + return !(validChunks.has(id) || invalidChunks.has(id)); + }); + + await Promise.all(chunksLeft.map(async id => { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + // Loads and requires a chunk + if (!isWasm) { + await wreq.e(id as any); + if (wreq.m[id]) wreq(id as any); + } + })); + + LazyChunkLoaderLogger.log("Finished loading all chunks!"); + } catch (e) { + LazyChunkLoaderLogger.log("A fatal error occurred:", e); + } +} diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 61c9f162b9..6c7a2a03f2 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -5,171 +5,22 @@ */ import { Logger } from "@utils/Logger"; -import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; -import { wreq } from "@webpack"; import { patches } from "plugins"; +import { loadLazyChunks } from "./loadLazyChunks"; + const ReporterLogger = new Logger("Reporter"); async function runReporter() { - ReporterLogger.log("Starting test..."); - try { - const validChunks = new Set(); - const invalidChunks = new Set(); - const deferredRequires = new Set(); - - let chunksSearchingResolve: (value: void | PromiseLike) => void; - const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); - - // True if resolved, false otherwise - const chunksSearchPromises = [] as Array<() => boolean>; - - const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); - - async function searchAndLoadLazyChunks(factoryCode: string) { - const lazyChunks = factoryCode.matchAll(LazyChunkRegex); - const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); - - // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before - // the chunk containing the component - const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); - - await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { - const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; - - if (chunkIds.length === 0) { - return; - } - - let invalidChunkGroup = false; + ReporterLogger.log("Starting test..."); - for (const id of chunkIds) { - if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; - - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - if (isWasm && IS_WEB) { - invalidChunks.add(id); - invalidChunkGroup = true; - continue; - } - - validChunks.add(id); - } - - if (!invalidChunkGroup) { - validChunkGroups.add([chunkIds, entryPoint]); - } - })); - - // Loads all found valid chunk groups - await Promise.all( - Array.from(validChunkGroups) - .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) - ) - ); - - // Requires the entry points for all valid chunk groups - for (const [, entryPoint] of validChunkGroups) { - try { - if (shouldForceDefer) { - deferredRequires.add(entryPoint); - continue; - } - - if (wreq.m[entryPoint]) wreq(entryPoint as any); - } catch (err) { - console.error(err); - } - } - - // setImmediate to only check if all chunks were loaded after this function resolves - // We check if all chunks were loaded every time a factory is loaded - // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved - // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them - setTimeout(() => { - let allResolved = true; - - for (let i = 0; i < chunksSearchPromises.length; i++) { - const isResolved = chunksSearchPromises[i](); - - if (isResolved) { - // Remove finished promises to avoid having to iterate through a huge array everytime - chunksSearchPromises.splice(i--, 1); - } else { - allResolved = false; - } - } - - if (allResolved) chunksSearchingResolve(); - }, 0); - } - - Webpack.beforeInitListeners.add(async () => { - ReporterLogger.log("Loading all chunks..."); - - Webpack.factoryListeners.add(factory => { - let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - }); - - // setImmediate to only search the initial factories after Discord initialized the app - // our beforeInitListeners are called before Discord initializes the app - setTimeout(() => { - for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - } - }, 0); - }); - - await chunksSearchingDone; - - // Require deferred entry points - for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); - } - - // All chunks Discord has mapped to asset files, even if they are not used anymore - const allChunks = [] as string[]; - - // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { - const id = currentMatch[1] ?? currentMatch[2]; - if (id == null) continue; - - allChunks.push(id); - } - - if (allChunks.length === 0) throw new Error("Failed to get all chunks"); - - // Chunks that are not loaded (not used) by Discord code anymore - const chunksLeft = allChunks.filter(id => { - return !(validChunks.has(id) || invalidChunks.has(id)); - }); - - await Promise.all(chunksLeft.map(async id => { - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - // Loads and requires a chunk - if (!isWasm) { - await wreq.e(id as any); - if (wreq.m[id]) wreq(id as any); - } - })); + let loadLazyChunksResolve: (value: void | PromiseLike) => void; + const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r); - ReporterLogger.log("Finished loading all chunks!"); + Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve))); + await loadLazyChunksDone; for (const patch of patches) { if (!patch.all) { diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index ee86b5fcfd..0a1323e755 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack"; import * as Common from "@webpack/common"; +import { loadLazyChunks } from "debug/loadLazyChunks"; import type { ComponentType } from "react"; const DESKTOP_ONLY = (f: string) => () => { @@ -82,6 +83,7 @@ function makeShortcuts() { wpsearch: search, wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), + loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); }, find, findAll: findAll, findByProps, diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 53ab7983a5..32bfe7e978 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -44,7 +44,6 @@ const settings = Settings.plugins; export function isPluginEnabled(p: string) { return ( - IS_REPORTER || Plugins[p]?.required || Plugins[p]?.isDependency || settings[p]?.enabled diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts index 56c19c02c6..c40f2e3c75 100644 --- a/src/plugins/partyMode/index.ts +++ b/src/plugins/partyMode/index.ts @@ -18,7 +18,7 @@ import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; const enum Intensity { @@ -46,6 +46,7 @@ export default definePlugin({ name: "PartyMode", description: "Allows you to use party mode cause the party never ends ✨", authors: [Devs.UwUDev], + reporterTestable: ReporterTestable.None, settings, start() { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 5296184d4b..22a3813600 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -32,7 +32,7 @@ export class Logger { constructor(public name: string, public color: string = "white") { } private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { - if (IS_REPORTER) { + if (IS_REPORTER && IS_WEB) { console[level]("[Vencord]", this.name + ":", ...args); return; } From d8524b087c850443cee954d8582f18de98a6806e Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 18:39:01 -0300 Subject: [PATCH 100/125] Add shortcut for lazy loading chunks --- scripts/generateReport.ts | 13 +- src/api/Settings.ts | 2 +- src/debug/loadLazyChunks.ts | 167 ++++++++++++++++++++++++++ src/debug/runReporter.ts | 163 ++----------------------- src/plugins/consoleShortcuts/index.ts | 2 + src/plugins/index.ts | 1 - src/plugins/partyMode/index.ts | 3 +- src/utils/Logger.ts | 2 +- 8 files changed, 191 insertions(+), 162 deletions(-) create mode 100644 src/debug/loadLazyChunks.ts diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 0fde486375..cf42107792 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -241,17 +241,26 @@ page.on("console", async e => { error: await maybeGetError(e.args()[3]) ?? "Unknown error" }); + break; + case "LazyChunkLoader:": + console.error(await getText()); + + switch (message) { + case "A fatal error occurred:": + process.exit(1); + } + break; case "Reporter:": console.error(await getText()); switch (message) { + case "A fatal error occurred:": + process.exit(1); case "Webpack Find Fail:": process.exitCode = 1; report.badWebpackFinds.push(otherMessage); break; - case "A fatal error occurred:": - process.exit(1); case "Finished test": await browser.close(); await printReport(); diff --git a/src/api/Settings.ts b/src/api/Settings.ts index b94e6a3fd9..70ba0bd4a0 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, { if (path === "plugins" && key in plugins) return target[key] = { - enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false + enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false }; // Since the property is not set, check if this is a plugin's setting and if so, try to resolve diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts new file mode 100644 index 0000000000..d8f84335c3 --- /dev/null +++ b/src/debug/loadLazyChunks.ts @@ -0,0 +1,167 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@utils/Logger"; +import { canonicalizeMatch } from "@utils/patches"; +import * as Webpack from "@webpack"; +import { wreq } from "@webpack"; + +const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); + +export async function loadLazyChunks() { + try { + LazyChunkLoaderLogger.log("Loading all chunks..."); + + const validChunks = new Set(); + const invalidChunks = new Set(); + const deferredRequires = new Set(); + + let chunksSearchingResolve: (value: void | PromiseLike) => void; + const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); + + // True if resolved, false otherwise + const chunksSearchPromises = [] as Array<() => boolean>; + + const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); + + async function searchAndLoadLazyChunks(factoryCode: string) { + const lazyChunks = factoryCode.matchAll(LazyChunkRegex); + const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); + + // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before + // the chunk containing the component + const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); + + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { + const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; + + if (chunkIds.length === 0) { + return; + } + + let invalidChunkGroup = false; + + for (const id of chunkIds) { + if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; + + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + if (isWasm && IS_WEB) { + invalidChunks.add(id); + invalidChunkGroup = true; + continue; + } + + validChunks.add(id); + } + + if (!invalidChunkGroup) { + validChunkGroups.add([chunkIds, entryPoint]); + } + })); + + // Loads all found valid chunk groups + await Promise.all( + Array.from(validChunkGroups) + .map(([chunkIds]) => + Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + ) + ); + + // Requires the entry points for all valid chunk groups + for (const [, entryPoint] of validChunkGroups) { + try { + if (shouldForceDefer) { + deferredRequires.add(entryPoint); + continue; + } + + if (wreq.m[entryPoint]) wreq(entryPoint as any); + } catch (err) { + console.error(err); + } + } + + // setImmediate to only check if all chunks were loaded after this function resolves + // We check if all chunks were loaded every time a factory is loaded + // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved + // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + setTimeout(() => { + let allResolved = true; + + for (let i = 0; i < chunksSearchPromises.length; i++) { + const isResolved = chunksSearchPromises[i](); + + if (isResolved) { + // Remove finished promises to avoid having to iterate through a huge array everytime + chunksSearchPromises.splice(i--, 1); + } else { + allResolved = false; + } + } + + if (allResolved) chunksSearchingResolve(); + }, 0); + } + + Webpack.factoryListeners.add(factory => { + let isResolved = false; + searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + }); + + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + + await chunksSearchingDone; + + // Require deferred entry points + for (const deferredRequire of deferredRequires) { + wreq!(deferredRequire as any); + } + + // All chunks Discord has mapped to asset files, even if they are not used anymore + const allChunks = [] as string[]; + + // Matches "id" or id: + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + const id = currentMatch[1] ?? currentMatch[2]; + if (id == null) continue; + + allChunks.push(id); + } + + if (allChunks.length === 0) throw new Error("Failed to get all chunks"); + + // Chunks that are not loaded (not used) by Discord code anymore + const chunksLeft = allChunks.filter(id => { + return !(validChunks.has(id) || invalidChunks.has(id)); + }); + + await Promise.all(chunksLeft.map(async id => { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + // Loads and requires a chunk + if (!isWasm) { + await wreq.e(id as any); + if (wreq.m[id]) wreq(id as any); + } + })); + + LazyChunkLoaderLogger.log("Finished loading all chunks!"); + } catch (e) { + LazyChunkLoaderLogger.log("A fatal error occurred:", e); + } +} diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 61c9f162b9..6c7a2a03f2 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -5,171 +5,22 @@ */ import { Logger } from "@utils/Logger"; -import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; -import { wreq } from "@webpack"; import { patches } from "plugins"; +import { loadLazyChunks } from "./loadLazyChunks"; + const ReporterLogger = new Logger("Reporter"); async function runReporter() { - ReporterLogger.log("Starting test..."); - try { - const validChunks = new Set(); - const invalidChunks = new Set(); - const deferredRequires = new Set(); - - let chunksSearchingResolve: (value: void | PromiseLike) => void; - const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); - - // True if resolved, false otherwise - const chunksSearchPromises = [] as Array<() => boolean>; - - const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g); - - async function searchAndLoadLazyChunks(factoryCode: string) { - const lazyChunks = factoryCode.matchAll(LazyChunkRegex); - const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); - - // Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before - // the chunk containing the component - const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT"); - - await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { - const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : []; - - if (chunkIds.length === 0) { - return; - } - - let invalidChunkGroup = false; + ReporterLogger.log("Starting test..."); - for (const id of chunkIds) { - if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; - - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - if (isWasm && IS_WEB) { - invalidChunks.add(id); - invalidChunkGroup = true; - continue; - } - - validChunks.add(id); - } - - if (!invalidChunkGroup) { - validChunkGroups.add([chunkIds, entryPoint]); - } - })); - - // Loads all found valid chunk groups - await Promise.all( - Array.from(validChunkGroups) - .map(([chunkIds]) => - Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) - ) - ); - - // Requires the entry points for all valid chunk groups - for (const [, entryPoint] of validChunkGroups) { - try { - if (shouldForceDefer) { - deferredRequires.add(entryPoint); - continue; - } - - if (wreq.m[entryPoint]) wreq(entryPoint as any); - } catch (err) { - console.error(err); - } - } - - // setImmediate to only check if all chunks were loaded after this function resolves - // We check if all chunks were loaded every time a factory is loaded - // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved - // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them - setTimeout(() => { - let allResolved = true; - - for (let i = 0; i < chunksSearchPromises.length; i++) { - const isResolved = chunksSearchPromises[i](); - - if (isResolved) { - // Remove finished promises to avoid having to iterate through a huge array everytime - chunksSearchPromises.splice(i--, 1); - } else { - allResolved = false; - } - } - - if (allResolved) chunksSearchingResolve(); - }, 0); - } - - Webpack.beforeInitListeners.add(async () => { - ReporterLogger.log("Loading all chunks..."); - - Webpack.factoryListeners.add(factory => { - let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - }); - - // setImmediate to only search the initial factories after Discord initialized the app - // our beforeInitListeners are called before Discord initializes the app - setTimeout(() => { - for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); - } - }, 0); - }); - - await chunksSearchingDone; - - // Require deferred entry points - for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); - } - - // All chunks Discord has mapped to asset files, even if they are not used anymore - const allChunks = [] as string[]; - - // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { - const id = currentMatch[1] ?? currentMatch[2]; - if (id == null) continue; - - allChunks.push(id); - } - - if (allChunks.length === 0) throw new Error("Failed to get all chunks"); - - // Chunks that are not loaded (not used) by Discord code anymore - const chunksLeft = allChunks.filter(id => { - return !(validChunks.has(id) || invalidChunks.has(id)); - }); - - await Promise.all(chunksLeft.map(async id => { - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - // Loads and requires a chunk - if (!isWasm) { - await wreq.e(id as any); - if (wreq.m[id]) wreq(id as any); - } - })); + let loadLazyChunksResolve: (value: void | PromiseLike) => void; + const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r); - ReporterLogger.log("Finished loading all chunks!"); + Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve))); + await loadLazyChunksDone; for (const patch of patches) { if (!patch.all) { diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index ee86b5fcfd..0a1323e755 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, findModuleId, search } from "@webpack"; import * as Common from "@webpack/common"; +import { loadLazyChunks } from "debug/loadLazyChunks"; import type { ComponentType } from "react"; const DESKTOP_ONLY = (f: string) => () => { @@ -82,6 +83,7 @@ function makeShortcuts() { wpsearch: search, wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), + loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); }, find, findAll: findAll, findByProps, diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 53ab7983a5..32bfe7e978 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -44,7 +44,6 @@ const settings = Settings.plugins; export function isPluginEnabled(p: string) { return ( - IS_REPORTER || Plugins[p]?.required || Plugins[p]?.isDependency || settings[p]?.enabled diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts index 56c19c02c6..c40f2e3c75 100644 --- a/src/plugins/partyMode/index.ts +++ b/src/plugins/partyMode/index.ts @@ -18,7 +18,7 @@ import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; import { FluxDispatcher } from "@webpack/common"; const enum Intensity { @@ -46,6 +46,7 @@ export default definePlugin({ name: "PartyMode", description: "Allows you to use party mode cause the party never ends ✨", authors: [Devs.UwUDev], + reporterTestable: ReporterTestable.None, settings, start() { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 5296184d4b..22a3813600 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -32,7 +32,7 @@ export class Logger { constructor(public name: string, public color: string = "white") { } private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") { - if (IS_REPORTER) { + if (IS_REPORTER && IS_WEB) { console[level]("[Vencord]", this.name + ":", ...args); return; } From 61e1eada0174b18476d371ba5028dff199301555 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:38:01 -0300 Subject: [PATCH 101/125] Fix not restoring some original factories --- src/debug/loadLazyChunks.ts | 4 +-- src/webpack/patchWebpack.ts | 49 +++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index 91946c3731..f4f7f88b3d 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -111,14 +111,14 @@ export async function loadLazyChunks() { Webpack.factoryListeners.add(factory => { let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); chunksSearchPromises.push(() => isResolved); }); for (const factoryId in wreq.m) { let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); chunksSearchPromises.push(() => isResolved); } diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index bf4efa5f17..c147d1ad05 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -88,6 +88,10 @@ define(Function.prototype, "O", { // Patch the pre-populated factories for (const id in this.m) { + if (updateExistingFactory(this.m, id, this.m[id])) { + continue; + } + defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, this.m[id]) : this.m[id]); } @@ -140,6 +144,35 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto } } +/** + * 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 id The id of the module + * @param newFactory The new original factory + * @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance + */ +function updateExistingFactory(target: AnyWebpackRequire["m"], id: PropertyKey, newFactory: ModuleFactory) { + let existingFactory: TypedPropertyDescriptor | undefined; + for (const wreq of allWebpackInstances) { + if (Reflect.getOwnPropertyDescriptor(wreq.m, id) != null) { + existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); + break; + } + } + + if (existingFactory != null) { + // If existingFactory exists in any Webpack instance, its either wrapped in defineModuleFactoryGetter, or it has already been required. + // So define the descriptor of it on this current Webpack instance, call Reflect.set with the new original, + // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) + + Reflect.defineProperty(target, id, existingFactory); + return Reflect.set(target, id, newFactory, target); + } + + return false; +} + const moduleFactoriesHandler: ProxyHandler = { /* If Discord 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 @@ -163,26 +196,16 @@ const moduleFactoriesHandler: ProxyHandler = { return define(target, p, { value: newValue }); } - const existingFactory = Reflect.get(target, p, receiver); + if (updateExistingFactory(target, p, newValue)) { + return true; + } if (!Settings.eagerPatches) { - // If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required - // so call Reflect.set with the new original and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) - if (existingFactory != null) { - return Reflect.set(target, p, newValue, receiver); - } - // eagerPatches are disabled, so the factory argument should be the original defineModulesFactoryGetter(p, newValue); return true; } - // Check if this factory is already patched - if (existingFactory?.$$vencordOriginal != null) { - existingFactory.$$vencordOriginal = newValue; - return true; - } - const patchedFactory = patchFactory(p, newValue); // If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects. From 4e14232b5aab7a1b5ed84c1ce8ea74ae1c8643e1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:52:17 -0300 Subject: [PATCH 102/125] Fix patching pre-populated factories --- src/webpack/patchWebpack.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index c147d1ad05..a17a2e5dd0 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -88,7 +88,7 @@ define(Function.prototype, "O", { // Patch the pre-populated factories for (const id in this.m) { - if (updateExistingFactory(this.m, id, this.m[id])) { + if (updateExistingFactory(this.m, id, this.m[id], true)) { continue; } @@ -150,11 +150,14 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto * @target The module factories where this new original factory is being set * @param id The id of the module * @param newFactory The new original factory + * @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 */ -function updateExistingFactory(target: AnyWebpackRequire["m"], id: PropertyKey, newFactory: ModuleFactory) { +function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: ModuleFactory, ignoreExistingInTarget: boolean = false) { let existingFactory: TypedPropertyDescriptor | undefined; for (const wreq of allWebpackInstances) { + if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; + if (Reflect.getOwnPropertyDescriptor(wreq.m, id) != null) { existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); break; @@ -166,8 +169,8 @@ function updateExistingFactory(target: AnyWebpackRequire["m"], id: PropertyKey, // So define the descriptor of it on this current Webpack instance, call Reflect.set with the new original, // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) - Reflect.defineProperty(target, id, existingFactory); - return Reflect.set(target, id, newFactory, target); + Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory); + return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget); } return false; From 6e6ee4db689808c380662de17f5ae9ed0d28036e Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 1 Jun 2024 23:39:58 -0300 Subject: [PATCH 103/125] NoPendingCount: Fix for message requests --- src/plugins/noPendingCount/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/plugins/noPendingCount/index.ts b/src/plugins/noPendingCount/index.ts index 29458df9d6..57a65f52cc 100644 --- a/src/plugins/noPendingCount/index.ts +++ b/src/plugins/noPendingCount/index.ts @@ -62,6 +62,16 @@ export default definePlugin({ replace: "return 0;" } }, + // New message requests hook + { + find: "useNewMessageRequestsCount:", + predicate: () => settings.store.hideMessageRequestsCount, + replacement: { + match: /getNonChannelAckId\(\i\.\i\.MESSAGE_REQUESTS\).+?return /, + replace: "$&0;" + } + }, + // Old message requests hook { find: "getMessageRequestsCount(){", predicate: () => settings.store.hideMessageRequestsCount, From 53dd86fa6e8cd8ad8832bf1fd53397a8527289a0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:46:03 -0300 Subject: [PATCH 104/125] Improve typings and other stuff --- src/debug/loadLazyChunks.ts | 16 +++++++++------- src/webpack/patchWebpack.ts | 24 ++++++++---------------- src/webpack/webpack.ts | 4 ++-- src/webpack/wreq.d.ts | 18 +++++++++++++++++- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index f4f7f88b3d..4aea9ff5d8 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -8,6 +8,7 @@ import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; import { wreq } from "@webpack"; +import { AnyModuleFactory, ModuleFactory } from "webpack"; const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); @@ -109,21 +110,22 @@ export async function loadLazyChunks() { }, 0); } - Webpack.factoryListeners.add(factory => { + function factoryListener(factory: AnyModuleFactory | ModuleFactory) { let isResolved = false; - searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true); + searchAndLoadLazyChunks(String(factory)) + .then(() => isResolved = true) + .catch(() => isResolved = true); chunksSearchPromises.push(() => isResolved); - }); + } + Webpack.factoryListeners.add(factoryListener); for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); + factoryListener(wreq.m[factoryId]); } await chunksSearchingDone; + Webpack.factoryListeners.delete(factoryListener); // Require deferred entry points for (const deferredRequire of deferredRequires) { diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index a17a2e5dd0..7ac7cac001 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -12,15 +12,7 @@ import { PatchReplacement } from "@utils/types"; import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; -import { _initWebpack, factoryListeners, ModuleFactory, moduleListeners, subscriptions, WebpackRequire, wreq } from "."; - -type AnyWebpackRequire = Partial & Pick; - -type PatchedModuleFactory = ModuleFactory & { - $$vencordOriginal?: ModuleFactory; -}; - -type PatchedModuleFactories = Record; +import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, PatchedModuleFactories, PatchedModuleFactory, subscriptions, WebpackRequire, wreq } from "."; const logger = new Logger("WebpackInterceptor", "#8caaee"); @@ -56,7 +48,7 @@ const define: Define = (target, p, attributes) => { define(Function.prototype, "O", { enumerable: false, - set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) { + set(this: AnyWebpackRequire, onChunksLoaded: AnyWebpackRequire["O"]) { define(this, "O", { value: onChunksLoaded }); const { stack } = new Error(); @@ -104,7 +96,7 @@ define(Function.prototype, "O", { const proxiedModuleFactories = new Proxy(this.m, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype - Reflect.setPrototypeOf(moduleFactories, new Proxy(moduleFactories, moduleFactoriesHandler)); + define(this, "m", { value: Reflect.setPrototypeOf(this.m, new Proxy(this.m, moduleFactoriesHandler)) }); */ define(this, "m", { value: proxiedModuleFactories }); @@ -133,7 +125,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto return (factory = patchFactory(id, factory)); }, - set(v: ModuleFactory) { + set(v: AnyModuleFactory) { if (factory.$$vencordOriginal != null) { factory.$$vencordOriginal = v; } else { @@ -153,7 +145,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto * @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 */ -function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: ModuleFactory, ignoreExistingInTarget: boolean = false) { +function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) { let existingFactory: TypedPropertyDescriptor | undefined; for (const wreq of allWebpackInstances) { if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; @@ -228,7 +220,7 @@ const moduleFactoriesHandler: ProxyHandler = { * @param factory The original or patched module factory * @returns The wrapper for the patched module factory */ -function patchFactory(id: PropertyKey, factory: ModuleFactory) { +function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { const originalFactory = factory; for (const factoryListener of factoryListeners) { @@ -347,7 +339,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { // The patched factory wrapper, define it in an object to preserve the name after minification const patchedFactory: PatchedModuleFactory = { - PatchedFactory(...args: Parameters) { + PatchedFactory(...args: Parameters) { // Restore the original factory in all the module factories objects, // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance for (const wreq of allWebpackInstances) { @@ -370,7 +362,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) { `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + ")" ); - _initWebpack(require); + _initWebpack(require as WebpackRequire); } else if (IS_DEV) { logger.error("WebpackRequire was not initialized, running modules without patches instead."); } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index f318558702..5d09c3b0f4 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -22,7 +22,7 @@ import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; import { traceFunction } from "../debug/Tracer"; -import { ModuleExports, ModuleFactory, WebpackRequire } from "./wreq"; +import { AnyModuleFactory, ModuleExports, ModuleFactory, WebpackRequire } from "./wreq"; const logger = new Logger("Webpack"); @@ -72,7 +72,7 @@ export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void; export const subscriptions = new Map(); export const moduleListeners = new Set(); -export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); +export const factoryListeners = new Set<(factory: AnyModuleFactory) => void>(); export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 6611680bdd..c27ad3094c 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -53,7 +53,7 @@ export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: Prop j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; }; -export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { +export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ m: Record; /** The module cache, where all modules which have been WebpackRequire'd are stored */ @@ -182,3 +182,19 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & { /** Document baseURI or WebWorker location.href */ b: string; }; + +// Utility section for Vencord + +export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial> & Pick & { + /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ + m: Record; +}; + +/** exports can be anything, however initially it is always an empty object */ +export type AnyModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void; + +export type PatchedModuleFactory = AnyModuleFactory & { + $$vencordOriginal?: AnyModuleFactory; +}; + +export type PatchedModuleFactories = Record; From c3bbc92fa2ee19080c8b52dec09e67b15837ab41 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:30:26 -0300 Subject: [PATCH 105/125] Decouple factoryListeners from patchFactory --- src/webpack/patchWebpack.ts | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 7ac7cac001..47373acc93 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -84,6 +84,7 @@ define(Function.prototype, "O", { continue; } + notifyFactoryListeners(this.m[id]); defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, this.m[id]) : this.m[id]); } @@ -168,6 +169,20 @@ function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id return false; } +/** + * Notify all factory listeners + * @param factory The original factory to notify for + */ +function notifyFactoryListeners(factory: AnyModuleFactory) { + for (const factoryListener of factoryListeners) { + try { + factoryListener(factory); + } catch (err) { + logger.error("Error in Webpack factory listener:\n", err, factoryListener); + } + } +} + const moduleFactoriesHandler: ProxyHandler = { /* If Discord 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 @@ -195,18 +210,8 @@ const moduleFactoriesHandler: ProxyHandler = { return true; } - if (!Settings.eagerPatches) { - // eagerPatches are disabled, so the factory argument should be the original - defineModulesFactoryGetter(p, newValue); - return true; - } - - const patchedFactory = patchFactory(p, newValue); - - // If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects. - // Because patches are only executed once, we need to set the patched version in all of them, to avoid the Webpack instance - // that uses the factory to contain the original factory instead of the patched, in case it was set first in another instance - defineModulesFactoryGetter(p, patchedFactory); + notifyFactoryListeners(newValue); + defineModulesFactoryGetter(p, Settings.eagerPatches ? patchFactory(p, newValue) : newValue); return true; } @@ -223,14 +228,6 @@ const moduleFactoriesHandler: ProxyHandler = { function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { const originalFactory = factory; - for (const factoryListener of factoryListeners) { - try { - factoryListener(originalFactory); - } catch (err) { - logger.error("Error in Webpack factory listener:\n", err, factoryListener); - } - } - const patchedBy = new Set(); // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, From 6826fe003cb6c68f1c683cbed5b10f6a5959ce66 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:31:05 -0300 Subject: [PATCH 106/125] guh --- src/webpack/patchWebpack.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 47373acc93..769c713f5c 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -170,7 +170,8 @@ function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id } /** - * Notify all factory listeners + * Notify all factory listeners. + * * @param factory The original factory to notify for */ function notifyFactoryListeners(factory: AnyModuleFactory) { From 853585c0bc76feeadae3b80de1a475e3ea833e66 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:33:26 -0300 Subject: [PATCH 107/125] move down --- src/webpack/patchWebpack.ts | 68 ++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 769c713f5c..78ef03595a 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -104,6 +104,40 @@ define(Function.prototype, "O", { } }); +const moduleFactoriesHandler: ProxyHandler = { + /* + If Discord 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 + and that requires defining additional traps for keeping the object working + + // Proxies on the prototype dont intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined, + // to avoid Reflect.get having no effect and causing a stack overflow + get: (target, p, receiver) => { + return undefined; + }, + // Same thing as get + has: (target, p) => { + return false; + } + */ + + // The set trap for patching or defining getters for the module factories when new module factories are loaded + set: (target, p, newValue, receiver) => { + // If the property is not a number, we are not dealing with a module factory + if (Number.isNaN(Number(p))) { + return define(target, p, { value: newValue }); + } + + if (updateExistingFactory(target, p, newValue)) { + return true; + } + + notifyFactoryListeners(newValue); + defineModulesFactoryGetter(p, Settings.eagerPatches ? patchFactory(p, newValue) : newValue); + + return true; + } +}; + /** * Define the getter for returning the patched version of the module factory. * @@ -184,40 +218,6 @@ function notifyFactoryListeners(factory: AnyModuleFactory) { } } -const moduleFactoriesHandler: ProxyHandler = { - /* - If Discord 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 - and that requires defining additional traps for keeping the object working - - // Proxies on the prototype dont intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined, - // to avoid Reflect.get having no effect and causing a stack overflow - get: (target, p, receiver) => { - return undefined; - }, - // Same thing as get - has: (target, p) => { - return false; - } - */ - - // The set trap for patching or defining getters for the module factories when new module factories are loaded - set: (target, p, newValue, receiver) => { - // If the property is not a number, we are not dealing with a module factory - if (Number.isNaN(Number(p))) { - return define(target, p, { value: newValue }); - } - - if (updateExistingFactory(target, p, newValue)) { - return true; - } - - notifyFactoryListeners(newValue); - defineModulesFactoryGetter(p, Settings.eagerPatches ? patchFactory(p, newValue) : newValue); - - return true; - } -}; - /** * Patches a module factory. * From 6d587e6530c2134aef9a7402a9ec3a99f02712b0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 17:36:43 -0300 Subject: [PATCH 108/125] More re-ordering --- src/webpack/patchWebpack.ts | 66 ++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 78ef03595a..1394d7fac6 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -138,39 +138,6 @@ const moduleFactoriesHandler: ProxyHandler = { } }; -/** - * Define the getter for returning the patched version of the module factory. - * - * If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original - * and only be patched when accessed for the first time. - * - * @param id The id of the module - * @param factory The original or patched module factory - */ -function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { - // 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, { - get() { - // $$vencordOriginal means the factory is already patched - if (factory.$$vencordOriginal != null) { - return factory; - } - - return (factory = patchFactory(id, factory)); - }, - set(v: AnyModuleFactory) { - if (factory.$$vencordOriginal != null) { - factory.$$vencordOriginal = v; - } else { - factory = v; - } - } - }); - } -} - /** * Update a factory that exists in any Webpack instance with a new original factory. * @@ -218,6 +185,39 @@ function notifyFactoryListeners(factory: AnyModuleFactory) { } } +/** + * Define the getter for returning the patched version of the module factory. + * + * If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original + * and only be patched when accessed for the first time. + * + * @param id The id of the module + * @param factory The original or patched module factory + */ +function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { + // 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, { + get() { + // $$vencordOriginal means the factory is already patched + if (factory.$$vencordOriginal != null) { + return factory; + } + + return (factory = patchFactory(id, factory)); + }, + set(v: AnyModuleFactory) { + if (factory.$$vencordOriginal != null) { + factory.$$vencordOriginal = v; + } else { + factory = v; + } + } + }); + } +} + /** * Patches a module factory. * From 8d31f3cf622771eb3e06d5a7b2f9a7e8f591cd8b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:14:52 -0300 Subject: [PATCH 109/125] Use $$vencordOriginal as fallback --- src/webpack/patchWebpack.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 1394d7fac6..0ebf6f7cac 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -209,6 +209,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto }, set(v: AnyModuleFactory) { if (factory.$$vencordOriginal != null) { + factory.toString = v.toString.bind(v); factory.$$vencordOriginal = v; } else { factory = v; @@ -367,7 +368,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { } if (IS_DEV) { - return originalFactory.apply(this, args); + return patchedFactory.$$vencordOriginal?.apply(this, args); } } @@ -377,15 +378,19 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { factoryReturn = factory.apply(this, args); } catch (err) { // Just re-throw Discord errors - if (factory === originalFactory) throw err; + if (factory === originalFactory) { + throw err; + } logger.error("Error in patched module factory", err); - return originalFactory.apply(this, args); + return patchedFactory.$$vencordOriginal?.apply(this, args); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it exports = module?.exports; - if (exports == null) return factoryReturn; + if (exports == null) { + return factoryReturn; + } // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance From d473d520479926b9fc082c2411c2e39241d2fc3b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:32:42 -0300 Subject: [PATCH 110/125] update comment --- src/webpack/patchWebpack.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 0ebf6f7cac..d28388600c 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -339,8 +339,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { // The patched factory wrapper, define it in an object to preserve the name after minification const patchedFactory: PatchedModuleFactory = { PatchedFactory(...args: Parameters) { - // Restore the original factory in all the module factories objects, - // because we want to make sure the original factory is restored properly, no matter what is the Webpack instance + // 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: patchedFactory.$$vencordOriginal }); } From afe6d9f7b3546dce866e08096689eeb77bb1aff1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 16 Jun 2024 19:53:31 -0300 Subject: [PATCH 111/125] Separate wrapFactory and patchFactory --- src/webpack/patchWebpack.ts | 231 +++++++++++++++++++----------------- src/webpack/wreq.d.ts | 4 +- 2 files changed, 123 insertions(+), 112 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d28388600c..cef3132632 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -12,7 +12,7 @@ import { PatchReplacement } from "@utils/types"; import { traceFunction } from "../debug/Tracer"; import { patches } from "../plugins"; -import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, PatchedModuleFactories, PatchedModuleFactory, subscriptions, WebpackRequire, wreq } from "."; +import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, subscriptions, WebpackRequire, WrappedModuleFactory, wreq } from "."; const logger = new Logger("WebpackInterceptor", "#8caaee"); @@ -85,7 +85,7 @@ define(Function.prototype, "O", { } notifyFactoryListeners(this.m[id]); - defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, this.m[id]) : this.m[id]); + defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, this.m[id]) : this.m[id]); } define(this.m, Symbol.toStringTag, { @@ -104,7 +104,7 @@ define(Function.prototype, "O", { } }); -const moduleFactoriesHandler: ProxyHandler = { +const moduleFactoriesHandler: ProxyHandler = { /* If Discord 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 and that requires defining additional traps for keeping the object working @@ -132,7 +132,7 @@ const moduleFactoriesHandler: ProxyHandler = { } notifyFactoryListeners(newValue); - defineModulesFactoryGetter(p, Settings.eagerPatches ? patchFactory(p, newValue) : newValue); + defineModulesFactoryGetter(p, Settings.eagerPatches ? wrapAndPatchFactory(p, newValue) : newValue); return true; } @@ -148,7 +148,7 @@ const moduleFactoriesHandler: ProxyHandler = { * @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) { - let existingFactory: TypedPropertyDescriptor | undefined; + let existingFactory: TypedPropertyDescriptor | undefined; for (const wreq of allWebpackInstances) { if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; @@ -194,7 +194,7 @@ function notifyFactoryListeners(factory: AnyModuleFactory) { * @param id The id of the module * @param factory The original or patched module factory */ -function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) { +function defineModulesFactoryGetter(id: PropertyKey, factory: WrappedModuleFactory) { // 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) { @@ -205,7 +205,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto return factory; } - return (factory = patchFactory(id, factory)); + return (factory = wrapAndPatchFactory(id, factory)); }, set(v: AnyModuleFactory) { if (factory.$$vencordOriginal != null) { @@ -220,20 +220,125 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto } /** - * Patches a module factory. + * Wraps and patches a module factory. * - * The factory argument will become the patched version of the factory. * @param id The id of the module * @param factory The original or patched module factory * @returns The wrapper for the patched module factory */ -function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { - const originalFactory = factory; +function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) { + const patchedFactory = patchFactory(id, originalFactory); - const patchedBy = new Set(); + // The patched factory wrapper, define it in an object to preserve the name after minification + const wrappedFactory: WrappedModuleFactory = { + PatchedFactory(...args: Parameters) { + // 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.$$vencordOriginal }); + } + + // eslint-disable-next-line prefer-const + let [module, exports, require] = args; + + if (wreq == null) { + if (!wreqFallbackApplied) { + wreqFallbackApplied = true; + + // Make sure the require argument is actually the WebpackRequire function + if (typeof require === "function" && require.m != null) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `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."); + } + } + + if (IS_DEV) { + return wrappedFactory.$$vencordOriginal?.apply(this, args); + } + } + + let factoryReturn: unknown; + try { + // Call the patched factory + factoryReturn = patchedFactory.apply(this, args); + } catch (err) { + // Just re-throw Discord errors + if (patchedFactory === originalFactory) { + throw err; + } + + logger.error("Error in patched module factory", err); + return wrappedFactory.$$vencordOriginal?.apply(this, args); + } + + // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it + exports = module?.exports; + if (exports == null) { + return factoryReturn; + } + + // There are (at the time of writing) 11 modules exporting the window + // Make these non enumerable to improve webpack search performance + if ((exports === window || exports?.default === window) && typeof require === "function" && require.c != null) { + define(require.c, id, { + value: require.c[id], + enumerable: false + }); + 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 subscriptions) { + try { + if (exports && filter(exports)) { + subscriptions.delete(filter); + callback(exports, id); + } else if (exports.default && filter(exports.default)) { + subscriptions.delete(filter); + callback(exports.default, id); + } + } catch (err) { + logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); + } + } + + return factoryReturn; + } + }.PatchedFactory; + + wrappedFactory.toString = originalFactory.toString.bind(originalFactory); + wrappedFactory.$$vencordOriginal = originalFactory; + + return wrappedFactory; +} + +/** + * Patches a module factory. + * + * @param id The id of the module + * @param factory The original module factory + * @returns The patched module factory + */ +function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, let code: string = "0," + String(factory); + let patchedFactory = factory; + + const patchedBy = new Set(); for (let i = 0; i < patches.length; i++) { const patch = patches[i]; @@ -273,7 +378,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); code = previousCode; - factory = previousFactory; + patchedFactory = previousFactory; patchedBy.delete(patch.plugin); break; } @@ -282,7 +387,7 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { } code = newCode; - factory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`); + patchedFactory = (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); @@ -324,111 +429,17 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { if (patch.group) { logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); code = previousCode; - factory = previousFactory; + patchedFactory = previousFactory; break; } code = lastCode; - factory = lastFactory; + patchedFactory = lastFactory; } } if (!patch.all) patches.splice(i--, 1); } - // The patched factory wrapper, define it in an object to preserve the name after minification - const patchedFactory: PatchedModuleFactory = { - PatchedFactory(...args: Parameters) { - // 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: patchedFactory.$$vencordOriginal }); - } - - // eslint-disable-next-line prefer-const - let [module, exports, require] = args; - - if (wreq == null) { - if (!wreqFallbackApplied) { - wreqFallbackApplied = true; - - // Make sure the require argument is actually the WebpackRequire function - if (typeof require === "function" && require.m != null) { - const { stack } = new Error(); - const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; - logger.warn( - "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + - `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."); - } - } - - if (IS_DEV) { - return patchedFactory.$$vencordOriginal?.apply(this, args); - } - } - - let factoryReturn: unknown; - try { - // Call the patched factory - factoryReturn = factory.apply(this, args); - } catch (err) { - // Just re-throw Discord errors - if (factory === originalFactory) { - throw err; - } - - logger.error("Error in patched module factory", err); - return patchedFactory.$$vencordOriginal?.apply(this, args); - } - - // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it - exports = module?.exports; - if (exports == null) { - return factoryReturn; - } - - // There are (at the time of writing) 11 modules exporting the window - // Make these non enumerable to improve webpack search performance - if ((exports === window || exports?.default === window) && typeof require === "function" && require.c != null) { - define(require.c, id, { - value: require.c[id], - enumerable: false - }); - 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 subscriptions) { - try { - if (exports && filter(exports)) { - subscriptions.delete(filter); - callback(exports, id); - } else if (exports.default && filter(exports.default)) { - subscriptions.delete(filter); - callback(exports.default, id); - } - } catch (err) { - logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); - } - } - - return factoryReturn; - } - }.PatchedFactory; - - patchedFactory.toString = originalFactory.toString.bind(originalFactory); - patchedFactory.$$vencordOriginal = originalFactory; - return patchedFactory; } diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index c27ad3094c..648ce839cf 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -193,8 +193,8 @@ export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Par /** exports can be anything, however initially it is always an empty object */ export type AnyModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void; -export type PatchedModuleFactory = AnyModuleFactory & { +export type WrappedModuleFactory = AnyModuleFactory & { $$vencordOriginal?: AnyModuleFactory; }; -export type PatchedModuleFactories = Record; +export type WrappedModuleFactories = Record; From f20847284195951c85f32fc5baf4f3b4219e82b8 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 16 Jun 2024 20:07:48 -0300 Subject: [PATCH 112/125] \n --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index cef3132632..5fa548361d 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -274,7 +274,7 @@ function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) throw err; } - logger.error("Error in patched module factory", err); + logger.error("Error in patched module factory:\n", err); return wrappedFactory.$$vencordOriginal?.apply(this, args); } From 066001c57ac39bd4facf66987a2fe115ea6e02de Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 21 Jun 2024 04:45:16 -0300 Subject: [PATCH 113/125] Add wreq.j --- src/plugins/consoleShortcuts/index.ts | 1 + src/webpack/webpack.ts | 6 ++--- src/webpack/wreq.d.ts | 32 ++++++++++++++------------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 4be2e2ee86..5703f7f539 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -85,6 +85,7 @@ function makeShortcuts() { wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); }, + filters, find, findAll: findAll, findByProps, diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 989c239fd7..bbe8ab5137 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -453,14 +453,14 @@ export function findExportedComponentLazy(...props: stri * closeModal: filters.byCode("key==") * }) */ -export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string, mappers: Record): Record { - const exports = {} as Record; +export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string, mappers: Record): Record { + const exports = {} as Record; const id = findModuleId(code); if (id === null) return exports; - const mod = wreq(id as any); + const mod = wreq(id); outer: for (const key in mod) { const member = mod[key]; diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 648ce839cf..3416bbfbbc 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -58,21 +58,21 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { m: Record; /** The module cache, where all modules which have been WebpackRequire'd are stored */ c: Record; - /** - * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: - * @example - * const fromObject = { a: 1 }; - * Object.keys(fromObject).forEach(key => { - * if (key !== "default" && !Object.hasOwn(toObject, key)) { - * Object.defineProperty(toObject, key, { - * get: () => fromObject[key], - * enumerable: true - * }); - * } - * }); - * @returns fromObject - */ - es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; + // /** + // * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: + // * @example + // * const fromObject = { a: 1 }; + // * Object.keys(fromObject).forEach(key => { + // * if (key !== "default" && !Object.hasOwn(toObject, key)) { + // * Object.defineProperty(toObject, key, { + // * get: () => fromObject[key], + // * enumerable: true + // * }); + // * } + // * }); + // * @returns fromObject + // */ + // es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; /** * Creates an async module. A module that exports something that is a Promise, or requires an export from an async module. * @@ -179,6 +179,8 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise; /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ p: string; + /** The runtime id of the current runtime */ + j: string; /** Document baseURI or WebWorker location.href */ b: string; }; From 343e2802c30bfca8bf8c6a0efd8772731760209b Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 21 Jun 2024 04:52:08 -0300 Subject: [PATCH 114/125] boop --- src/webpack/webpack.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index bbe8ab5137..b5f0ab6200 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -630,15 +630,15 @@ export function search(...filters: Array) { * @param id The id of the module to extract */ export function extract(id: PropertyKey) { - const mod = wreq.m[id]; - if (!mod) return null; + const factory = wreq.m[id]; + if (!factory) return null; const code = ` // [EXTRACTED] WebpackModule${String(id)} // WARNING: This module was extracted to be more easily readable. // This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!! -0,${String(mod)} +0,${String(factory)} //# sourceURL=ExtractedWebpackModule${String(id)} `; const extracted: ModuleFactory = (0, eval)(code); From 3cf702a2ba35cfcedce695ed444ad3ad0b5e6e1a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 21 Jun 2024 05:01:35 -0300 Subject: [PATCH 115/125] fixes --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index d257cc7a5b..80676fd165 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -260,7 +260,7 @@ function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) } if (IS_DEV) { - return wrappedFactory.$$vencordOriginal?.apply(this, args); + return wrappedFactory.$$vencordOriginal!.apply(this, args); } } @@ -275,7 +275,7 @@ function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) } logger.error("Error in patched module factory:\n", err); - return wrappedFactory.$$vencordOriginal?.apply(this, args); + return wrappedFactory.$$vencordOriginal!.apply(this, args); } // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it From c4e7233604ef1a5ff6ee636d34ba208d2a1e59fc Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 22 Jun 2024 02:57:36 -0300 Subject: [PATCH 116/125] check for bundlePath value again --- src/webpack/patchWebpack.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 80676fd165..2918cf1365 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -70,6 +70,8 @@ define(Function.prototype, "O", { define(this, "p", { value: bundlePath }); clearTimeout(setterTimeout); + if (bundlePath !== "/assets/") return; + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); } From e592a50d7c720a55eb28f485a4d7648c1bdfc2d5 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 27 Jun 2024 06:35:01 -0300 Subject: [PATCH 117/125] Fix broken patches --- scripts/generateReport.ts | 1 - src/plugins/fakeProfileThemes/index.tsx | 2 +- src/plugins/userVoiceShow/index.tsx | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index d8cbb44a0d..0b94aaf726 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -136,7 +136,6 @@ async function printReport() { body: JSON.stringify({ description: "Here's the latest Vencord Report!", username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""), - avatar_url: "https://cdn.discordapp.com/avatars/1017176847865352332/c312b6b44179ae6817de7e4b09e9c6af.webp?size=512", embeds: [ { title: "Bad Patches", diff --git a/src/plugins/fakeProfileThemes/index.tsx b/src/plugins/fakeProfileThemes/index.tsx index 85aadca136..9e784da685 100644 --- a/src/plugins/fakeProfileThemes/index.tsx +++ b/src/plugins/fakeProfileThemes/index.tsx @@ -109,7 +109,7 @@ interface ProfileModalProps { } const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); -const ProfileModal = findComponentByCodeLazy("isTryItOutFlow:", "pendingThemeColors:", "avatarDecorationOverride:", ".CUSTOM_STATUS"); +const ProfileModal = findComponentByCodeLazy("isTryItOutFlow:", "pendingThemeColors:", "pendingAvatarDecoration:", "EDIT_PROFILE_BANNER"); const requireColorPicker = extractAndLoadChunksLazy(["USER_SETTINGS_PROFILE_COLOR_DEFAULT_BUTTON.format"], /createPromise:\(\)=>\i\.\i(\("?.+?"?\)).then\(\i\.bind\(\i,"?(.+?)"?\)\)/); diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index feba283169..53044a5587 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -98,8 +98,8 @@ export default definePlugin({ { find: ".popularApplicationCommandIds,", replacement: { - match: /applicationId:\i\.id}\),(?=.{0,50}setNote:\i)/, - replace: "$&$self.patchPopout(arguments[0]),", + match: /(?<=,)(?=!\i&&!\i&&.{0,50}setNote:)/, + replace: "$self.patchPopout(arguments[0]),", } }, // below username From 1e4ad4db4cebc09f6ee26028ab14c38f9ce62e12 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 28 Jun 2024 00:42:36 -0300 Subject: [PATCH 118/125] Fix for latest webpack change --- src/webpack/patchWebpack.ts | 32 +++++++++++++------------------- src/webpack/wreq.d.ts | 34 +++++++++++++++++----------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 2918cf1365..c4bcd8cd1d 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -34,25 +34,19 @@ const define: Define = (target, p, attributes) => { }); }; -// wreq.O is the Webpack onChunksLoaded function. -// It is pretty likely that all important Discord Webpack instances will have this property set, -// because Discord bundled code is chunked. -// As of the time of writing, only the main and sentry Webpack instances have this property, and they are the only ones we care about. - -// We use this setter to intercept when wreq.O is defined, so we can patch the modules factories (wreq.m). -// wreq.m 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 wreq.m, so this also patches those. +// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push +// 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 definining getters for the patched versions. // If this is the main Webpack, we also set up the internal references to WebpackRequire. -define(Function.prototype, "O", { +define(Function.prototype, "m", { enumerable: false, - set(this: AnyWebpackRequire, onChunksLoaded: AnyWebpackRequire["O"]) { - define(this, "O", { value: onChunksLoaded }); + set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) { + define(this, "m", { value: originalModules }); const { stack } = new Error(); - if (this.m == null || !(stack?.includes("discord.com") || stack?.includes("discordapp.com"))) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(originalModules)) { return; } @@ -81,25 +75,25 @@ define(Function.prototype, "O", { const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); // Patch the pre-populated factories - for (const id in this.m) { - if (updateExistingFactory(this.m, id, this.m[id], true)) { + for (const id in originalModules) { + if (updateExistingFactory(originalModules, id, originalModules[id], true)) { continue; } - notifyFactoryListeners(this.m[id]); - defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, this.m[id]) : this.m[id]); + notifyFactoryListeners(originalModules[id]); + defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]); } - define(this.m, Symbol.toStringTag, { + define(originalModules, Symbol.toStringTag, { value: "ModuleFactories", enumerable: false }); // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions - const proxiedModuleFactories = new Proxy(this.m, moduleFactoriesHandler); + const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype - define(this, "m", { value: Reflect.setPrototypeOf(this.m, new Proxy(this.m, moduleFactoriesHandler)) }); + define(this, "m", { value: Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler)) }); */ define(this, "m", { value: proxiedModuleFactories }); diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 3416bbfbbc..6dd40b84cb 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -48,10 +48,10 @@ export type ChunkHandlers = { export type ScriptLoadDone = (event: Event) => void; -export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { - /** Check if a chunk has been loaded */ - j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; -}; +// export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { +// /** Check if a chunk has been loaded */ +// j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; +// }; export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ @@ -160,18 +160,18 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { r: (this: WebpackRequire, exports: ModuleExports) => void; /** Node.js module decorator. Decorates a module as a Node.js module */ nmd: (this: WebpackRequire, module: Module) => any; - /** - * Register deferred code which will be executed when the passed chunks are loaded. - * - * If chunkIds is defined, it defers the execution of the callback and returns undefined. - * - * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument. - * - * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code. - * - * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. - */ - O: OnChunksLoaded; + // /** + // * Register deferred code which will be executed when the passed chunks are loaded. + // * + // * If chunkIds is defined, it defers the execution of the callback and returns undefined. + // * + // * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument. + // * + // * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code. + // * + // * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. + // */ + // O: OnChunksLoaded; /** * Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports". * @returns The exports argument, but now assigned with the exports of the wasm instance @@ -187,7 +187,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { // Utility section for Vencord -export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial> & Pick & { +export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial> & { /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ m: Record; }; From 748a456cfb973cdfcae92d5f342bfba6b3762183 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:54:37 -0300 Subject: [PATCH 119/125] Delete patching properties after used --- src/plugins/_core/noTrack.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index ef2849bbcc..b94a91092a 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -114,6 +114,7 @@ export default definePlugin({ } })); + Reflect.deleteProperty(Function.prototype, "g"); Reflect.deleteProperty(Object.prototype, cacheExtractSym); Reflect.deleteProperty(window, "DiscordSentry"); return { exports: {} }; @@ -130,6 +131,7 @@ export default definePlugin({ set() { new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry"); + Reflect.deleteProperty(Function.prototype, "g"); Reflect.deleteProperty(window, "DiscordSentry"); } }); From 9e998db456e13f1f1479928a06ea1b6d3f8859a3 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:03:09 -0300 Subject: [PATCH 120/125] typings --- src/plugins/_core/noTrack.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index b94a91092a..901e2da74e 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { WebpackRequire } from "webpack"; const settings = definePluginSettings({ disableAnalytics: { @@ -82,9 +83,9 @@ export default definePlugin({ Object.defineProperty(Function.prototype, "g", { configurable: true, - set(v: any) { + set(this: WebpackRequire, globalObj: WebpackRequire["g"]) { Object.defineProperty(this, "g", { - value: v, + value: globalObj, configurable: true, enumerable: true, writable: true @@ -101,7 +102,7 @@ export default definePlugin({ Object.defineProperty(Object.prototype, cacheExtractSym, { configurable: true, - get() { + get(this: WebpackRequire["c"]) { // One more condition to check if this is the Sentry WebpackInstance if (Array.isArray(this)) { return { exports: {} }; From 5a85d6e78e2fea8e9663b5317f62f4479a5555fa Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 29 Jun 2024 01:26:03 -0300 Subject: [PATCH 121/125] Harder conditions for Sentry patching --- src/plugins/_core/noTrack.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index ef2849bbcc..d284403691 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -93,7 +93,21 @@ export default definePlugin({ // Ensure this is most likely the Sentry WebpackInstance. // Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: ) to include it const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || this.c != null || !String(this).includes("exports:{}")) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) { + return; + } + + const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0]; + if (!assetPath) { + return; + } + + const srcRequest = new XMLHttpRequest(); + srcRequest.open("GET", assetPath, false); + srcRequest.send(); + + // Final condition to see if this is the Sentry WebpackInstance + if (!srcRequest.responseText.includes("window.DiscordSentry=")) { return; } @@ -102,11 +116,6 @@ export default definePlugin({ configurable: true, get() { - // One more condition to check if this is the Sentry WebpackInstance - if (Array.isArray(this)) { - return { exports: {} }; - } - new Logger("NoTrack", "#8caaee").info("Disabling Sentry by proxying its WebpackInstance cache"); Object.setPrototypeOf(this, new Proxy(this, { get() { @@ -114,8 +123,10 @@ export default definePlugin({ } })); + Reflect.deleteProperty(Function.prototype, "g"); Reflect.deleteProperty(Object.prototype, cacheExtractSym); Reflect.deleteProperty(window, "DiscordSentry"); + return { exports: {} }; } }); @@ -130,6 +141,8 @@ export default definePlugin({ set() { new Logger("NoTrack", "#8caaee").error("Failed to disable Sentry. Falling back to deleting window.DiscordSentry"); + + Reflect.deleteProperty(Function.prototype, "g"); Reflect.deleteProperty(window, "DiscordSentry"); } }); From 332f3e532ba0ff861570f73c35be6044e39204f0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:21:44 -0300 Subject: [PATCH 122/125] Dont depend on modules being an object again --- src/webpack/patchWebpack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 55425431f3..eca02dc490 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -48,7 +48,7 @@ define(Function.prototype, "m", { // We may also catch Discord bundled libs, React Devtools or other extensions WebpackInstance here. // This ensures we actually got the right ones const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(originalModules)) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || (stack != null ? /at \d+? \(/.test(stack) : true) || !String(this).includes("exports:{}")) { return; } @@ -66,7 +66,7 @@ define(Function.prototype, "m", { define(this, "p", { value: bundlePath }); clearTimeout(setterTimeout); - if (bundlePath !== "/assets/") return; + if (window.GLOBAL_ENV?.PUBLIC_PATH != null && bundlePath !== window.GLOBAL_ENV.PUBLIC_PATH) return; logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); _initWebpack(this); From b333deb7312107a629920737a0e9cd56936a35a5 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Tue, 2 Jul 2024 00:17:40 +0200 Subject: [PATCH 123/125] improve settings ui (again) --- .stylelintrc.json | 8 +- src/components/Icons.tsx | 15 +++ src/components/VencordSettings/ThemesTab.tsx | 88 ++++++++--------- src/components/VencordSettings/VencordTab.tsx | 77 ++++++--------- .../VencordSettings/quickActions.css | 34 +++++++ .../VencordSettings/quickActions.tsx | 39 ++++++++ .../VencordSettings/settingsStyles.css | 20 ---- src/utils/misc.ts | 2 +- src/webpack/common/types/utils.d.ts | 62 +++++++++++- src/webpack/common/utils.ts | 52 +++++----- src/webpack/webpack.ts | 98 ++++++++++--------- 11 files changed, 309 insertions(+), 186 deletions(-) create mode 100644 src/components/VencordSettings/quickActions.css create mode 100644 src/components/VencordSettings/quickActions.tsx diff --git a/.stylelintrc.json b/.stylelintrc.json index 6449c3f294..ec25497621 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,6 +1,12 @@ { "extends": "stylelint-config-standard", "rules": { - "indentation": 4 + "indentation": 4, + "selector-class-pattern": [ + "^[a-z][a-zA-Z0-9]*(-[a-z0-9][a-zA-Z0-9]*)*$", + { + "message": "Expected class selector to be kebab-case with camelCase segments" + } + ] } } diff --git a/src/components/Icons.tsx b/src/components/Icons.tsx index d82ce0b006..7ba078d333 100644 --- a/src/components/Icons.tsx +++ b/src/components/Icons.tsx @@ -392,6 +392,21 @@ export function PaintbrushIcon(props: IconProps) { ); } +export function PencilIcon(props: IconProps) { + return ( + + + + ); +} + const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg"; const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; diff --git a/src/components/VencordSettings/ThemesTab.tsx b/src/components/VencordSettings/ThemesTab.tsx index 2eb91cb82b..016371bedf 100644 --- a/src/components/VencordSettings/ThemesTab.tsx +++ b/src/components/VencordSettings/ThemesTab.tsx @@ -19,21 +19,21 @@ import { useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import { Flex } from "@components/Flex"; -import { DeleteIcon } from "@components/Icons"; +import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons"; import { Link } from "@components/Link"; -import PluginModal from "@components/PluginSettings/PluginModal"; +import { openPluginModal } from "@components/PluginSettings/PluginModal"; import type { UserThemeHeader } from "@main/themes"; import { openInviteModal } from "@utils/discord"; import { Margins } from "@utils/margins"; import { classes } from "@utils/misc"; -import { openModal } from "@utils/modal"; import { showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; import { findByPropsLazy, findLazy } from "@webpack"; -import { Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; +import { Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common"; import type { ComponentType, Ref, SyntheticEvent } from "react"; import { AddonCard } from "./AddonCard"; +import { QuickAction, QuickActionCard } from "./quickActions"; import { SettingsTab, wrapTab } from "./shared"; type FileInput = ComponentType<{ @@ -213,60 +213,52 @@ function ThemesTab() { - + <> {IS_WEB ? ( - + + Upload Theme + + + } + Icon={PlusIcon} + /> ) : ( - + Icon={FolderIcon} + /> )} - - + + VencordNative.quickCss.openEditor()} + Icon={PaintbrushIcon} + /> {Vencord.Settings.plugins.ClientTheme.enabled && ( - + openPluginModal(Vencord.Plugins.plugins.ClientTheme)} + Icon={PencilIcon} + /> )} - +
{userThemes?.map(theme => ( diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index d13b43fb2c..97f82e7775 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -21,15 +21,16 @@ import { useSettings } from "@api/Settings"; import { classNameFactory } from "@api/Styles"; import DonateButton from "@components/DonateButton"; import { openPluginModal } from "@components/PluginSettings/PluginModal"; +import { gitRemote } from "@shared/vencordUserAgent"; import { Margins } from "@utils/margins"; import { identity } from "@utils/misc"; import { relaunch, showItemInFolder } from "@utils/native"; import { useAwaiter } from "@utils/react"; -import { Button, Card, Forms, React, Select, Switch, TooltipContainer } from "@webpack/common"; -import { ComponentType } from "react"; +import { Button, Card, Forms, React, Select, Switch } from "@webpack/common"; import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from ".."; import { openNotificationSettingsModal } from "./NotificationSettings"; +import { QuickAction, QuickActionCard } from "./quickActions"; import { SettingsTab, wrapTab } from "./shared"; const cl = classNameFactory("vc-settings-"); @@ -41,17 +42,6 @@ type KeysOfType = { [K in keyof Object]: Object[K] extends Type ? K : never; }[keyof Object]; -const iconWithTooltip = (Icon: ComponentType<{ className?: string; }>, tooltip: string) => () => ( - - - -); - -const NotificationLogIcon = iconWithTooltip(LogIcon, "Open Notification Log"); -const QuickCssIcon = iconWithTooltip(PaintbrushIcon, "Edit QuickCSS"); -const RelaunchIcon = iconWithTooltip(RestartIcon, "Relaunch Discord"); -const OpenSettingsDirIcon = iconWithTooltip(FolderIcon, "Open Settings Directory"); -const OpenGithubIcon = iconWithTooltip(GithubIcon, "View Vencord's GitHub Repository"); function VencordSettings() { const [settingsDir, , settingsDirPending] = useAwaiter(VencordNative.settings.getSettingsDir, { @@ -111,44 +101,37 @@ function VencordSettings() { - - - + + + VencordNative.quickCss.openEditor()} + /> {!IS_WEB && ( - + )} {!IS_WEB && ( - + showItemInFolder(settingsDir)} + /> )} - - + VencordNative.native.openExternal("https://github.com/" + gitRemote)} + /> + diff --git a/src/components/VencordSettings/quickActions.css b/src/components/VencordSettings/quickActions.css new file mode 100644 index 0000000000..897bc8c811 --- /dev/null +++ b/src/components/VencordSettings/quickActions.css @@ -0,0 +1,34 @@ +.vc-settings-quickActions-card { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, max-content)); + gap: 0.5em; + justify-content: center; + padding: 0.5em 0; + margin-bottom: 1em; +} + +.vc-settings-quickActions-pill { + all: unset; + + background: var(--background-secondary); + color: var(--header-secondary); + display: flex; + align-items: center; + gap: 0.5em; + padding: 8px 12px; + border-radius: 9999px; +} + +.vc-settings-quickActions-pill:hover { + background: var(--background-secondary-alt); +} + +.vc-settings-quickActions-pill:focus-visible { + outline: 2px solid var(--focus-primary); + outline-offset: 2px; +} + +.vc-settings-quickActions-img { + width: 24px; + height: 24px; +} diff --git a/src/components/VencordSettings/quickActions.tsx b/src/components/VencordSettings/quickActions.tsx new file mode 100644 index 0000000000..6cc57180a8 --- /dev/null +++ b/src/components/VencordSettings/quickActions.tsx @@ -0,0 +1,39 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./quickActions.css"; + +import { classNameFactory } from "@api/Styles"; +import { Card } from "@webpack/common"; +import type { ComponentType, PropsWithChildren, ReactNode } from "react"; + +const cl = classNameFactory("vc-settings-quickActions-"); + +export interface QuickActionProps { + Icon: ComponentType<{ className?: string; }>; + text: ReactNode; + action?: () => void; + disabled?: boolean; +} + +export function QuickAction(props: QuickActionProps) { + const { Icon, action, text, disabled } = props; + + return ( + + ); +} + +export function QuickActionCard(props: PropsWithChildren) { + return ( + + {props.children} + + ); +} diff --git a/src/components/VencordSettings/settingsStyles.css b/src/components/VencordSettings/settingsStyles.css index 6e8826c521..13558be21e 100644 --- a/src/components/VencordSettings/settingsStyles.css +++ b/src/components/VencordSettings/settingsStyles.css @@ -10,26 +10,6 @@ margin-bottom: -2px; } -.vc-settings-quick-actions-card { - color: var(--text-normal); - padding: 1em; - display: flex; - justify-content: space-evenly; - gap: 1em; - flex-wrap: wrap; - align-items: center; - margin-bottom: 1em; -} - -.vc-settings-quick-actions-card button { - min-width: unset; -} - -.vc-settings-quick-actions-img { - width: 30px; - height: 30px; -} - .vc-settings-donate { display: flex; flex-direction: row; diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 7d6b4affcf..28c371c5b7 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -24,7 +24,7 @@ import { DevsById } from "./constants"; * Calls .join(" ") on the arguments * classes("one", "two") => "one two" */ -export function classes(...classes: Array) { +export function classes(...classes: Array) { return classes.filter(Boolean).join(" "); } diff --git a/src/webpack/common/types/utils.d.ts b/src/webpack/common/types/utils.d.ts index ee3f69944f..ce1e3e2682 100644 --- a/src/webpack/common/types/utils.d.ts +++ b/src/webpack/common/types/utils.d.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Guild, GuildMember } from "discord-types/general"; +import { Guild, GuildMember, User } from "discord-types/general"; import type { ReactNode } from "react"; import { LiteralUnion } from "type-fest"; @@ -256,3 +256,63 @@ export interface PopoutActions { close(key: string): void; setAlwaysOnTop(key: string, alwaysOnTop: boolean): void; } + +export type UserNameUtilsTagInclude = LiteralUnion<"auto" | "always" | "never", string>; +export interface UserNameUtilsTagOptions { + forcePomelo?: boolean; + identifiable?: UserNameUtilsTagInclude; + decoration?: UserNameUtilsTagInclude; + mode?: "full" | "username"; +} + +export interface UsernameUtils { + getGlobalName(user: User): string; + getFormattedName(user: User, useTagInsteadOfUsername?: boolean): string; + getName(user: User): string; + useName(user: User): string; + getUserTag(user: User, options?: UserNameUtilsTagOptions): string; + useUserTag(user: User, options?: UserNameUtilsTagOptions): string; + + + useDirectMessageRecipient: any; + humanizeStatus: any; +} + +export class DisplayProfile { + userId: string; + banner?: string; + bio?: string; + pronouns?: string; + accentColor?: number; + themeColors?: number[]; + popoutAnimationParticleType?: any; + profileEffectId?: string; + _userProfile?: any; + _guildMemberProfile?: any; + canUsePremiumProfileCustomization: boolean; + canEditThemes: boolean; + premiumGuildSince: Date | null; + premiumSince: Date | null; + premiumType?: number; + primaryColor?: number; + + getBadges(): Array<{ + id: string; + description: string; + icon: string; + link?: string; + }>; + getBannerURL(options: { canAnimate: boolean; size: number; }): string; + getLegacyUsername(): string | null; + hasFullProfile(): boolean; + hasPremiumCustomization(): boolean; + hasThemeColors(): boolean; + isUsingGuildMemberBanner(): boolean; + isUsingGuildMemberBio(): boolean; + isUsingGuildMemberPronouns(): boolean; +} + +export interface DisplayProfileUtils { + getDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null; + useDisplayProfile(userId: string, guildId?: string, customStores?: any): DisplayProfile | null; +} diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index a6853c84a0..280b2ba907 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -73,6 +73,25 @@ const ToastPosition = { BOTTOM: 1 }; +export interface ToastData { + message: string, + id: string, + /** + * Toasts.Type + */ + type: number, + options?: ToastOptions; +} + +export interface ToastOptions { + /** + * Toasts.Position + */ + position?: number; + component?: React.ReactNode, + duration?: number; +} + export const Toasts = { Type: ToastType, Position: ToastPosition, @@ -81,23 +100,9 @@ export const Toasts = { // hack to merge with the following interface, dunno if there's a better way ...{} as { - show(data: { - message: string, - id: string, - /** - * Toasts.Type - */ - type: number, - options?: { - /** - * Toasts.Position - */ - position?: number; - component?: React.ReactNode, - duration?: number; - }; - }): void; + show(data: ToastData): void; pop(): void; + create(message: string, type: number, options?: ToastOptions): ToastData; } }; @@ -105,18 +110,15 @@ export const Toasts = { waitFor("showToast", m => { Toasts.show = m.showToast; Toasts.pop = m.popToast; + Toasts.create = m.createToast; }); /** * Show a simple toast. If you need more options, use Toasts.show manually */ -export function showToast(message: string, type = ToastType.MESSAGE) { - Toasts.show({ - id: Toasts.genId(), - message, - type - }); +export function showToast(message: string, type = ToastType.MESSAGE, options?: ToastOptions) { + Toasts.show(Toasts.create(message, type, options)); } export const UserUtils = { @@ -172,3 +174,9 @@ export const PopoutActions: t.PopoutActions = mapMangledModuleLazy('type:"POPOUT close: filters.byCode('type:"POPOUT_WINDOW_CLOSE"'), setAlwaysOnTop: filters.byCode('type:"POPOUT_WINDOW_SET_ALWAYS_ON_TOP"'), }); + +export const UsernameUtils: t.UsernameUtils = findByPropsLazy("useName", "getGlobalName"); +export const DisplayProfileUtils: t.DisplayProfileUtils = mapMangledModuleLazy(/=\i\.getUserProfile\(\i\),\i=\i\.getGuildMemberProfile\(/, { + getDisplayProfile: filters.byCode(".getGuildMemberProfile("), + useDisplayProfile: filters.byCode(/\[\i\.\i,\i\.\i],\(\)=>/) +}); diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index f776ab1c33..f21a38d670 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -38,31 +38,43 @@ export let cache: WebpackInstance["c"]; export type FilterFn = (mod: any) => boolean; +type PropsFilter = Array; +type CodeFilter = Array; +type StoreNameFilter = string; + +const stringMatches = (s: string, filter: CodeFilter) => + filter.every(f => + typeof f === "string" + ? s.includes(f) + : f.test(s) + ); + export const filters = { - byProps: (...props: string[]): FilterFn => + byProps: (...props: PropsFilter): FilterFn => props.length === 1 ? m => m[props[0]] !== void 0 : m => props.every(p => m[p] !== void 0), - byCode: (...code: string[]): FilterFn => m => { - if (typeof m !== "function") return false; - const s = Function.prototype.toString.call(m); - for (const c of code) { - if (!s.includes(c)) return false; - } - return true; + byCode: (...code: CodeFilter): FilterFn => { + code = code.map(canonicalizeMatch); + return m => { + if (typeof m !== "function") return false; + return stringMatches(Function.prototype.toString.call(m), code); + }; }, - byStoreName: (name: string): FilterFn => m => + byStoreName: (name: StoreNameFilter): FilterFn => m => m.constructor?.displayName === name, - componentByCode: (...code: string[]): FilterFn => { + componentByCode: (...code: CodeFilter): FilterFn => { const filter = filters.byCode(...code); return m => { if (filter(m)) return true; if (!m.$$typeof) return false; - if (m.type && m.type.render) return filter(m.type.render); // memo + forwardRef - if (m.type) return filter(m.type); // memos - if (m.render) return filter(m.render); // forwardRefs + if (m.type) + return m.type.render + ? filter(m.type.render) // memo + forwardRef + : filter(m.type); // memo + if (m.render) return filter(m.render); // forwardRef return false; }; } @@ -245,15 +257,9 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns * Find the id of the first module factory that includes all the given code * @returns string or null */ -export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: string[]) { - outer: +export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: CodeFilter) { for (const id in wreq.m) { - const str = wreq.m[id].toString(); - - for (const c of code) { - if (!str.includes(c)) continue outer; - } - return id; + if (stringMatches(wreq.m[id].toString(), code)) return id; } const err = new Error("Didn't find module with code(s):\n" + code.join("\n")); @@ -272,7 +278,7 @@ export const findModuleId = traceFunction("findModuleId", function findModuleId( * Find the first module factory that includes all the given code * @returns The module factory or null */ -export function findModuleFactory(...code: string[]) { +export function findModuleFactory(...code: CodeFilter) { const id = findModuleId(...code); if (!id) return null; @@ -325,7 +331,7 @@ export function findLazy(filter: FilterFn) { /** * Find the first module that has the specified properties */ -export function findByProps(...props: string[]) { +export function findByProps(...props: PropsFilter) { const res = find(filters.byProps(...props), { isIndirect: true }); if (!res) handleModuleNotFound("findByProps", ...props); @@ -335,7 +341,7 @@ export function findByProps(...props: string[]) { /** * Find the first module that has the specified properties, lazily */ -export function findByPropsLazy(...props: string[]) { +export function findByPropsLazy(...props: PropsFilter) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]); return proxyLazy(() => findByProps(...props)); @@ -344,7 +350,7 @@ export function findByPropsLazy(...props: string[]) { /** * Find the first function that includes all the given code */ -export function findByCode(...code: string[]) { +export function findByCode(...code: CodeFilter) { const res = find(filters.byCode(...code), { isIndirect: true }); if (!res) handleModuleNotFound("findByCode", ...code); @@ -354,7 +360,7 @@ export function findByCode(...code: string[]) { /** * Find the first function that includes all the given code, lazily */ -export function findByCodeLazy(...code: string[]) { +export function findByCodeLazy(...code: CodeFilter) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]); return proxyLazy(() => findByCode(...code)); @@ -363,7 +369,7 @@ export function findByCodeLazy(...code: string[]) { /** * Find a store by its displayName */ -export function findStore(name: string) { +export function findStore(name: StoreNameFilter) { const res = find(filters.byStoreName(name), { isIndirect: true }); if (!res) handleModuleNotFound("findStore", name); @@ -373,7 +379,7 @@ export function findStore(name: string) { /** * Find a store by its displayName, lazily */ -export function findStoreLazy(name: string) { +export function findStoreLazy(name: StoreNameFilter) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]); return proxyLazy(() => findStore(name)); @@ -382,7 +388,7 @@ export function findStoreLazy(name: string) { /** * Finds the component which includes all the given code. Checks for plain components, memos and forwardRefs */ -export function findComponentByCode(...code: string[]) { +export function findComponentByCode(...code: CodeFilter) { const res = find(filters.componentByCode(...code), { isIndirect: true }); if (!res) handleModuleNotFound("findComponentByCode", ...code); @@ -407,7 +413,7 @@ export function findComponentLazy(filter: FilterFn) { /** * Finds the first component that includes all the given code, lazily */ -export function findComponentByCodeLazy(...code: string[]) { +export function findComponentByCodeLazy(...code: CodeFilter) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]); return LazyComponent(() => { @@ -421,7 +427,7 @@ export function findComponentByCodeLazy(...code: string[ /** * Finds the first component that is exported by the first prop name, lazily */ -export function findExportedComponentLazy(...props: string[]) { +export function findExportedComponentLazy(...props: PropsFilter) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]); return LazyComponent(() => { @@ -445,10 +451,13 @@ export function findExportedComponentLazy(...props: stri * closeModal: filters.byCode("key==") * }) */ -export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string, mappers: Record): Record { +export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record): Record { + if (!Array.isArray(code)) code = [code]; + code = code.map(canonicalizeMatch); + const exports = {} as Record; - const id = findModuleId(code); + const id = findModuleId(...code); if (id === null) return exports; @@ -482,7 +491,7 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa * closeModal: filters.byCode("key==") * }) */ -export function mapMangledModuleLazy(code: string, mappers: Record): Record { +export function mapMangledModuleLazy(code: string | RegExp | CodeFilter, mappers: Record): Record { if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]); return proxyLazy(() => mapMangledModule(code, mappers)); @@ -497,7 +506,7 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g; * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory * @returns A promise that resolves with a boolean whether the chunks were loaded */ -export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) { +export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = DefaultExtractAndLoadChunksRegex) { const module = findModuleFactory(...code); if (!module) { const err = new Error("extractAndLoadChunks: Couldn't find module factory"); @@ -562,7 +571,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call */ -export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) { +export function extractAndLoadChunksLazy(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) { if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]); return makeLazy(() => extractAndLoadChunks(code, matcher)); @@ -572,7 +581,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac * Wait for a module that matches the provided filter to be registered, * then call the callback with the module as the first argument */ -export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { +export function waitFor(filter: string | PropsFilter | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); if (typeof filter === "string") @@ -593,21 +602,18 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback /** * Search modules by keyword. This searches the factory methods, * meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc - * @param filters One or more strings or regexes + * @param code One or more strings or regexes * @returns Mapping of found modules */ -export function search(...filters: Array) { +export function search(...code: CodeFilter) { const results = {} as Record; const factories = wreq.m; - outer: + for (const id in factories) { const factory = factories[id].original ?? factories[id]; - const str: string = factory.toString(); - for (const filter of filters) { - if (typeof filter === "string" && !str.includes(filter)) continue outer; - if (filter instanceof RegExp && !filter.test(str)) continue outer; - } - results[id] = factory; + + if (stringMatches(factory.toString(), code)) + results[id] = factory; } return results; From c8356570a370757984fb93e7a1a75b909df17e42 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:15:45 -0300 Subject: [PATCH 124/125] crazy --- src/webpack/patchWebpack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index e7201ea3f0..709104f896 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -95,7 +95,7 @@ define(Function.prototype, "m", { const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); /* If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype - define(this, "m", { value: Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler)) }); + Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler)); */ define(this, "m", { value: proxiedModuleFactories }); From 8fa19656ae49cb89c3031908569611484241cad9 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:07:50 -0300 Subject: [PATCH 125/125] fix grammar --- src/webpack/patchWebpack.ts | 2 +- src/webpack/wreq.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 709104f896..18c08b2332 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -157,7 +157,7 @@ function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id } if (existingFactory != null) { - // If existingFactory exists in any Webpack instance, its either wrapped in defineModuleFactoryGetter, or it has already been required. + // If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required. // So define the descriptor of it on this current Webpack instance, call Reflect.set with the new original, // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) diff --git a/src/webpack/wreq.d.ts b/src/webpack/wreq.d.ts index 6dd40b84cb..f865c4b6d7 100644 --- a/src/webpack/wreq.d.ts +++ b/src/webpack/wreq.d.ts @@ -140,7 +140,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { * Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded. */ e: (this: WebpackRequire, chunkId: PropertyKey) => Promise; - /** Get the filename name for the css part of a chunk */ + /** Get the filename for the css part of a chunk */ k: (this: WebpackRequire, chunkId: PropertyKey) => string; /** Get the filename for the js part of a chunk */ u: (this: WebpackRequire, chunkId: PropertyKey) => string;