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