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

This commit is contained in:
Nuckyz 2024-06-01 18:16:40 -03:00
commit 13337950dc
No known key found for this signature in database
GPG key ID: 440BF8296E1C4AD9
9 changed files with 192 additions and 185 deletions

View file

@ -242,16 +242,23 @@ page.on("console", async e => {
}); });
break; break;
case "LazyChunkLoader:":
console.error(await getText());
switch (message) {
case "A fatal error occurred:":
process.exit(1);
}
case "Reporter:": case "Reporter:":
console.error(await getText()); console.error(await getText());
switch (message) { switch (message) {
case "A fatal error occurred:":
process.exit(1);
case "Webpack Find Fail:": case "Webpack Find Fail:":
process.exitCode = 1; process.exitCode = 1;
report.badWebpackFinds.push(otherMessage); report.badWebpackFinds.push(otherMessage);
break; break;
case "A fatal error occurred:":
process.exit(1);
case "Finished test": case "Finished test":
await browser.close(); await browser.close();
await printReport(); await printReport();

View file

@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
{timeout !== 0 && !permanent && ( {timeout !== 0 && !permanent && (
<div <div
className="vc-notification-progressbar" className="vc-notification-progressbar"
style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-experiment)" }} style={{ width: `${(1 - timeoutProgress) * 100}%`, backgroundColor: color || "var(--brand-500)" }}
/> />
)} )}
</button> </button>

View file

@ -131,7 +131,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path === "plugins" && key in plugins) if (path === "plugins" && key in plugins)
return target[key] = { return target[key] = {
enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
}; };
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve // Since the property is not set, check if this is a plugin's setting and if so, try to resolve

167
src/debug/loadLazyChunks.ts Normal file
View file

@ -0,0 +1,167 @@
/*
* 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";
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
export async function loadLazyChunks() {
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
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.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
}
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);
}
}));
LazyChunkLoaderLogger.log("Finished loading all chunks!");
} catch (e) {
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
}
}

View file

@ -4,200 +4,31 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import { SYM_PROXY_INNER_GET, SYM_PROXY_INNER_VALUE } from "@utils/proxyInner"; import { SYM_PROXY_INNER_GET, SYM_PROXY_INNER_VALUE } from "@utils/proxyInner";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { wreq } from "@webpack"; import { patches } from "plugins";
import { addPatch, patches } from "plugins";
import { loadLazyChunks } from "./loadLazyChunks";
const ReporterLogger = new Logger("Reporter"); const ReporterLogger = new Logger("Reporter");
async function runReporter() { async function runReporter() {
try {
ReporterLogger.log("Starting test..."); ReporterLogger.log("Starting test...");
try { let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
// Enable eagerPatches to make all patches apply regardless of the module being required const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
Settings.eagerPatches = true;
// The main patch for starting the reporter chunk loading Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
addPatch({ await loadLazyChunksDone;
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);
};
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)))
)
);
// 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);
} 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) {
wreq(deferredRequire);
}
// 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 String(wreq.u).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);
if (wreq.m[id]) wreq(id);
}
}));
ReporterLogger.log("Finished loading all chunks!");
for (const patch of patches) { for (const patch of patches) {
if (!patch.all) { if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
} }
} }
await Promise.all(Webpack.webpackSearchHistory.map(async ([searchType, args]) => { await Promise.all(Webpack.webpackSearchHistory.map(async ([searchType, args]) => {
args = [...args]; args = [...args];

View file

@ -26,6 +26,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { cacheFindAll, extract, filters, findModuleId, search } from "@webpack"; import { cacheFindAll, extract, filters, findModuleId, search } from "@webpack";
import * as Common from "@webpack/common"; import * as Common from "@webpack/common";
import { loadLazyChunks } from "debug/loadLazyChunks";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
const DESKTOP_ONLY = (f: string) => () => { const DESKTOP_ONLY = (f: string) => () => {
@ -84,6 +85,7 @@ function makeShortcuts() {
wpsearch: search, wpsearch: search,
wpex: extract, wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!), wpexs: (code: string) => extract(findModuleId(code)!),
loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
find, find,
findAll: cacheFindAll, findAll: cacheFindAll,
findByProps, findByProps,

View file

@ -44,7 +44,6 @@ const settings = Settings.plugins;
export function isPluginEnabled(p: string) { export function isPluginEnabled(p: string) {
return ( return (
IS_REPORTER ||
Plugins[p]?.required || Plugins[p]?.required ||
Plugins[p]?.isDependency || Plugins[p]?.isDependency ||
settings[p]?.enabled settings[p]?.enabled

View file

@ -18,7 +18,7 @@
import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
const enum Intensity { const enum Intensity {
@ -46,6 +46,7 @@ export default definePlugin({
name: "PartyMode", name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨", description: "Allows you to use party mode cause the party never ends ✨",
authors: [Devs.UwUDev], authors: [Devs.UwUDev],
reporterTestable: ReporterTestable.None,
settings, settings,
start() { start() {

View file

@ -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) { if (IS_REPORTER && IS_WEB) {
console[level]("[Vencord]", this.name + ":", ...args); console[level]("[Vencord]", this.name + ":", ...args);
return; return;
} }