Fix patcher and reporter for latest Discord changes
This commit is contained in:
parent
f749baf81e
commit
b8dc0ae720
7 changed files with 68 additions and 50 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue