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 01/11] 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 19800e701..2e8ece8f1 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 02/11] 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 2e8ece8f1..ab1f43c53 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 03/11] 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 ab1f43c53..4ac85a1cc 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 04/11] 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 4ac85a1cc..38038907a 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 05/11] 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 38038907a..85f2c9ab7 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 06/11] 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 85f2c9ab7..e155f20cd 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 07/11] 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 e155f20cd..a491fc4af 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 08/11] 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 a491fc4af..6457fcc81 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 09/11] 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 6457fcc81..3dccb629d 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 10/11] Document patchWebpack better --- src/webpack/patchWebpack.ts | 187 +++++++++++++++++++++--------------- src/webpack/wreq.d.ts | 10 +- 2 files changed, 114 insertions(+), 83 deletions(-) diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index efebaf6bb..719cd4020 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -24,94 +24,27 @@ const logger = new Logger("WebpackInterceptor", "#8caaee"); /** A set with all the module factories objects */ const allModuleFactories = new Set(); - -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; - } - } - }); - } -} - -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))) { - 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); - } - - 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); - - // Modules are only patched once, so we need to set the patched factory on all the modules - for (const moduleFactories of allModuleFactories) { - Reflect.defineProperty(moduleFactories, p, { - value: patchedFactory, - configurable: true, - enumerable: true, - writable: true - }); - } - - return true; - } -}; +/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ +let wreqFallbackApplied = false; // 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 +// 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 + // 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); + // 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, @@ -130,15 +63,20 @@ Reflect.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(() => 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 we have eagerPatches enabled we have to patch the pre-populated factories 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]); } } @@ -149,6 +87,8 @@ Reflect.defineProperty(Function.prototype, "m", { 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); } @@ -161,8 +101,95 @@ Reflect.defineProperty(Function.prototype, "m", { } }); -let wreqFallbackApplied = false; +/** + * 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, + enumerable: true, + get() { + // $$vencordOriginal means the factory is already patched + if (factory.$$vencordOriginal != null) { + return factory; + } + + return (factory = patchFactory(id, factory)); + }, + set(v: ModuleFactory) { + if (factory.$$vencordOriginal != null) { + factory.$$vencordOriginal = v; + } else { + factory = v; + } + } + }); + } +} + +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))) { + 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); + } + + // eagerPatches are disabled, so set up the getter for the patched version + 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. + // 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 + }); + } + + return true; + } +}; +/** + * 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 3dccb629d..de392668b 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 11/11] 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 719cd4020..b29452a56 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 */