Make Reporter runnable in desktop
This commit is contained in:
parent
7ccd073506
commit
d07e4c71b5
|
@ -22,6 +22,7 @@
|
||||||
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
|
||||||
"buildWebStandalone": "pnpm buildWeb --standalone",
|
"buildWebStandalone": "pnpm buildWeb --standalone",
|
||||||
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
|
||||||
|
"buildReporterDesktop": "pnpm build --reporter",
|
||||||
"watch": "pnpm build --watch",
|
"watch": "pnpm build --watch",
|
||||||
"watchWeb": "pnpm buildWeb --watch",
|
"watchWeb": "pnpm buildWeb --watch",
|
||||||
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
"generatePluginJson": "tsx scripts/generatePluginList.ts",
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-fallthrough */
|
||||||
|
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
/// <reference types="../src/globals" />
|
/// <reference types="../src/globals" />
|
||||||
// eslint-disable-next-line spaced-comment
|
// eslint-disable-next-line spaced-comment
|
||||||
|
@ -40,10 +42,11 @@ const browser = await pup.launch({
|
||||||
|
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||||
|
await page.setBypassCSP(true);
|
||||||
|
|
||||||
function maybeGetError(handle: JSHandle) {
|
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
|
||||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
return await (handle as JSHandle<Error>)?.getProperty("message")
|
||||||
.then(m => m.jsonValue());
|
.then(m => m?.jsonValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
const report = {
|
const report = {
|
||||||
|
@ -59,6 +62,7 @@ const report = {
|
||||||
error: string;
|
error: string;
|
||||||
}[],
|
}[],
|
||||||
otherErrors: [] as string[],
|
otherErrors: [] as string[],
|
||||||
|
ignoredErrors: [] as string[],
|
||||||
badWebpackFinds: [] as string[]
|
badWebpackFinds: [] as string[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,15 +110,6 @@ async function printReport() {
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const ignoredErrors = [] as string[];
|
|
||||||
report.otherErrors = report.otherErrors.filter(e => {
|
|
||||||
if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
|
|
||||||
ignoredErrors.push(e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("## Discord Errors");
|
console.log("## Discord Errors");
|
||||||
report.otherErrors.forEach(e => {
|
report.otherErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e)}`);
|
||||||
|
@ -123,7 +118,7 @@ async function printReport() {
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
console.log("## Ignored Discord Errors");
|
console.log("## Ignored Discord Errors");
|
||||||
ignoredErrors.forEach(e => {
|
report.ignoredErrors.forEach(e => {
|
||||||
console.log(`- ${toCodeBlock(e)}`);
|
console.log(`- ${toCodeBlock(e)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -188,66 +183,6 @@ page.on("console", async e => {
|
||||||
const level = e.type();
|
const level = e.type();
|
||||||
const rawArgs = e.args();
|
const rawArgs = e.args();
|
||||||
|
|
||||||
const firstArg = await rawArgs[0]?.jsonValue();
|
|
||||||
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
|
|
||||||
await browser.close();
|
|
||||||
await printReport();
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const isVencord = firstArg === "[Vencord]";
|
|
||||||
const isDebug = firstArg === "[PUP_DEBUG]";
|
|
||||||
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";
|
|
||||||
|
|
||||||
if (isWebpackFindFail) {
|
|
||||||
process.exitCode = 1;
|
|
||||||
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVencord) {
|
|
||||||
let args: unknown[] = [];
|
|
||||||
try {
|
|
||||||
args = await Promise.all(e.args().map(a => a.jsonValue()));
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, tag, message] = args as Array<string>;
|
|
||||||
const cause = await maybeGetError(e.args()[3]);
|
|
||||||
|
|
||||||
switch (tag) {
|
|
||||||
case "WebpackInterceptor:":
|
|
||||||
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
|
||||||
if (!patchFailMatch) break;
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
|
|
||||||
const [, plugin, type, id, regex] = patchFailMatch;
|
|
||||||
report.badPatches.push({
|
|
||||||
plugin,
|
|
||||||
type,
|
|
||||||
id,
|
|
||||||
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
|
||||||
error: cause
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
case "PluginManager:":
|
|
||||||
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
|
||||||
if (!failedToStartMatch) break;
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
|
|
||||||
const [, name] = failedToStartMatch;
|
|
||||||
report.badStarts.push({
|
|
||||||
plugin: name,
|
|
||||||
error: cause
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getText() {
|
async function getText() {
|
||||||
try {
|
try {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
|
@ -260,137 +195,91 @@ page.on("console", async e => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDebug) {
|
const firstArg = await rawArgs[0]?.jsonValue();
|
||||||
const text = await getText();
|
|
||||||
|
|
||||||
console.error(text);
|
const isVencord = firstArg === "[Vencord]";
|
||||||
if (text.includes("A fatal error occurred:")) {
|
const isDebug = firstArg === "[PUP_DEBUG]";
|
||||||
process.exit(1);
|
|
||||||
|
outer:
|
||||||
|
if (isVencord) {
|
||||||
|
try {
|
||||||
|
var args = await Promise.all(e.args().map(a => a.jsonValue()));
|
||||||
|
} catch {
|
||||||
|
break outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [, tag, message, otherMessage] = args as Array<string>;
|
||||||
|
|
||||||
|
switch (tag) {
|
||||||
|
case "WebpackInterceptor:":
|
||||||
|
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
|
||||||
|
if (!patchFailMatch) break;
|
||||||
|
|
||||||
|
console.error(await getText());
|
||||||
|
process.exitCode = 1;
|
||||||
|
|
||||||
|
const [, plugin, type, id, regex] = patchFailMatch;
|
||||||
|
report.badPatches.push({
|
||||||
|
plugin,
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
|
||||||
|
error: await maybeGetError(e.args()[3])
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "PluginManager:":
|
||||||
|
const failedToStartMatch = message.match(/Failed to start (.+)/);
|
||||||
|
if (!failedToStartMatch) break;
|
||||||
|
|
||||||
|
console.error(await getText());
|
||||||
|
process.exitCode = 1;
|
||||||
|
|
||||||
|
const [, name] = failedToStartMatch;
|
||||||
|
report.badStarts.push({
|
||||||
|
plugin: name,
|
||||||
|
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "Reporter:":
|
||||||
|
console.error(await getText());
|
||||||
|
|
||||||
|
switch (message) {
|
||||||
|
case "Webpack Find Fail:":
|
||||||
|
process.exitCode = 1;
|
||||||
|
report.badWebpackFinds.push(otherMessage);
|
||||||
|
break;
|
||||||
|
case "A fatal error occurred:":
|
||||||
|
process.exit(1);
|
||||||
|
case "Finished test":
|
||||||
|
await browser.close();
|
||||||
|
await printReport();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDebug) {
|
||||||
|
console.error(await getText());
|
||||||
} else if (level === "error") {
|
} else if (level === "error") {
|
||||||
const text = await getText();
|
const text = await getText();
|
||||||
|
|
||||||
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
|
||||||
|
if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
|
||||||
|
report.ignoredErrors.push(text);
|
||||||
|
} else {
|
||||||
console.error("[Unexpected Error]", text);
|
console.error("[Unexpected Error]", text);
|
||||||
report.otherErrors.push(text);
|
report.otherErrors.push(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
page.on("error", e => console.error("[Error]", e));
|
page.on("error", e => console.error("[Error]", e.message));
|
||||||
page.on("pageerror", e => console.error("[Page Error]", e));
|
page.on("pageerror", e => console.error("[Page Error]", e.message));
|
||||||
|
|
||||||
await page.setBypassCSP(true);
|
|
||||||
|
|
||||||
async function reporterRuntime(token: string) {
|
async function reporterRuntime(token: string) {
|
||||||
console.log("[PUP_DEBUG]", "Starting test...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Spoof languages to not be suspicious
|
|
||||||
Object.defineProperty(navigator, "languages", {
|
|
||||||
get: function () {
|
|
||||||
return ["en-US", "en"];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let wreq: typeof Vencord.Webpack.wreq;
|
|
||||||
|
|
||||||
const { canonicalizeMatch, Logger } = Vencord.Util;
|
|
||||||
|
|
||||||
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(Vencord.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 => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
if (isWasm) {
|
|
||||||
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]) =>
|
|
||||||
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Requires the entry points for all valid chunk groups
|
|
||||||
for (const [, entryPoint] of validChunkGroups) {
|
|
||||||
try {
|
|
||||||
if (shouldForceDefer) {
|
|
||||||
deferredRequires.add(entryPoint);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Vencord.Webpack.waitFor(
|
Vencord.Webpack.waitFor(
|
||||||
"loginToken",
|
"loginToken",
|
||||||
m => {
|
m => {
|
||||||
|
@ -398,118 +287,6 @@ async function reporterRuntime(token: string) {
|
||||||
m.loginToken(token);
|
m.loginToken(token);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
|
|
||||||
console.log("[PUP_DEBUG]", "Loading all chunks...");
|
|
||||||
|
|
||||||
wreq = webpackRequire;
|
|
||||||
|
|
||||||
Vencord.Webpack.factoryListeners.add(factory => {
|
|
||||||
let isResolved = false;
|
|
||||||
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
|
||||||
|
|
||||||
chunksSearchPromises.push(() => isResolved);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await chunksSearchingDone;
|
|
||||||
|
|
||||||
// Require deferred entry points
|
|
||||||
for (const deferredRequire of deferredRequires) {
|
|
||||||
wreq!(deferredRequire as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
|
||||||
const allChunks = [] as string[];
|
|
||||||
|
|
||||||
// Matches "id" or id:
|
|
||||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
|
||||||
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 => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
|
|
||||||
|
|
||||||
// Loads and requires a chunk
|
|
||||||
if (!isWasm) {
|
|
||||||
await wreq.e(id as any);
|
|
||||||
if (wreq.m[id]) wreq(id as any);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
|
||||||
|
|
||||||
for (const patch of Vencord.Plugins.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 Vencord.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 Vencord.Webpack.extractAndLoadChunks(code, matcher);
|
|
||||||
if (result === false) result = null;
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
result = Vencord.Webpack[method](...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
|
|
||||||
} catch (e) {
|
|
||||||
let logMessage = searchType;
|
|
||||||
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
|
||||||
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
|
||||||
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;
|
|
||||||
|
|
||||||
console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluateOnNewDocument(`
|
await page.evaluateOnNewDocument(`
|
||||||
|
|
|
@ -42,6 +42,10 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
|
||||||
import { onceReady } from "./webpack";
|
import { onceReady } from "./webpack";
|
||||||
import { SettingsRouter } from "./webpack/common";
|
import { SettingsRouter } from "./webpack/common";
|
||||||
|
|
||||||
|
if (IS_REPORTER) {
|
||||||
|
require("./debug/runReporter");
|
||||||
|
}
|
||||||
|
|
||||||
async function syncSettings() {
|
async function syncSettings() {
|
||||||
// pre-check for local shared settings
|
// pre-check for local shared settings
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
|
||||||
|
|
||||||
function defaultGetStore() {
|
function defaultGetStore() {
|
||||||
if (!defaultGetStoreFunc) {
|
if (!defaultGetStoreFunc) {
|
||||||
defaultGetStoreFunc = createStore("VencordData", "VencordStore");
|
defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
|
||||||
}
|
}
|
||||||
return defaultGetStoreFunc;
|
return defaultGetStoreFunc;
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,7 @@ const DefaultSettings: Settings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = VencordNative.settings.get();
|
const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
|
||||||
mergeDefaults(settings, DefaultSettings);
|
mergeDefaults(settings, DefaultSettings);
|
||||||
|
|
||||||
const saveSettingsOnFrequentAction = debounce(async () => {
|
const saveSettingsOnFrequentAction = debounce(async () => {
|
||||||
|
@ -156,12 +156,14 @@ export const SettingsStore = new SettingsStoreClass(settings, {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!IS_REPORTER) {
|
||||||
SettingsStore.addGlobalChangeListener((_, path) => {
|
SettingsStore.addGlobalChangeListener((_, path) => {
|
||||||
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
|
||||||
localStorage.Vencord_settingsDirty = true;
|
localStorage.Vencord_settingsDirty = true;
|
||||||
saveSettingsOnFrequentAction();
|
saveSettingsOnFrequentAction();
|
||||||
VencordNative.settings.set(SettingsStore.plain, path);
|
VencordNative.settings.set(SettingsStore.plain, path);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
* Same as {@link Settings} but unproxied. You should treat this as readonly,
|
||||||
|
|
224
src/debug/runReporter.ts
Normal file
224
src/debug/runReporter.ts
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Logger } from "@utils/Logger";
|
||||||
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
|
import * as Webpack from "@webpack";
|
||||||
|
import { wreq } from "@webpack";
|
||||||
|
import { patches } from "plugins";
|
||||||
|
|
||||||
|
const ReporterLogger = new Logger("Reporter");
|
||||||
|
|
||||||
|
async function runReporter() {
|
||||||
|
ReporterLogger.log("Starting test...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
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]) =>
|
||||||
|
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requires the entry points for all valid chunk groups
|
||||||
|
for (const [, entryPoint] of validChunkGroups) {
|
||||||
|
try {
|
||||||
|
if (shouldForceDefer) {
|
||||||
|
deferredRequires.add(entryPoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wreq.m[entryPoint]) wreq(entryPoint as any);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Webpack.beforeInitListeners.add(async () => {
|
||||||
|
ReporterLogger.log("Loading all chunks...");
|
||||||
|
|
||||||
|
Webpack.factoryListeners.add(factory => {
|
||||||
|
let isResolved = false;
|
||||||
|
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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(wreq.m[factoryId].toString()).then(() => isResolved = true);
|
||||||
|
|
||||||
|
chunksSearchPromises.push(() => isResolved);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await chunksSearchingDone;
|
||||||
|
|
||||||
|
// Require deferred entry points
|
||||||
|
for (const deferredRequire of deferredRequires) {
|
||||||
|
wreq!(deferredRequire as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All chunks Discord has mapped to asset files, even if they are not used anymore
|
||||||
|
const allChunks = [] as string[];
|
||||||
|
|
||||||
|
// Matches "id" or id:
|
||||||
|
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||||
|
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) {
|
||||||
|
await wreq.e(id as any);
|
||||||
|
if (wreq.m[id]) wreq(id as any);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
|
||||||
|
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
|
||||||
|
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();
|
|
@ -17,4 +17,4 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (!IS_UPDATER_DISABLED)
|
if (!IS_UPDATER_DISABLED)
|
||||||
import(IS_STANDALONE ? "./http" : "./git");
|
require(IS_STANDALONE ? "./http" : "./git");
|
||||||
|
|
|
@ -32,7 +32,7 @@ export class Logger {
|
||||||
constructor(public name: string, public color: string = "white") { }
|
constructor(public name: string, public color: string = "white") { }
|
||||||
|
|
||||||
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
|
||||||
if (IS_REPORTER && (level === "warn" || level === "error")) {
|
if (IS_REPORTER) {
|
||||||
console[level]("[Vencord]", this.name + ":", ...args);
|
console[level]("[Vencord]", this.name + ":", ...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue