Fix patcher and reporter for latest Discord changes

This commit is contained in:
Nuckyz 2026-04-23 20:33:57 -03:00
parent f749baf81e
commit b8dc0ae720
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
7 changed files with 68 additions and 50 deletions

View file

@ -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,

View file

@ -14,11 +14,11 @@ import pLimit from "p-limit";
function getWebpackChunkMap() {
const sym = Symbol();
let v: Record<PropertyKey, string> | 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<PropertyKey, string> | 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);

View file

@ -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");

View file

@ -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);
});

View file

@ -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<AnyWebpackRequire["m"]> = {
},
*/
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<MaybePatchedModuleFactory> = {
apply(target, thisArg: unknown, argArray: Parameters<AnyModuleFactory>) {
// 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<MaybePatchedModuleFactory> = {
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<MaybePatchedModuleFactory> = {
}
};
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;

View file

@ -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<Omit<WebpackRequire, "m">> & {
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
m: Record<PropertyKey, AnyModuleFactory>;
m: AnyModuleFactories;
};
export type AnyModuleFactories = Record<PropertyKey, AnyModuleFactory> & {
[SYM_ORIGINAL_MODULE_FACTORIES]?: Record<PropertyKey, AnyModuleFactory>;
};
/** exports can be anything, however initially it is always an empty object */

View file

@ -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)));