diff --git a/src/api/Settings.ts b/src/api/Settings.ts index 400295051..8ab14f97d 100644 --- a/src/api/Settings.ts +++ b/src/api/Settings.ts @@ -96,7 +96,7 @@ const DefaultSettings: Settings = { autoUpdateNotification: true, useQuickCss: true, themeLinks: [], - eagerPatches: IS_REPORTER, + eagerPatches: false, // Eagerly patching no longer works due to module factories with the same id being able to have different sources now. enabledThemes: [], enableReactDevtools: false, frameless: false, diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts index 997e69fa2..05a58ef1c 100644 --- a/src/debug/loadLazyChunks.ts +++ b/src/debug/loadLazyChunks.ts @@ -14,11 +14,11 @@ import pLimit from "p-limit"; function getWebpackChunkMap() { const sym = Symbol(); - let v: Record | null = null; + let chunksMap: unknown; Object.defineProperty(Object.prototype, sym, { get() { - v = this; + chunksMap = this; return ""; }, configurable: true @@ -27,7 +27,7 @@ function getWebpackChunkMap() { wreq.u(sym); delete Object.prototype[sym]; - return v; + return chunksMap as Record | null; } export async function loadLazyChunks() { @@ -66,7 +66,7 @@ export async function loadLazyChunks() { ?.matchAll(Webpack.ChunkIdsRegex) .map(m => { const numChunkId = Number(m[1]); - return Number.isNaN(numChunkId) ? m[1] : numChunkId; + return Number.isNaN(numChunkId) ? m[1] : String(numChunkId); }) .toArray() ?? []; @@ -107,7 +107,7 @@ export async function loadLazyChunks() { if (!invalidChunkGroup) { const numEntryPoint = Number(entryPoint); - validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]); + validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : String(numEntryPoint)]); } })); @@ -178,10 +178,10 @@ export async function loadLazyChunks() { } // All chunks Discord has mapped to asset files, even if they are not used anymore - const chunkMap = getWebpackChunkMap(); - if (!chunkMap) throw new Error("Failed to get chunk map"); + const chunksMap = getWebpackChunkMap(); + if (!chunksMap) throw new Error("Failed to get chunk map"); - const allChunks = Object.keys(chunkMap).map(id => Number.isNaN(Number(id)) ? id : Number(id)); + const allChunks = Object.keys(chunksMap); if (allChunks.length === 0) throw new Error("Failed to get all chunks"); // Chunks which our regex could not catch to load @@ -201,6 +201,13 @@ export async function loadLazyChunks() { } }))); + // Manually require all modules to make sure all lazily required modules are patched + for (const moduleId of Object.keys(wreq.m)) { + try { + wreq(moduleId); + } catch { } + } + LazyChunkLoaderLogger.log("Finished loading all chunks!"); } catch (e) { LazyChunkLoaderLogger.log("A fatal error occurred:", e); diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index 529123cd2..bcd178338 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -23,8 +23,8 @@ async function runReporter() { addPatch({ find: '"Could not find app-mount"', replacement: { - match: /(?<="use strict";)/, - replace: "Vencord.Webpack._initReporter();" + match: /"Could not find app-mount"/, + replace: "(Vencord.Webpack._initReporter(),$&)" } }, "Vencord Reporter"); diff --git a/src/webpack/common/react.ts b/src/webpack/common/react.ts index b8b0e7532..a6ca363c3 100644 --- a/src/webpack/common/react.ts +++ b/src/webpack/common/react.ts @@ -31,7 +31,7 @@ export const ReactDOM: typeof import("react-dom") = findByPropsLazy("createPorta // 299 is an error code used in createRoot and createPortal export const createRoot: typeof import("react-dom/client").createRoot = findByCodeLazy("(299));", ".onRecoverableError"); -waitFor("useState", m => { +waitFor(["createElement", "useEffect"], m => { React = m; ({ useEffect, useState, useLayoutEffect, useMemo, useRef, useReducer, useCallback } = React); }); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 99ded5e90..cc68751e7 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -17,6 +17,7 @@ import { _blacklistBadModules, _initWebpack, factoryListeners, findModuleFactory export const patches = [] as Patch[]; +export const SYM_ORIGINAL_MODULE_FACTORIES = Symbol("WebpackPatcher.originalModuleFactories"); export const SYM_IS_PROXIED_FACTORY = Symbol("WebpackPatcher.isProxiedFactory"); export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory"); export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource"); @@ -36,7 +37,7 @@ export const getBuildNumber = makeLazy(() => { return Number(buildNumberMatch[1]); } - const hardcodedFactoryStr = String(wreq.m[446023]); + const hardcodedFactoryStr = String(wreq.m[927815]); if (hardcodedFactoryStr.includes("Trying to open a changelog for an invalid build number")) { const hardcodedBuildNumber = matchBuildNumber(hardcodedFactoryStr); @@ -111,7 +112,7 @@ define(Function.prototype, "m", { // Currently, sentry and libdiscore Webpack instances are not meant to be patched. // As an extra measure, take advatange of the fact their files include the names and return early if it's one of them. // Later down we also include other measures to avoid patching them. - if (["sentry", "libdiscore"].some(name => fileName?.toLowerCase()?.includes(name))) { + if (["sentry", "libdiscore", "fast-connect"].some(name => fileName?.toLowerCase()?.includes(name))) { return; } @@ -128,7 +129,7 @@ define(Function.prototype, "m", { // libdiscore init Webpack instance always returns a constant string for the js filename of a chunk. // In that case, avoid patching this instance, // as it runs before the main Webpack instance and will make the WebpackRequire fallback not work properly, or init an wrongful main WebpackRequire. - if (bundlePath !== "/assets/" || /(?:=>|{return)"[^"]/.exec(String(this.u))) { + if (bundlePath !== "/assets/" || /(?:=>|{return)"[^"]+?"[^=]/.exec(String(this.u))) { return; } @@ -185,7 +186,7 @@ define(Function.prototype, "m", { // Proxy (and maybe patch) pre-populated factories for (const moduleId in originalModules) { - updateExistingOrProxyFactory(originalModules, moduleId, originalModules[moduleId], originalModules, true); + proxyFactoryAndUpdateExisting(originalModules, moduleId, originalModules[moduleId], originalModules, true); } define(originalModules, Symbol.toStringTag, { @@ -235,20 +236,30 @@ const moduleFactoriesHandler: ProxyHandler = { }, */ - set: updateExistingOrProxyFactory + get(target, p, receiver) { + if (p === SYM_ORIGINAL_MODULE_FACTORIES) { + return target; + } + + return Reflect.get(target, p, receiver); + }, + + set: proxyFactoryAndUpdateExisting }; // The proxy for patching lazily and/or running factories with our wrapper. const moduleFactoryHandler: ProxyHandler = { apply(target, thisArg: unknown, argArray: Parameters) { - // SYM_ORIGINAL_FACTORY means the factory has already been patched + // If SYM_ORIGINAL_FACTORY exists in the proxy target, it means the proxy target is already a patched factory. + // In that case, just run it. if (target[SYM_ORIGINAL_FACTORY] != null) { return runFactoryWithWrap(target as PatchedModuleFactory, thisArg, argArray); } - // SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id + // SAFETY: Factories have `name` as their key in the module factories object, and that is always their module id. const moduleId: string = target.name; + // Patch the proxy target, which is the original factory. const patchedFactory = patchFactory(moduleId, target); return runFactoryWithWrap(patchedFactory, thisArg, argArray); }, @@ -258,9 +269,10 @@ const moduleFactoryHandler: ProxyHandler = { return true; } + // If the proxy target is a patched factory, SYM_ORIGINAL_FACTORY will have the original factory. Otherwise the target itself will be it. const originalFactory: AnyModuleFactory = target[SYM_ORIGINAL_FACTORY] ?? target; - // Redirect these properties to the original factory, including making `toString` return the original factory `toString` + // Redirect getting these properties from the original factory, including making `toString` return the original factory `toString` if (p === "toString" || p === SYM_PATCHED_SOURCE || p === SYM_PATCHED_BY) { const v = Reflect.get(originalFactory, p, originalFactory); return p === "toString" ? v.bind(originalFactory) : v; @@ -270,62 +282,57 @@ const moduleFactoryHandler: ProxyHandler = { } }; -function updateExistingOrProxyFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) { - if (updateExistingFactory(moduleFactories, moduleId, newFactory, receiver, ignoreExistingInTarget)) { +function proxyFactoryAndUpdateExisting(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget = false) { + notifyFactoryListeners(moduleId, newFactory); + const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler); + + if (updateExistingFactory(moduleFactories, moduleId, newFactory, proxiedFactory, ignoreExistingInTarget)) { return true; } - notifyFactoryListeners(moduleId, newFactory); - - const proxiedFactory = new Proxy(Settings.eagerPatches ? patchFactory(moduleId, newFactory) : newFactory, moduleFactoryHandler); return Reflect.set(moduleFactories, moduleId, proxiedFactory, receiver); } /** - * Update a duplicated factory that exists in any of the Webpack instances we track with a new original factory. + * Update a duplicated factory that exists in any of the Webpack instances we track with a new original and proxied factory. * * @param moduleFactories The module factories where this new original factory is being set * @param moduleId The id of the module * @param newFactory The new original factory + * @param newProxiedFactory The new original and proxied factory * @param receiver The receiver of the factory * @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactories where it is being set * @returns Whether the original factory was updated, or false if it doesn't exist in any of the tracked Webpack instances */ -function updateExistingFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, receiver: any, ignoreExistingInTarget) { +function updateExistingFactory(moduleFactories: AnyWebpackRequire["m"], moduleId: PropertyKey, newFactory: AnyModuleFactory, newProxiedFactory: AnyModuleFactory, ignoreExistingInTarget: boolean) { let existingFactory: AnyModuleFactory | undefined; let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined; for (const wreq of allWebpackInstances) { - if (ignoreExistingInTarget && wreq.m === moduleFactories) { + const instanceModuleFactories = wreq.m[SYM_ORIGINAL_MODULE_FACTORIES] ?? wreq.m; + + if (ignoreExistingInTarget && instanceModuleFactories === moduleFactories) { continue; } if (Object.hasOwn(wreq.m, moduleId)) { - existingFactory = wreq.m[moduleId]; - moduleFactoriesWithFactory = wreq.m; + existingFactory = wreq.m[moduleId]; // Must use wreq.m here instead of instanceModuleFactories to use our proxy in case the module factories are already proxied. + moduleFactoriesWithFactory = instanceModuleFactories; break; } } if (existingFactory != null) { - // If existingFactory exists in any of the Webpack instances we track, it's either wrapped in our proxy, or it has already been required. - // In the case it is wrapped in our proxy, and the instance we are setting does not already have it, we need to make sure the instance contains our proxy too. - if (moduleFactoriesWithFactory !== moduleFactories && existingFactory[SYM_IS_PROXIED_FACTORY]) { - Reflect.set(moduleFactories, moduleId, existingFactory, receiver); - } - // Else, if it is not wrapped in our proxy, set this new original factory in all the instances - else { - defineInWebpackInstances(moduleId, newFactory); - } - - // Update existingFactory with the new original, if it does have a current original factory + // SYM_ORIGINAL_FACTORY means the factory has already been patched. In this case, only update existingFactory original factory to the new one. if (existingFactory[SYM_ORIGINAL_FACTORY] != null) { existingFactory[SYM_ORIGINAL_FACTORY] = newFactory; - } - // Persist patched source and patched by in the new original factory - if (IS_DEV) { - newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE]; - newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY]; + // Persist patched source and patched by in the new original factory + if (IS_DEV) { + newFactory[SYM_PATCHED_SOURCE] = existingFactory[SYM_PATCHED_SOURCE]; + newFactory[SYM_PATCHED_BY] = existingFactory[SYM_PATCHED_BY]; + } + } else { + defineInWebpackInstances(moduleId, newProxiedFactory); } return true; diff --git a/src/webpack/types.ts b/src/webpack/types.ts index 328aa8bc4..db6aa7c63 100644 --- a/src/webpack/types.ts +++ b/src/webpack/types.ts @@ -6,11 +6,15 @@ import { Module, ModuleExports, WebpackRequire } from "@vencord/discord-types/webpack"; -import { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack"; +import { SYM_ORIGINAL_FACTORY, SYM_ORIGINAL_MODULE_FACTORIES, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack"; 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; + m: AnyModuleFactories; +}; + +export type AnyModuleFactories = Record & { + [SYM_ORIGINAL_MODULE_FACTORIES]?: Record; }; /** exports can be anything, however initially it is always an empty object */ diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 4c7f0e92c..7e420575b 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -726,12 +726,12 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultEx } const numEntryPoint = Number(entryPointId); - const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : numEntryPoint; + const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : String(numEntryPoint); if (rawChunkIds) { const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map(m => { const numChunkId = Number(m[1]); - return Number.isNaN(numChunkId) ? m[1] : numChunkId; + return Number.isNaN(numChunkId) ? m[1] : String(numChunkId); }); await Promise.all(chunkIds.map(id => wreq.e(id)));