2024-05-31 23:28:58 -03:00
|
|
|
/*
|
|
|
|
* Vencord, a Discord client mod
|
|
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
*/
|
|
|
|
|
2024-05-31 23:57:07 -03:00
|
|
|
import { Settings } from "@api/Settings";
|
2024-05-31 23:28:58 -03:00
|
|
|
import { Logger } from "@utils/Logger";
|
|
|
|
import { canonicalizeMatch } from "@utils/patches";
|
|
|
|
import * as Webpack from "@webpack";
|
|
|
|
import { wreq } from "@webpack";
|
2024-05-31 23:57:07 -03:00
|
|
|
import { addPatch, patches } from "plugins";
|
2024-05-31 23:28:58 -03:00
|
|
|
|
|
|
|
const ReporterLogger = new Logger("Reporter");
|
|
|
|
|
|
|
|
async function runReporter() {
|
|
|
|
ReporterLogger.log("Starting test...");
|
|
|
|
|
|
|
|
try {
|
2024-05-31 23:57:07 -03:00
|
|
|
// Enable eagerPatches to make all patches apply regardless of the module being required
|
|
|
|
Settings.eagerPatches = true;
|
|
|
|
|
|
|
|
// 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(() => {
|
|
|
|
ReporterLogger.log("Loading all chunks...");
|
|
|
|
|
|
|
|
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)
|
|
|
|
.catch(() => isResolved = true);
|
|
|
|
|
|
|
|
chunksSearchPromises.push(() => isResolved);
|
|
|
|
}, 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
for (const factoryId in wreq.m) {
|
|
|
|
let isResolved = false;
|
|
|
|
searchAndLoadLazyChunks(String(wreq.m[factoryId]))
|
|
|
|
.then(() => isResolved = true)
|
|
|
|
.catch(() => isResolved = true);
|
|
|
|
|
|
|
|
chunksSearchPromises.push(() => isResolved);
|
|
|
|
}
|
|
|
|
}, 0);
|
|
|
|
};
|
|
|
|
|
2024-05-31 23:28:58 -03:00
|
|
|
const validChunks = new Set<string>();
|
|
|
|
const invalidChunks = new Set<string>();
|
|
|
|
const deferredRequires = new Set<string>();
|
|
|
|
|
|
|
|
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
|
|
|
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
|
|
|
|
|
|
|
// True if resolved, false otherwise
|
|
|
|
const chunksSearchPromises = [] as Array<() => boolean>;
|
|
|
|
|
|
|
|
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);
|
|
|
|
|
|
|
|
async function searchAndLoadLazyChunks(factoryCode: string) {
|
|
|
|
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
|
|
|
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();
|
|
|
|
|
|
|
|
// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
|
|
|
|
// the chunk containing the component
|
|
|
|
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");
|
|
|
|
|
|
|
|
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
|
|
|
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => m[1]) : [];
|
|
|
|
|
|
|
|
if (chunkIds.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let invalidChunkGroup = false;
|
|
|
|
|
|
|
|
for (const id of chunkIds) {
|
|
|
|
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
|
|
|
|
|
|
|
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
|
|
.then(r => r.text())
|
|
|
|
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
|
|
|
|
|
|
if (isWasm && IS_WEB) {
|
|
|
|
invalidChunks.add(id);
|
|
|
|
invalidChunkGroup = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
validChunks.add(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!invalidChunkGroup) {
|
|
|
|
validChunkGroups.add([chunkIds, entryPoint]);
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
// Loads all found valid chunk groups
|
|
|
|
await Promise.all(
|
|
|
|
Array.from(validChunkGroups)
|
|
|
|
.map(([chunkIds]) =>
|
2024-05-31 23:57:07 -03:00
|
|
|
Promise.all(chunkIds.map(id => wreq.e(id)))
|
2024-05-31 23:28:58 -03:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Requires the entry points for all valid chunk groups
|
|
|
|
for (const [, entryPoint] of validChunkGroups) {
|
|
|
|
try {
|
|
|
|
if (shouldForceDefer) {
|
|
|
|
deferredRequires.add(entryPoint);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2024-05-31 23:57:07 -03:00
|
|
|
if (wreq.m[entryPoint]) wreq(entryPoint);
|
2024-05-31 23:28:58 -03:00
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// setImmediate to only check if all chunks were loaded after this function resolves
|
|
|
|
// We check if all chunks were loaded every time a factory is loaded
|
|
|
|
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
|
|
|
|
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
|
|
|
|
setTimeout(() => {
|
|
|
|
let allResolved = true;
|
|
|
|
|
|
|
|
for (let i = 0; i < chunksSearchPromises.length; i++) {
|
|
|
|
const isResolved = chunksSearchPromises[i]();
|
|
|
|
|
|
|
|
if (isResolved) {
|
|
|
|
// Remove finished promises to avoid having to iterate through a huge array everytime
|
|
|
|
chunksSearchPromises.splice(i--, 1);
|
|
|
|
} else {
|
|
|
|
allResolved = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (allResolved) chunksSearchingResolve();
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
await chunksSearchingDone;
|
|
|
|
|
|
|
|
// Require deferred entry points
|
|
|
|
for (const deferredRequire of deferredRequires) {
|
2024-05-31 23:57:07 -03:00
|
|
|
wreq(deferredRequire as any);
|
2024-05-31 23:28:58 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
|
|
|
const allChunks = [] as string[];
|
|
|
|
|
|
|
|
// Matches "id" or id:
|
2024-05-31 23:57:07 -03:00
|
|
|
for (const currentMatch of String(wreq.u).matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
2024-05-31 23:28:58 -03:00
|
|
|
const id = currentMatch[1] ?? currentMatch[2];
|
|
|
|
if (id == null) continue;
|
|
|
|
|
|
|
|
allChunks.push(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
|
|
|
|
|
|
|
|
// Chunks that are not loaded (not used) by Discord code anymore
|
|
|
|
const chunksLeft = allChunks.filter(id => {
|
|
|
|
return !(validChunks.has(id) || invalidChunks.has(id));
|
|
|
|
});
|
|
|
|
|
|
|
|
await Promise.all(chunksLeft.map(async id => {
|
|
|
|
const isWasm = await fetch(wreq.p + wreq.u(id))
|
|
|
|
.then(r => r.text())
|
|
|
|
.then(t => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
|
|
|
|
|
|
// Loads and requires a chunk
|
|
|
|
if (!isWasm) {
|
2024-05-31 23:57:07 -03:00
|
|
|
await wreq.e(id);
|
|
|
|
if (wreq.m[id]) wreq(id);
|
2024-05-31 23:28:58 -03:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
|
|
|
ReporterLogger.log("Finished loading all chunks!");
|
|
|
|
|
|
|
|
for (const patch of patches) {
|
|
|
|
if (!patch.all) {
|
|
|
|
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
|
|
|
|
let method = searchType;
|
|
|
|
|
|
|
|
if (searchType === "findComponent") method = "find";
|
|
|
|
if (searchType === "findExportedComponent") method = "findByProps";
|
|
|
|
if (searchType === "waitFor" || searchType === "waitForComponent") {
|
|
|
|
if (typeof args[0] === "string") method = "findByProps";
|
|
|
|
else method = "find";
|
|
|
|
}
|
|
|
|
if (searchType === "waitForStore") method = "findStore";
|
|
|
|
|
|
|
|
try {
|
|
|
|
let result: any;
|
|
|
|
|
|
|
|
if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
|
|
|
|
const [factory] = args;
|
|
|
|
result = factory();
|
|
|
|
} else if (method === "extractAndLoadChunks") {
|
|
|
|
const [code, matcher] = args;
|
|
|
|
|
|
|
|
result = await Webpack.extractAndLoadChunks(code, matcher);
|
|
|
|
if (result === false) result = null;
|
|
|
|
} else {
|
|
|
|
// @ts-ignore
|
|
|
|
result = Webpack[method](...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
|
|
|
} catch (e) {
|
|
|
|
let logMessage = searchType;
|
2024-05-31 23:57:07 -03:00
|
|
|
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])})`;
|
2024-05-31 23:28:58 -03:00
|
|
|
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
|
|
|
|
|
|
|
ReporterLogger.log("Webpack Find Fail:", logMessage);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ReporterLogger.log("Finished test");
|
|
|
|
} catch (e) {
|
|
|
|
ReporterLogger.log("A fatal error occurred:", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
runReporter();
|