Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite WebpackPatcher to support new features #3157

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/build/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export const commonOpts = {
logLevel: "info",
bundle: true,
watch,
minify: !watch,
minify: !watch && !IS_REPORTER,
sourcemap: watch ? "inline" : "",
legalComments: "linked",
banner,
Expand Down
19 changes: 4 additions & 15 deletions scripts/generateReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import { readFileSync } from "fs";
import pup, { JSHandle } from "puppeteer-core";

for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
for (const variable of ["CHROMIUM_BIN"]) {
if (!process.env[variable]) {
console.error(`Missing environment variable ${variable}`);
process.exit(1);
Expand Down Expand Up @@ -215,7 +215,7 @@ page.on("console", async e => {

switch (tag) {
case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module|took [\d.]+?ms) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;

console.error(await getText());
Expand All @@ -226,7 +226,7 @@ page.on("console", async e => {
plugin,
type,
id,
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
match: regex,
error: await maybeGetError(e.args()[3])
});

Expand Down Expand Up @@ -292,28 +292,17 @@ page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;

if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module") && !/^.{1,2}$/.test(e.message)) {
console.error("[Page Error]", e.message);
report.otherErrors.push(e.message);
} else {
report.ignoredErrors.push(e.message);
}
});

async function reporterRuntime(token: string) {
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
}

await page.evaluateOnNewDocument(`
if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`);

Expand Down
1 change: 1 addition & 0 deletions src/Vencord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater";
export * as Webpack from "./webpack";
export * as WebpackPatcher from "./webpack/patchWebpack";
export { PlainSettings, Settings };

import "./utils/quickCss";
Expand Down
4 changes: 3 additions & 1 deletion src/api/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ export interface Settings {
autoUpdate: boolean;
autoUpdateNotification: boolean,
useQuickCss: boolean;
eagerPatches: boolean;
enabledThemes: string[];
enableReactDevtools: boolean;
themeLinks: string[];
enabledThemes: string[];
frameless: boolean;
transparent: boolean;
winCtrlQ: boolean;
Expand Down Expand Up @@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true,
useQuickCss: true,
themeLinks: [],
eagerPatches: IS_REPORTER,
enabledThemes: [],
enableReactDevtools: false,
frameless: false,
Expand Down
52 changes: 39 additions & 13 deletions src/debug/Tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
var logger = new Logger("Tracer", "#FFD166");
}

const noop = function () { };

export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
if (name in traces) {
throw new Error(`Trace ${name} already exists!`);
}

traces[name] = [performance.now(), args];
};

export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now();
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
function finishTrace(name: string) {
const end = performance.now();

const [start, args] = traces[name];
delete traces[name];

const [start, args] = traces[name];
delete traces[name];
const totalTime = end - start;
logger.debug(`${name} took ${totalTime}ms`, args);

logger.debug(`${name} took ${end - start}ms`, args);
};
return totalTime;
};

type Func = (...args: any[]) => any;
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;

const noopTracer =
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {
return [f.apply(this, args), 0];
};
}

function noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
return f;
}

export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
? noopTracerWithResults
: function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {
return function (this: unknown, ...args: Parameters<F>) {
const traceName = mapper?.(...args) ?? name;

beginTrace(traceName, ...arguments);
try {
return [f.apply(this, args), finishTrace(traceName)];
} catch (e) {
finishTrace(traceName);
throw e;
}
};
};

export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
return function (this: any, ...args: Parameters<F>) {
return function (this: unknown, ...args: Parameters<F>) {
const traceName = mapper?.(...args) ?? name;

beginTrace(traceName, ...arguments);
Expand Down
41 changes: 22 additions & 19 deletions src/debug/loadLazyChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";

const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
import { AnyModuleFactory, ModuleFactory } from "webpack";

export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");

try {
LazyChunkLoaderLogger.log("Loading all chunks...");

Expand All @@ -25,6 +26,9 @@ export async function loadLazyChunks() {
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;

/* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
*/
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);

let foundCssDebuggingLoad = false;
Expand Down Expand Up @@ -82,7 +86,7 @@ export async function loadLazyChunks() {
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
Promise.all(chunkIds.map(id => wreq.e(id)))
)
);

Expand All @@ -94,7 +98,7 @@ export async function loadLazyChunks() {
continue;
}

if (wreq.m[entryPoint]) wreq(entryPoint as any);
if (wreq.m[entryPoint]) wreq(entryPoint);
} catch (err) {
console.error(err);
}
Expand Down Expand Up @@ -122,32 +126,33 @@ export async function loadLazyChunks() {
}, 0);
}

Webpack.factoryListeners.add(factory => {
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
searchAndLoadLazyChunks(String(factory))
.then(() => isResolved = true)
.catch(() => isResolved = true);

chunksSearchPromises.push(() => isResolved);
});
}

Webpack.factoryListeners.add(factoryListener);
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);

chunksSearchPromises.push(() => isResolved);
factoryListener(wreq.m[factoryId]);
}

await chunksSearchingDone;
Webpack.factoryListeners.delete(factoryListener);

// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
wreq(deferredRequire);
}

// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as number[];

// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;

Expand All @@ -156,7 +161,8 @@ export async function loadLazyChunks() {

if (allChunks.length === 0) throw new Error("Failed to get all chunks");

// Chunks that are not loaded (not used) by Discord code anymore
// Chunks which our regex could not catch to load
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
Expand All @@ -166,12 +172,9 @@ export async function loadLazyChunks() {
.then(r => r.text())
.then(t => t.includes("importScripts("));

// Loads and requires a chunk
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
await wreq.e(id as any);
// Technically, the id of the chunk does not match the entry point
// But, still try it because we have no way to get the actual entry point
if (wreq.m[id]) wreq(id as any);
await wreq.e(id);
}
}));

Expand Down
31 changes: 27 additions & 4 deletions src/debug/runReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
import { patches } from "plugins";
import { addPatch, patches } from "plugins";

import { loadLazyChunks } from "./loadLazyChunks";

Expand All @@ -16,10 +16,25 @@ async function runReporter() {
try {
ReporterLogger.log("Starting test...");

let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
let loadLazyChunksResolve: (value: void) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);

Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
// 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(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
};

await loadLazyChunksDone;

for (const patch of patches) {
Expand All @@ -28,6 +43,12 @@ async function runReporter() {
}
}

for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) {
if (totalTime > 3) {
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
}
}

for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
let method = searchType;

Expand Down Expand Up @@ -88,4 +109,6 @@ async function runReporter() {
}
}

runReporter();
// Run after the Vencord object has been created.
// We need to add extra properties to it, and it is only created after all of Vencord code has ran
setTimeout(runReporter, 0);
12 changes: 7 additions & 5 deletions src/plugins/_core/noTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "webpack";

const settings = definePluginSettings({
disableAnalytics: {
Expand Down Expand Up @@ -81,9 +82,9 @@ export default definePlugin({
Object.defineProperty(Function.prototype, "g", {
configurable: true,

set(v: any) {
set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
Object.defineProperty(this, "g", {
value: v,
value: globalObj,
configurable: true,
enumerable: true,
writable: true
Expand All @@ -92,11 +93,11 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
return;
}

const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
if (!assetPath) {
return;
}
Expand All @@ -106,7 +107,8 @@ export default definePlugin({
srcRequest.send();

// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
if (!srcRequest.responseText.includes(".DiscordSentry=")) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/devCompanion.dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length);

const mod = candidates[keys[0]];
let src = String(mod.original ?? mod).replaceAll("\n", "");
let src = String(mod).replaceAll("\n", "");

if (src.startsWith("function(")) {
src = "0," + src;
Expand Down
Loading