diff --git a/src/Vencord.ts b/src/Vencord.ts index c4c6d47058..5de0729825 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -23,10 +23,10 @@ export * as Util from "./utils"; export * as QuickCss from "./utils/quickCss"; export * as Updater from "./utils/updater"; export * as Webpack from "./webpack"; +export * as WebpackPatcher from "./webpack/patchWebpack"; export { PlainSettings, Settings }; import "./utils/quickCss"; -import "./webpack/patchWebpack"; import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab"; import { StartAt } from "@utils/types"; diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 88337a917d..d7c3a2143b 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: IS_REPORTER, enabledThemes: [], enableReactDevtools: false, frameless: false, diff --git a/src/components/VencordSettings/PatchHelperTab.tsx b/src/components/VencordSettings/PatchHelperTab.tsx index e09a1dbf37..f9204669b2 100644 --- a/src/components/VencordSettings/PatchHelperTab.tsx +++ b/src/components/VencordSettings/PatchHelperTab.tsx @@ -56,7 +56,7 @@ function ReplacementComponent({ module, match, replacement, setReplacementError const [compileResult, setCompileResult] = React.useState<[boolean, string]>(); const [patchedCode, matchResult, diff] = React.useMemo(() => { - const src: string = fact.toString().replaceAll("\n", ""); + const src: string = String(fact).replaceAll("\n", ""); try { new RegExp(match); diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index d3484bd9a3..a8e005250f 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -8,6 +8,7 @@ import { Logger } from "@utils/Logger"; import { canonicalizeMatch } from "@utils/patches"; import * as Webpack from "@webpack"; import { wreq } from "@webpack"; +import { AnyModuleFactory, ModuleFactory } from "webpack"; const LazyChunkLoaderLogger = new Logger("LazyChunkLoader"); @@ -69,7 +70,7 @@ export async function loadLazyChunks() { 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))) ) ); @@ -81,7 +82,7 @@ export async function loadLazyChunks() { continue; } - if (wreq.m[entryPoint]) wreq(entryPoint as any); + if (wreq.m[entryPoint]) wreq(entryPoint); } catch (err) { console.error(err); } @@ -109,32 +110,33 @@ export async function loadLazyChunks() { }, 0); } - Webpack.factoryListeners.add(factory => { + function factoryListener(factory: AnyModuleFactory | ModuleFactory) { let isResolved = false; - searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + searchAndLoadLazyChunks(String(factory)) + .then(() => isResolved = true) + .catch(() => isResolved = true); chunksSearchPromises.push(() => isResolved); - }); + } + Webpack.factoryListeners.add(factoryListener); for (const factoryId in wreq.m) { - let isResolved = false; - searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); - - chunksSearchPromises.push(() => isResolved); + factoryListener(wreq.m[factoryId]); } await chunksSearchingDone; + Webpack.factoryListeners.delete(factoryListener); // Require deferred entry points for (const deferredRequire of deferredRequires) { - wreq!(deferredRequire as any); + wreq(deferredRequire); } // All chunks Discord has mapped to asset files, even if they are not used anymore const allChunks = [] as number[]; // Matches "id" or id: - for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)")|(?:([\deE]+?):)/g)) { + for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)")|(?:([\deE]+?):)/g)) { const id = currentMatch[1] ?? currentMatch[2]; if (id == null) continue; @@ -155,10 +157,10 @@ export async function loadLazyChunks() { // Loads and requires a chunk if (!isWorkerAsset) { - await wreq.e(id as any); + await wreq.e(id); // Technically, the id of the chunk does not match the entry point // But, still try it because we have no way to get the actual entry point - if (wreq.m[id]) wreq(id as any); + if (wreq.m[id]) wreq(id); } })); diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index ddd5e5f183..5290cc5512 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -6,7 +6,7 @@ import { Logger } from "@utils/Logger"; import * as Webpack from "@webpack"; -import { patches } from "plugins"; +import { addPatch, patches } from "plugins"; import { loadLazyChunks } from "./loadLazyChunks"; @@ -19,7 +19,24 @@ async function runReporter() { let loadLazyChunksResolve: (value: void | PromiseLike) => void; const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r); - Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve))); + // The main patch for starting the reporter chunk loading + addPatch({ + find: '"Could not find app-mount"', + replacement: { + match: /(?<="use strict";)/, + replace: "Vencord.Webpack._initReporter();" + } + }, "Vencord Reporter"); + + // @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(async () => { + loadLazyChunks().then(loadLazyChunksResolve); + }, 0); + }; + await loadLazyChunksDone; for (const patch of patches) { @@ -62,12 +79,12 @@ async function runReporter() { if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); } catch (e) { let logMessage = searchType; - if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; - else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${String(args[0]).slice(0, 147)}...)`; + else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${String(args[1])})`; else if (method === "mapMangledModule") { const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null); - logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`; + logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${String(args[1][mapping]).slice(0, 147)}...`).join(",\n")}\n})`; } else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; @@ -81,4 +98,5 @@ async function runReporter() { } } -runReporter(); +// Run after the Vencord object has been created +setTimeout(runReporter, 0); diff --git a/src/globals.d.ts b/src/globals.d.ts index e20ca4b71a..4456564ccb 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -64,13 +64,8 @@ declare global { export var Vesktop: any; export var VesktopNative: any; - interface Window { - webpackChunkdiscord_app: { - push(chunk: any): any; - pop(): any; - }; + interface Window extends Record { _: LoDashStatic; - [k: string]: any; } } diff --git a/src/plugins/_core/noTrack.ts b/src/plugins/_core/noTrack.ts index de1c205622..d030c65775 100644 --- a/src/plugins/_core/noTrack.ts +++ b/src/plugins/_core/noTrack.ts @@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; import definePlugin, { OptionType, StartAt } from "@utils/types"; +import { WebpackRequire } from "webpack"; const settings = definePluginSettings({ disableAnalytics: { @@ -81,9 +82,9 @@ export default definePlugin({ Object.defineProperty(Function.prototype, "g", { configurable: true, - set(v: any) { + set(this: WebpackRequire, globalObj: WebpackRequire["g"]) { Object.defineProperty(this, "g", { - value: v, + value: globalObj, configurable: true, enumerable: true, writable: true diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts index 2fdf873568..5703f7f539 100644 --- a/src/plugins/consoleShortcuts/index.ts +++ b/src/plugins/consoleShortcuts/index.ts @@ -80,10 +80,12 @@ function makeShortcuts() { wp: Webpack, wpc: { getter: () => Webpack.cache }, wreq: { getter: () => Webpack.wreq }, + WebpackInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances }, wpsearch: search, wpex: extract, wpexs: (code: string) => extract(findModuleId(code)!), loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); }, + filters, find, findAll: findAll, findByProps, diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index a495907b2d..d6a56fe691 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -160,7 +160,7 @@ function initWs(isManual = false) { return reply("Expected exactly one 'find' matches, found " + keys.length); const mod = candidates[keys[0]]; - let src = String(mod.original ?? mod).replaceAll("\n", ""); + let src = String(mod).replaceAll("\n", ""); if (src.startsWith("function(")) { src = "0," + src; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c399baafea..cf2887d755 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const SUPPORT_CHANNEL_ID = "1026515880080842772"; diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts index e46e44ad74..b86533d4d0 100644 --- a/src/utils/lazy.ts +++ b/src/utils/lazy.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { UNCONFIGURABLE_PROPERTIES } from "./misc"; + export function makeLazy(factory: () => T, attempts = 5): () => T { let tries = 0; let cache: T; @@ -29,10 +31,6 @@ export function makeLazy(factory: () => T, attempts = 5): () => T { }; } -// Proxies demand that these properties be unmodified, so proxyLazy -// will always return the function default for them. -const unconfigurable = ["arguments", "caller", "prototype"]; - const handler: ProxyHandler = {}; export const SYM_LAZY_GET = Symbol.for("vencord.lazy.get"); @@ -59,14 +57,14 @@ for (const method of [ handler.ownKeys = target => { const v = target[SYM_LAZY_GET](); const keys = Reflect.ownKeys(v); - for (const key of unconfigurable) { + for (const key of UNCONFIGURABLE_PROPERTIES) { if (!keys.includes(key)) keys.push(key); } return keys; }; handler.getOwnPropertyDescriptor = (target, p) => { - if (typeof p === "string" && unconfigurable.includes(p)) + if (typeof p === "string" && UNCONFIGURABLE_PROPERTIES.includes(p)) return Reflect.getOwnPropertyDescriptor(target, p); const descriptor = Reflect.getOwnPropertyDescriptor(target[SYM_LAZY_GET](), p); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 28c371c5b7..e4490ba659 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -100,6 +100,14 @@ export function pluralise(amount: number, singular: string, plural = singular + return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; } +/** Proxies which have an internal target but use a function as the main target require these properties to be unconfigurable */ +export const UNCONFIGURABLE_PROPERTIES = ["arguments", "caller", "prototype"]; + +export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) { + if (args.some(arg => arg == null)) return ""; + return strings.reduce((acc, str, i) => `${acc}${str}${args[i] ?? ""}`, ""); +} + export function tryOrElse(func: () => T, fallback: T): T { try { const res = func(); diff --git a/src/webpack/index.ts b/src/webpack/index.ts index 036c2a3fcb..6f1fd25b85 100644 --- a/src/webpack/index.ts +++ b/src/webpack/index.ts @@ -18,3 +18,4 @@ export * as Common from "./common"; export * from "./webpack"; +export * from "./wreq.d"; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index f32aeb7898..18c08b2332 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -1,190 +1,300 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated, Nuckyz, and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ -import { WEBPACK_CHUNK } from "@utils/constants"; +import { Settings } from "@api/Settings"; import { Logger } from "@utils/Logger"; +import { interpolateIfDefined } from "@utils/misc"; import { canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; -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, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, subscriptions, WebpackRequire, WrappedModuleFactory, wreq } from "."; const logger = new Logger("WebpackInterceptor", "#8caaee"); -let webpackChunk: any[]; +/** A set with all the Webpack instances */ +export const allWebpackInstances = new Set(); +/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */ +let wreqFallbackApplied = false; -// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed -// This way we can patch the factory of everything being pushed to the modules array -Object.defineProperty(window, WEBPACK_CHUNK, { - configurable: true, +type Define = typeof Reflect.defineProperty; +const define: Define = (target, p, attributes) => { + if (Object.hasOwn(attributes, "value")) { + attributes.writable = true; + } - get: () => webpackChunk, - set: v => { - if (v?.push) { - if (!v.push.$$vencordOriginal) { - logger.info(`Patching ${WEBPACK_CHUNK}.push`); - patchPush(v); + return Reflect.defineProperty(target, p, { + configurable: true, + enumerable: true, + ...attributes + }); +}; - // @ts-ignore - delete window[WEBPACK_CHUNK]; - window[WEBPACK_CHUNK] = v; - } - } +// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push +// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories. +// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions. - webpackChunk = v; - } -}); +// If this is the main Webpack, we also set up the internal references to WebpackRequire. +define(Function.prototype, "m", { + enumerable: false, -// wreq.m is the webpack module factory. -// normally, this is populated via webpackGlobal.push, which we patch below. -// However, Discord has their .m prepopulated. -// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories -Object.defineProperty(Function.prototype, "m", { - configurable: true, - - set(v: any) { - Object.defineProperty(this, "m", { - value: v, - configurable: true, - enumerable: true, - writable: true - }); + set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) { + define(this, "m", { value: originalModules }); - // When using react devtools or other extensions, we may also catch their webpack here. - // This ensures we actually got the right one + // We may also catch Discord bundled libs, React Devtools or other extensions WebpackInstance here. + // This ensures we actually got the right ones const { stack } = new Error(); - if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) { + if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || (stack != null ? /at \d+? \(/.test(stack) : true) || !String(this).includes("exports:{}")) { return; } - const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; - logger.info("Found Webpack module factory", fileName); + const fileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`); - patchFactories(v); + allWebpackInstances.add(this); // 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. - Object.defineProperty(this, "p", { - configurable: true, - - set(this: WebpackInstance, bundlePath: string) { - Object.defineProperty(this, "p", { - value: bundlePath, - configurable: true, - enumerable: true, - writable: true - }); + define(this, "p", { + enumerable: false, + set(this: WebpackRequire, bundlePath: WebpackRequire["p"]) { + define(this, "p", { value: bundlePath }); clearTimeout(setterTimeout); - if (bundlePath !== "/assets/") return; - logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); - _initWebpack(this); + if (window.GLOBAL_ENV?.PUBLIC_PATH != null && bundlePath !== window.GLOBAL_ENV.PUBLIC_PATH) return; - for (const beforeInitListener of beforeInitListeners) { - beforeInitListener(this); - } + logger.info("Main Webpack found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire"); + _initWebpack(this); } }); // setImmediate to clear this property setter if this is not the main Webpack. // If this is the main Webpack, wreq.p will always be set before the timeout runs. const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); + + // Patch the pre-populated factories + for (const id in originalModules) { + if (updateExistingFactory(originalModules, id, originalModules[id], true)) { + continue; + } + + notifyFactoryListeners(originalModules[id]); + defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]); + } + + define(originalModules, Symbol.toStringTag, { + value: "ModuleFactories", + enumerable: false + }); + + // The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions + const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler); + /* + If Discord ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype + Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler)); + */ + + define(this, "m", { value: proxiedModuleFactories }); } }); -function patchPush(webpackGlobal: any) { - function handlePush(chunk: any) { - try { - patchFactories(chunk[1]); - } catch (err) { - logger.error("Error in handlePush", err); +const moduleFactoriesHandler: ProxyHandler = { + /* + If Discord ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype + and that requires defining additional traps for keeping the object working + + // Proxies on the prototype dont intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined, + // to avoid Reflect.get having no effect and causing a stack overflow + get: (target, p, receiver) => { + return undefined; + }, + // Same thing as get + has: (target, p) => { + return false; + } + */ + + // The set trap for patching or defining getters for the module factories when new module factories are loaded + set: (target, p, newValue, receiver) => { + // If the property is not a number, we are not dealing with a module factory + if (Number.isNaN(Number(p))) { + return define(target, p, { value: newValue }); } - return handlePush.$$vencordOriginal.call(webpackGlobal, chunk); + if (updateExistingFactory(target, p, newValue)) { + return true; + } + + notifyFactoryListeners(newValue); + defineModulesFactoryGetter(p, Settings.eagerPatches ? wrapAndPatchFactory(p, newValue) : newValue); + + return true; } +}; - handlePush.$$vencordOriginal = webpackGlobal.push; - handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal); - // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` - // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. - // If we then repatched the new push, we would end up with recursive patching, which leads to our patches - // being applied multiple times. - // Thus, override bind to use the original push - handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args); +/** + * Update a factory that exists in any Webpack instance with a new original factory. + * + * @target The module factories where this new original factory is being set + * @param id The id of the module + * @param newFactory The new original factory + * @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget + * @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance + */ +function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) { + let existingFactory: TypedPropertyDescriptor | undefined; + for (const wreq of allWebpackInstances) { + if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue; + + if (Reflect.getOwnPropertyDescriptor(wreq.m, id) != null) { + existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id); + break; + } + } - Object.defineProperty(webpackGlobal, "push", { - configurable: true, + if (existingFactory != null) { + // If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required. + // So define the descriptor of it on this current Webpack instance, call Reflect.set with the new original, + // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter) + + Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory); + return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget); + } + + return false; +} - get: () => handlePush, - set(v) { - handlePush.$$vencordOriginal = v; +/** + * Notify all factory listeners. + * + * @param factory The original factory to notify for + */ +function notifyFactoryListeners(factory: AnyModuleFactory) { + for (const factoryListener of factoryListeners) { + try { + factoryListener(factory); + } catch (err) { + logger.error("Error in Webpack factory listener:\n", err, factoryListener); } - }); + } } -let webpackNotInitializedLogged = false; +/** + * Define the getter for returning the patched version of the module factory. + * + * If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original + * and only be patched when accessed for the first time. + * + * @param id The id of the module + * @param factory The original or patched module factory + */ +function defineModulesFactoryGetter(id: PropertyKey, factory: WrappedModuleFactory) { + // Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object + // have the patched version + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { + get() { + // $$vencordOriginal means the factory is already patched + if (factory.$$vencordOriginal != null) { + return factory; + } -function patchFactories(factories: Record void>) { - for (const id in factories) { - let mod = factories[id]; + return (factory = wrapAndPatchFactory(id, factory)); + }, + set(v: AnyModuleFactory) { + if (factory.$$vencordOriginal != null) { + factory.toString = v.toString.bind(v); + factory.$$vencordOriginal = v; + } else { + factory = v; + } + } + }); + } +} - const originalMod = mod; - const patchedBy = new Set(); +/** + * Wraps and patches a module 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 wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) { + const patchedFactory = patchFactory(id, originalFactory); + + // The patched factory wrapper, define it in an object to preserve the name after minification + const wrappedFactory: WrappedModuleFactory = { + PatchedFactory(...args: Parameters) { + // Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance + for (const wreq of allWebpackInstances) { + define(wreq.m, id, { value: wrappedFactory.$$vencordOriginal }); + } - const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) { - if (wreq == null && IS_DEV) { - if (!webpackNotInitializedLogged) { - webpackNotInitializedLogged = true; - logger.error("WebpackRequire was not initialized, running modules without patches instead."); + // eslint-disable-next-line prefer-const + let [module, exports, require] = args; + + if (wreq == null) { + if (!wreqFallbackApplied) { + wreqFallbackApplied = true; + + // Make sure the require argument is actually the WebpackRequire function + if (typeof require === "function" && require.m != null) { + const { stack } = new Error(); + const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1]; + logger.warn( + "WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" + + `id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` + + ")" + ); + _initWebpack(require as WebpackRequire); + } else if (IS_DEV) { + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + } } - return void originalMod(module, exports, require); + if (IS_DEV) { + return wrappedFactory.$$vencordOriginal!.apply(this, args); + } } + let factoryReturn: unknown; try { - mod(module, exports, require); + // Call the patched factory + factoryReturn = patchedFactory.apply(this, args); } catch (err) { - // Just rethrow discord errors - if (mod === originalMod) throw err; + // Just re-throw Discord errors + if (patchedFactory === originalFactory) { + throw err; + } - logger.error("Error in patched module", err); - return void originalMod(module, exports, require); + logger.error("Error in patched module factory:\n", err); + return wrappedFactory.$$vencordOriginal!.apply(this, args); } - exports = module.exports; - - if (!exports) return; + // Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it + exports = module?.exports; + if (exports == null) { + return factoryReturn; + } // There are (at the time of writing) 11 modules exporting the window // Make these non enumerable to improve webpack search performance - if (require.c) { + if (typeof require === "function" && require.c != null) { let foundWindow = false; if (exports === window) { foundWindow = true; } else if (typeof exports === "object") { - if (exports?.default === window) { + if (exports.default === window) { foundWindow = true; } else { - for (const nested in exports) if (nested.length <= 3) { - if (exports[nested] === window) { + for (const exportKey in exports) if (exportKey.length <= 3) { + if (exports[exportKey] === window) { foundWindow = true; } } @@ -199,7 +309,7 @@ function patchFactories(factories: Record string, original: any, (...args: any[]): void; }; - - factory.toString = originalMod.toString.bind(originalMod); - factory.original = originalMod; - for (const factoryListener of factoryListeners) { - try { - factoryListener(originalMod); - } catch (err) { - logger.error("Error in Webpack factory listener:\n", err, factoryListener); - } + return factoryReturn; } + }.PatchedFactory; - // Discords Webpack chunks for some ungodly reason contain random - // newlines. Cyn recommended this workaround and it seems to work fine, - // however this could potentially break code, so if anything goes weird, - // this is probably why. - // Additionally, `[actual newline]` is one less char than "\n", so if Discord - // ever targets newer browsers, the minifier could potentially use this trick and - // cause issues. - // - // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, - let code: string = "0," + mod.toString().replaceAll("\n", ""); + wrappedFactory.toString = originalFactory.toString.bind(originalFactory); + wrappedFactory.$$vencordOriginal = originalFactory; - for (let i = 0; i < patches.length; i++) { - const patch = patches[i]; + return wrappedFactory; +} - const moduleMatches = typeof patch.find === "string" - ? code.includes(patch.find) - : patch.find.test(code); +/** + * Patches a module factory. + * + * @param id The id of the module + * @param factory The original module factory + * @returns The patched module factory + */ +function patchFactory(id: PropertyKey, factory: AnyModuleFactory) { + // 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0, + let code: string = "0," + String(factory); + let patchedFactory = factory; - if (!moduleMatches) continue; + const patchedBy = new Set(); - patchedBy.add(patch.plugin); + for (let i = 0; i < patches.length; i++) { + const patch = patches[i]; - const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); - const previousMod = mod; - const previousCode = code; + const moduleMatches = typeof patch.find === "string" + ? code.includes(patch.find) + : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code)); - // We change all patch.replacement to array in plugins/index - for (const replacement of patch.replacement as PatchReplacement[]) { - const lastMod = mod; - const lastCode = code; + if (!moduleMatches) continue; - canonicalizeReplacement(replacement, patch.plugin); + patchedBy.add(patch.plugin); - try { - const newCode = executePatch(replacement.match, replacement.replace as string); - if (newCode === code) { - if (!patch.noWarn) { - logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); - if (IS_DEV) { - logger.debug("Function Source:\n", code); - } - } + const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); + const previousCode = code; + const previousFactory = factory; - if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); - mod = previousMod; - code = previousCode; - patchedBy.delete(patch.plugin); - break; - } + // We change all patch.replacement to array in plugins/index + for (const replacement of patch.replacement as PatchReplacement[]) { + const lastCode = code; + const lastFactory = factory; - continue; - } + canonicalizeReplacement(replacement, patch.plugin); - code = newCode; - mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); - } catch (err) { - logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); - - if (IS_DEV) { - const changeSize = code.length - lastCode.length; - const match = lastCode.match(replacement.match)!; - - // Use 200 surrounding characters of context - const start = Math.max(0, match.index! - 200); - const end = Math.min(lastCode.length, match.index! + match[0].length + 200); - // (changeSize may be negative) - const endPatched = end + changeSize; - - const context = lastCode.slice(start, end); - const patchedContext = code.slice(start, endPatched); - - // inline require to avoid including it in !IS_DEV builds - const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); - let fmt = "%c %s "; - const elements = [] as string[]; - for (const d of diff) { - const color = d.removed - ? "red" - : d.added - ? "lime" - : "grey"; - fmt += "%c%s"; - elements.push("color:" + color, d.value); + try { + 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 ${String(id)}): ${replacement.match}`); + if (IS_DEV) { + logger.debug("Function Source:\n", code); } - - logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); - logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); - const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); - logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); } - patchedBy.delete(patch.plugin); - if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); - mod = previousMod; + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); code = previousCode; + patchedFactory = previousFactory; + patchedBy.delete(patch.plugin); break; } - mod = lastMod; - code = lastCode; + continue; } - } - if (!patch.all) patches.splice(i--, 1); + code = newCode; + patchedFactory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`); + } catch (err) { + logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err); + + if (IS_DEV) { + const changeSize = code.length - lastCode.length; + const match = lastCode.match(replacement.match)!; + + // Use 200 surrounding characters of context + const start = Math.max(0, match.index! - 200); + const end = Math.min(lastCode.length, match.index! + match[0].length + 200); + // (changeSize may be negative) + const endPatched = end + changeSize; + + const context = lastCode.slice(start, end); + const patchedContext = code.slice(start, endPatched); + + // inline require to avoid including it in !IS_DEV builds + const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); + let fmt = "%c %s "; + const elements = [] as string[]; + for (const d of diff) { + const color = d.removed + ? "red" + : d.added + ? "lime" + : "grey"; + fmt += "%c%s"; + elements.push("color:" + color, d.value); + } + + logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); + logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); + const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); + logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); + } + + patchedBy.delete(patch.plugin); + + if (patch.group) { + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); + code = previousCode; + patchedFactory = previousFactory; + break; + } + + code = lastCode; + patchedFactory = lastFactory; + } } + + if (!patch.all) patches.splice(i--, 1); } + + return patchedFactory; } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 272ecd94fc..50baf4637d 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 { AnyModuleFactory, ModuleExports, ModuleFactory, WebpackRequire } from "./wreq"; const logger = new Logger("Webpack"); @@ -33,10 +33,10 @@ 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; +export type FilterFn = (module: ModuleExports) => boolean; type PropsFilter = Array; type CodeFilter = Array; @@ -80,16 +80,24 @@ 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: AnyModuleFactory) => void>(); -export function _initWebpack(webpackRequire: WebpackInstance) { +export function _initWebpack(webpackRequire: WebpackRequire) { wreq = webpackRequire; + + if (webpackRequire.c == null) return; cache = webpackRequire.c; + + Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, { + value: "ModuleCache", + configurable: true, + writable: true, + enumerable: false + }); } let devToolsOpen = false; @@ -151,7 +159,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.loaded || !mod?.exports) continue; @@ -197,7 +205,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) { @@ -259,7 +267,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns */ export const findModuleId = traceFunction("findModuleId", function findModuleId(...code: CodeFilter) { for (const id in wreq.m) { - if (stringMatches(wreq.m[id].toString(), code)) return id; + if (stringMatches(String(wreq.m[id]), code)) return id; } const err = new Error("Didn't find module with code(s):\n" + code.join("\n")); @@ -451,17 +459,17 @@ export function findExportedComponentLazy(...props: Prop * closeModal: filters.byCode("key==") * }) */ -export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record): Record { +export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule(code: string | RegExp | CodeFilter, mappers: Record): Record { if (!Array.isArray(code)) code = [code]; code = code.map(canonicalizeMatch); - const exports = {} as Record; + const exports = {} as Record; const id = findModuleId(...code); if (id === null) return exports; - const mod = wreq(id as any); + const mod = wreq(id); outer: for (const key in mod) { const member = mod[key]; @@ -519,7 +527,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D return false; } - const match = module.toString().match(canonicalizeMatch(matcher)); + const match = String(module).match(canonicalizeMatch(matcher)); if (!match) { const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code"); logger.warn(err, "Code:", code, "Matcher:", matcher); @@ -606,14 +614,15 @@ export function waitFor(filter: string | PropsFilter | FilterFn, callback: Callb * @returns Mapping of found modules */ export function search(...code: CodeFilter) { - const results = {} as Record; + const results: WebpackRequire["m"] = {}; const factories = wreq.m; for (const id in factories) { - const factory = factories[id].original ?? factories[id]; + const factory = factories[id]; - if (stringMatches(factory.toString(), code)) + if (stringMatches(String(factory), code)) { results[id] = factory; + } } return results; @@ -627,18 +636,18 @@ export function search(...code: CodeFilter) { * 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; - if (!mod) return null; +export function extract(id: PropertyKey) { + const factory = wreq.m[id]; + if (!factory) 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} +0,${String(factory)} +//# 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 new file mode 100644 index 0000000000..f865c4b6d7 --- /dev/null +++ b/src/webpack/wreq.d.ts @@ -0,0 +1,202 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated, Nuckyz and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export type ModuleExports = any; + +export type Module = { + id: PropertyKey; + loaded: boolean; + exports: ModuleExports; +}; + +/** 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; + +export type AsyncModulePromise = Promise & { + [WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any; + [WebpackExports]: ModuleExports; + [WebpackError]?: any; +}; + +export type AsyncModuleBody = ( + handleAsyncDependencies: (deps: AsyncModulePromise[]) => + Promise<() => ModuleExports[]> | (() => ModuleExports[]), + asyncResult: (error?: any) => void +) => Promise; + +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: (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: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise) => void, +}; + +export type ScriptLoadDone = (event: Event) => void; + +// export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & { +// /** Check if a chunk has been loaded */ +// j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean; +// }; + +export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & { + /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ + m: Record; + /** The module cache, where all modules which have been WebpackRequire'd are stored */ + c: Record; + // /** + // * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this: + // * @example + // * const fromObject = { a: 1 }; + // * Object.keys(fromObject).forEach(key => { + // * if (key !== "default" && !Object.hasOwn(toObject, key)) { + // * Object.defineProperty(toObject, key, { + // * get: () => fromObject[key], + // * enumerable: true + // * }); + // * } + // * }); + // * @returns fromObject + // */ + // es: (this: WebpackRequire, fromObject: Record, toObject: Record) => Record; + /** + * 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 an {@link AsyncModulePromise}. + * + * The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies: + * @example + * 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 */ + n: (this: WebpackRequire, 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: (this: WebpackRequire, value: any, mode: number) => any; + /** + * Define getter functions for harmony exports. For every prop in "definiton" (the module exports), set a getter in "exports" for the getter function in the "definition", like this: + * @example + * const exports = {}; + * const definition = { exportName: () => someExportedValue }; + * for (const key in definition) { + * if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) { + * Object.defineProperty(exports, key, { + * get: definition[key], + * enumerable: true + * }); + * } + * } + * // exports is now { exportName: someExportedValue } (but each value is actually a getter) + */ + d: (this: WebpackRequire, exports: Record, definiton: Record) => void; + /** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */ + f: ChunkHandlers; + /** + * 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: (this: WebpackRequire, chunkId: PropertyKey) => Promise; + /** Get the filename for the css part of a chunk */ + k: (this: WebpackRequire, chunkId: PropertyKey) => string; + /** Get the filename for the js part of a chunk */ + u: (this: WebpackRequire, chunkId: PropertyKey) => string; + /** The global object, will likely always be the 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 */ + 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: (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: ModuleExports) => void; + /** Node.js module decorator. Decorates a module as a Node.js module */ + nmd: (this: WebpackRequire, module: Module) => any; + // /** + // * Register deferred code which will be executed when the passed chunks are loaded. + // * + // * If chunkIds is defined, it defers the execution of the callback and returns undefined. + // * + // * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument. + // * + // * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code. + // * + // * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed. + // */ + // O: OnChunksLoaded; + /** + * 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; + /** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */ + p: string; + /** The runtime id of the current runtime */ + j: string; + /** Document baseURI or WebWorker location.href */ + b: string; +}; + +// Utility section for Vencord + +export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial> & { + /** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */ + m: Record; +}; + +/** exports can be anything, however initially it is always an empty object */ +export type AnyModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void; + +export type WrappedModuleFactory = AnyModuleFactory & { + $$vencordOriginal?: AnyModuleFactory; +}; + +export type WrappedModuleFactories = Record;