diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index bb5c39ac7..89632addf 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 2f1deafb0..2e53a544d 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,84 +92,6 @@ 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", { - 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 - 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"); - - delete (Function.prototype as any).O; - - const originalOnChunksLoaded = onChunksLoaded; - onChunksLoaded = function (result, chunkIds, callback, priority) { - if (callback != null && initCallbackRegex.test(String(callback))) { - Object.defineProperty(this, "O", { - value: originalOnChunksLoaded, - 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 @@ -174,13 +99,35 @@ Object.defineProperty(Function.prototype, "O", { 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 + 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)) { logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); + // 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, + + 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 + }); + } + }); + for (const id in moduleFactories) { // If we have eagerPatches enabled we have to patch the pre-populated factories if (Settings.eagerPatches) { @@ -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 adc88ef83..3003c6bd8 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;