Merge branch 'modules-proxy-patches' into immediate-finds-modules-proxy

This commit is contained in:
Nuckyz 2024-05-24 05:17:17 -03:00
commit ebae259f5d
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
3 changed files with 87 additions and 137 deletions

View file

@ -327,8 +327,51 @@ async function runtime(token: string) {
// Enable eagerPatches to make all patches apply regardless of the module being required // Enable eagerPatches to make all patches apply regardless of the module being required
Vencord.Settings.eagerPatches = true; Vencord.Settings.eagerPatches = true;
let wreq: typeof Vencord.Webpack.wreq; // The main patch for starting the reporter chunk loading
Vencord.Plugins.patches.push({
plugin: "Vencord Reporter",
find: '"Could not find app-mount"',
replacement: [{
match: /(?<="use strict";)/,
replace: "Vencord.Webpack._initReporter();"
}]
});
Vencord.Webpack.waitFor(
Vencord.Webpack.filters.byProps("loginToken"),
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
// @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(() => {
console.log("[PUP_DEBUG]", "Loading all chunks...");
Vencord.Webpack.factoryListeners.add(factory => {
// setImmediate to avoid blocking the factory patching execution while checking for lazy chunks
setTimeout(() => {
let isResolved = false;
searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}, 0);
});
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
}, 0);
};
const wreq = Vencord.Util.proxyLazy(() => Vencord.Webpack.wreq);
const { canonicalizeMatch, Logger } = Vencord.Util; const { canonicalizeMatch, Logger } = Vencord.Util;
const validChunks = new Set<string>(); const validChunks = new Set<string>();
@ -426,43 +469,7 @@ async function runtime(token: string) {
}, 0); }, 0);
} }
Vencord.Webpack.waitFor(
Vencord.Webpack.filters.byProps("loginToken"),
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
console.log("[PUP_DEBUG]", "Loading all chunks...");
wreq = webpackRequire;
Vencord.Webpack.factoryListeners.add(factory => {
// setImmediate to avoid blocking the factory patching execution while checking for lazy chunks
setTimeout(() => {
let isResolved = false;
searchAndLoadLazyChunks(String(factory)).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}, 0);
});
// setImmediate to only search the initial factories after Discord initialized the app
// our beforeInitListeners are called before Discord initializes the app
setTimeout(() => {
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(String(wreq.m[factoryId])).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
}, 0);
});
await chunksSearchingDone; await chunksSearchingDone;
wreq = wreq!;
// Require deferred entry points // Require deferred entry points
for (const deferredRequire of deferredRequires) { for (const deferredRequire of deferredRequires) {

View file

@ -6,24 +6,33 @@
import { Settings } from "@api/Settings"; import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { PatchReplacement } from "@utils/types";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { patches } from "../plugins"; import { patches } from "../plugins";
import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, OnChunksLoaded, waitForSubscriptions, WebpackRequire, wreq } from "."; import { _initWebpack, factoryListeners, ModuleFactory, moduleListeners, waitForSubscriptions, WebpackRequire, wreq } from ".";
type PatchedModuleFactory = ModuleFactory & {
$$vencordOriginal?: ModuleFactory;
};
type PatchedModuleFactories = Record<PropertyKey, PatchedModuleFactory> & {
[Symbol.toStringTag]?: "ModuleFactories";
};
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
/** A set with all the module factories objects */ /** A set with all the module factories objects */
const allModuleFactories = new Set<WebpackRequire["m"]>(); const allModuleFactories = new Set<PatchedModuleFactories>();
function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: PropertyKey, factory: ModuleFactory) { function defineModuleFactoryGetter(modulesFactories: PatchedModuleFactories, id: PropertyKey, factory: PatchedModuleFactory) {
Object.defineProperty(modulesFactories, id, { Object.defineProperty(modulesFactories, id, {
configurable: true,
enumerable: true,
get() { get() {
// $$vencordOriginal means the factory is already patched // $$vencordOriginal means the factory is already patched
// @ts-ignore
if (factory.$$vencordOriginal != null) { if (factory.$$vencordOriginal != null) {
return factory; return factory;
} }
@ -32,20 +41,16 @@ function defineModuleFactoryGetter(modulesFactories: WebpackRequire["m"], id: Pr
return (factory = patchFactory(id, factory)); return (factory = patchFactory(id, factory));
}, },
set(v: ModuleFactory) { set(v: ModuleFactory) {
// @ts-ignore
if (factory.$$vencordOriginal != null) { if (factory.$$vencordOriginal != null) {
// @ts-ignore
factory.$$vencordOriginal = v; factory.$$vencordOriginal = v;
} else { } else {
factory = v; factory = v;
} }
}, }
configurable: true,
enumerable: true
}); });
} }
const moduleFactoriesHandler: ProxyHandler<WebpackRequire["m"]> = { const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
set: (target, p, newValue, receiver) => { set: (target, p, newValue, receiver) => {
// If the property is not a number, we are not dealing with a module factory // If the property is not a number, we are not dealing with a module factory
if (Number.isNaN(Number(p))) { if (Number.isNaN(Number(p))) {
@ -66,9 +71,7 @@ const moduleFactoriesHandler: ProxyHandler<WebpackRequire["m"]> = {
} }
// Check if this factory is already patched // Check if this factory is already patched
// @ts-ignore
if (existingFactory?.$$vencordOriginal != null) { if (existingFactory?.$$vencordOriginal != null) {
// @ts-ignore
existingFactory.$$vencordOriginal = newValue; existingFactory.$$vencordOriginal = newValue;
return true; return true;
} }
@ -89,84 +92,6 @@ const moduleFactoriesHandler: ProxyHandler<WebpackRequire["m"]> = {
} }
}; };
// wreq.O is the webpack onChunksLoaded function
// Discord uses it to await for all the chunks to be loaded before initializing the app
// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it
Object.defineProperty(Function.prototype, "O", {
configurable: true,
set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) {
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
// This ensures we actually got the right one
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
const { stack } = new Error();
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && String(this.e).includes("Promise.all")) {
logger.info("Found main WebpackRequire.onChunksLoaded");
delete (Function.prototype as any).O;
const originalOnChunksLoaded = onChunksLoaded;
onChunksLoaded = function (result, chunkIds, callback, priority) {
if (callback != null && initCallbackRegex.test(String(callback))) {
Object.defineProperty(this, "O", {
value: originalOnChunksLoaded,
configurable: true,
enumerable: true,
writable: true
});
const wreq = this;
const originalCallback = callback;
callback = function (this: unknown) {
logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners");
_initWebpack(wreq);
for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(wreq);
}
originalCallback.apply(this, arguments as any);
};
callback.toString = originalCallback.toString.bind(originalCallback);
arguments[2] = callback;
}
originalOnChunksLoaded.apply(this, arguments as any);
} as WebpackRequire["O"];
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
// Returns whether a chunk has been loaded
Object.defineProperty(onChunksLoaded, "j", {
configurable: true,
set(v: OnChunksLoaded["j"]) {
function setValue(target: any) {
Object.defineProperty(target, "j", {
value: v,
configurable: true,
enumerable: true,
writable: true
});
}
setValue(onChunksLoaded);
setValue(originalOnChunksLoaded);
}
});
}
Object.defineProperty(this, "O", {
value: onChunksLoaded,
configurable: true,
enumerable: true,
writable: true
});
}
});
// wreq.m is the webpack object containing module factories. // wreq.m is the webpack object containing module factories.
// This is pre-populated with module factories, and is also populated via webpackGlobal.push // This is pre-populated with module factories, and is also populated via webpackGlobal.push
// The sentry module also has their own webpack with a pre-populated module factories object, so this also targets that // The sentry module also has their own webpack with a pre-populated module factories object, so this also targets that
@ -174,13 +99,35 @@ Object.defineProperty(Function.prototype, "O", {
Object.defineProperty(Function.prototype, "m", { Object.defineProperty(Function.prototype, "m", {
configurable: true, configurable: true,
set(this: WebpackRequire, moduleFactories: WebpackRequire["m"]) { set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) {
// When using react devtools or other extensions, we may also catch their webpack here. // When using React DevTools or other extensions, we may also catch their Webpack here.
// This ensures we actually got the right one // This ensures we actually got the right ones
const { stack } = new Error(); const { stack } = new Error();
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) { if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) {
logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); logger.info("Found Webpack module factories", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
// setImmediate to clear this property setter if this is not the main Webpack
// If this is the main Webpack, wreq.m will always be set before the timeout runs
const setterTimeout = setTimeout(() => delete (this as Partial<WebpackRequire>).p, 0);
Object.defineProperty(this, "p", {
configurable: true,
set(this: WebpackRequire, v: WebpackRequire["p"]) {
if (v !== "/assets/") return;
logger.info("Main Webpack found, initializing internal references to WebpackRequire ");
_initWebpack(this);
clearTimeout(setterTimeout);
Object.defineProperty(this, "p", {
value: v,
configurable: true,
enumerable: true,
writable: true
});
}
});
for (const id in moduleFactories) { for (const id in moduleFactories) {
// If we have eagerPatches enabled we have to patch the pre-populated factories // If we have eagerPatches enabled we have to patch the pre-populated factories
if (Settings.eagerPatches) { if (Settings.eagerPatches) {
@ -192,7 +139,6 @@ Object.defineProperty(Function.prototype, "m", {
allModuleFactories.add(moduleFactories); allModuleFactories.add(moduleFactories);
// @ts-ignore
moduleFactories[Symbol.toStringTag] = "ModuleFactories"; moduleFactories[Symbol.toStringTag] = "ModuleFactories";
moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler); moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler);
} }
@ -332,10 +278,9 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) {
if (!patch.all) patches.splice(i--, 1); if (!patch.all) patches.splice(i--, 1);
} }
const patchedFactory: ModuleFactory = (module, exports, require) => { const patchedFactory: PatchedModuleFactory = (module, exports, require) => {
for (const moduleFactories of allModuleFactories) { for (const moduleFactories of allModuleFactories) {
Object.defineProperty(moduleFactories, id, { Object.defineProperty(moduleFactories, id, {
// @ts-ignore
value: patchedFactory.$$vencordOriginal, value: patchedFactory.$$vencordOriginal,
configurable: true, configurable: true,
enumerable: true, enumerable: true,
@ -402,7 +347,6 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) {
}; };
patchedFactory.toString = originalFactory.toString.bind(originalFactory); patchedFactory.toString = originalFactory.toString.bind(originalFactory);
// @ts-ignore
patchedFactory.$$vencordOriginal = originalFactory; patchedFactory.$$vencordOriginal = originalFactory;
return patchedFactory; return patchedFactory;

View file

@ -83,7 +83,6 @@ export type ModCallbackFnWithId = (module: ModuleExports, id: PropertyKey) => vo
export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>(); export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>();
export const moduleListeners = new Set<ModCallbackFnWithId>(); export const moduleListeners = new Set<ModCallbackFnWithId>();
export const factoryListeners = new Set<(factory: ModuleFactory) => void>(); export const factoryListeners = new Set<(factory: ModuleFactory) => void>();
export const beforeInitListeners = new Set<(wreq: WebpackRequire) => void>();
export function _initWebpack(webpackRequire: WebpackRequire) { export function _initWebpack(webpackRequire: WebpackRequire) {
wreq = webpackRequire; wreq = webpackRequire;