Skip to content

Commit e8639e2

Browse files
authored
Rewrite WebpackPatcher to support new features (#3157)
1 parent fcf8690 commit e8639e2

22 files changed

Lines changed: 889 additions & 351 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ vencord_installer
88
.DS_Store
99

1010
yarn.lock
11+
bun.lock
1112
package-lock.json
1213

1314
*.log

scripts/build/common.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export const commonOpts = {
312312
logLevel: "info",
313313
bundle: true,
314314
watch,
315-
minify: !watch,
315+
minify: !watch && !IS_REPORTER,
316316
sourcemap: watch ? "inline" : "",
317317
legalComments: "linked",
318318
banner,

scripts/generateReport.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ page.on("console", async e => {
215215

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

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

@@ -292,7 +292,7 @@ page.on("error", e => console.error("[Error]", e.message));
292292
page.on("pageerror", e => {
293293
if (e.message.includes("Sentry successfully disabled")) return;
294294

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

src/Vencord.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * as Util from "./utils";
2323
export * as QuickCss from "./utils/quickCss";
2424
export * as Updater from "./utils/updater";
2525
export * as Webpack from "./webpack";
26+
export * as WebpackPatcher from "./webpack/patchWebpack";
2627
export { PlainSettings, Settings };
2728

2829
import "./utils/quickCss";

src/api/Settings.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ export interface Settings {
3232
autoUpdate: boolean;
3333
autoUpdateNotification: boolean,
3434
useQuickCss: boolean;
35+
eagerPatches: boolean;
36+
enabledThemes: string[];
3537
enableReactDevtools: boolean;
3638
themeLinks: string[];
37-
enabledThemes: string[];
3839
frameless: boolean;
3940
transparent: boolean;
4041
winCtrlQ: boolean;
@@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
8182
autoUpdateNotification: true,
8283
useQuickCss: true,
8384
themeLinks: [],
85+
eagerPatches: IS_REPORTER,
8486
enabledThemes: [],
8587
enableReactDevtools: false,
8688
frameless: false,

src/debug/Tracer.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
2323
var logger = new Logger("Tracer", "#FFD166");
2424
}
2525

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

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

36-
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
37-
const end = performance.now();
35+
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
36+
function finishTrace(name: string) {
37+
const end = performance.now();
38+
39+
const [start, args] = traces[name];
40+
delete traces[name];
3841

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

42-
logger.debug(`${name} took ${end - start}ms`, args);
43-
};
45+
return totalTime;
46+
};
4447

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

48-
const noopTracer =
49-
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => f;
51+
function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
52+
return function (this: unknown, ...args: Parameters<F>): [ReturnType<F>, number] {
53+
return [f.apply(this, args), 0];
54+
};
55+
}
56+
57+
function noopTracer<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
58+
return f;
59+
}
60+
61+
export const traceFunctionWithResults = !(IS_DEV || IS_REPORTER)
62+
? noopTracerWithResults
63+
: function traceFunctionWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): (this: unknown, ...args: Parameters<F>) => [ReturnType<F>, number] {
64+
return function (this: unknown, ...args: Parameters<F>) {
65+
const traceName = mapper?.(...args) ?? name;
66+
67+
beginTrace(traceName, ...arguments);
68+
try {
69+
return [f.apply(this, args), finishTrace(traceName)];
70+
} catch (e) {
71+
finishTrace(traceName);
72+
throw e;
73+
}
74+
};
75+
};
5076

5177
export const traceFunction = !(IS_DEV || IS_REPORTER)
5278
? noopTracer
5379
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F {
54-
return function (this: any, ...args: Parameters<F>) {
80+
return function (this: unknown, ...args: Parameters<F>) {
5581
const traceName = mapper?.(...args) ?? name;
5682

5783
beginTrace(traceName, ...arguments);

src/debug/loadLazyChunks.ts

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,27 @@ import { Logger } from "@utils/Logger";
88
import { canonicalizeMatch } from "@utils/patches";
99
import * as Webpack from "@webpack";
1010
import { wreq } from "@webpack";
11-
12-
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
11+
import { AnyModuleFactory, ModuleFactory } from "webpack";
1312

1413
export async function loadLazyChunks() {
14+
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
15+
1516
try {
1617
LazyChunkLoaderLogger.log("Loading all chunks...");
1718

18-
const validChunks = new Set<number>();
19-
const invalidChunks = new Set<number>();
20-
const deferredRequires = new Set<number>();
19+
const validChunks = new Set<PropertyKey>();
20+
const invalidChunks = new Set<PropertyKey>();
21+
const deferredRequires = new Set<PropertyKey>();
2122

22-
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
23+
let chunksSearchingResolve: (value: void) => void;
2324
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
2425

2526
// True if resolved, false otherwise
2627
const chunksSearchPromises = [] as Array<() => boolean>;
2728

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

3034
let foundCssDebuggingLoad = false;
@@ -34,12 +38,15 @@ export async function loadLazyChunks() {
3438
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
3539

3640
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
37-
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
41+
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
3842

3943
const shouldForceDefer = false;
4044

4145
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
42-
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
46+
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
47+
const numChunkId = Number(m[1]);
48+
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
49+
}) : [];
4350

4451
if (chunkIds.length === 0) {
4552
return;
@@ -74,15 +81,16 @@ export async function loadLazyChunks() {
7481
}
7582

7683
if (!invalidChunkGroup) {
77-
validChunkGroups.add([chunkIds, Number(entryPoint)]);
84+
const numEntryPoint = Number(entryPoint);
85+
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
7886
}
7987
}));
8088

8189
// Loads all found valid chunk groups
8290
await Promise.all(
8391
Array.from(validChunkGroups)
8492
.map(([chunkIds]) =>
85-
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
93+
Promise.all(chunkIds.map(id => wreq.e(id)))
8694
)
8795
);
8896

@@ -94,7 +102,7 @@ export async function loadLazyChunks() {
94102
continue;
95103
}
96104

97-
if (wreq.m[entryPoint]) wreq(entryPoint as any);
105+
if (wreq.m[entryPoint]) wreq(entryPoint);
98106
} catch (err) {
99107
console.error(err);
100108
}
@@ -122,41 +130,44 @@ export async function loadLazyChunks() {
122130
}, 0);
123131
}
124132

125-
Webpack.factoryListeners.add(factory => {
133+
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
126134
let isResolved = false;
127-
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
135+
searchAndLoadLazyChunks(String(factory))
136+
.then(() => isResolved = true)
137+
.catch(() => isResolved = true);
128138

129139
chunksSearchPromises.push(() => isResolved);
130-
});
140+
}
131141

142+
Webpack.factoryListeners.add(factoryListener);
132143
for (const factoryId in wreq.m) {
133-
let isResolved = false;
134-
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
135-
136-
chunksSearchPromises.push(() => isResolved);
144+
factoryListener(wreq.m[factoryId]);
137145
}
138146

139147
await chunksSearchingDone;
148+
Webpack.factoryListeners.delete(factoryListener);
140149

141150
// Require deferred entry points
142151
for (const deferredRequire of deferredRequires) {
143-
wreq!(deferredRequire as any);
152+
wreq(deferredRequire);
144153
}
145154

146155
// All chunks Discord has mapped to asset files, even if they are not used anymore
147-
const allChunks = [] as number[];
156+
const allChunks = [] as PropertyKey[];
148157

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

154-
allChunks.push(Number(id));
163+
const numId = Number(id);
164+
allChunks.push(Number.isNaN(numId) ? id : numId);
155165
}
156166

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

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

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

src/debug/runReporter.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,35 @@
66

77
import { Logger } from "@utils/Logger";
88
import * as Webpack from "@webpack";
9-
import { patches } from "plugins";
9+
import { addPatch, patches } from "plugins";
1010

1111
import { loadLazyChunks } from "./loadLazyChunks";
1212

13-
const ReporterLogger = new Logger("Reporter");
14-
1513
async function runReporter() {
14+
const ReporterLogger = new Logger("Reporter");
15+
1616
try {
1717
ReporterLogger.log("Starting test...");
1818

19-
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
19+
let loadLazyChunksResolve: (value: void) => void;
2020
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
2121

22-
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
22+
// The main patch for starting the reporter chunk loading
23+
addPatch({
24+
find: '"Could not find app-mount"',
25+
replacement: {
26+
match: /(?<="use strict";)/,
27+
replace: "Vencord.Webpack._initReporter();"
28+
}
29+
}, "Vencord Reporter");
30+
31+
// @ts-ignore
32+
Vencord.Webpack._initReporter = function () {
33+
// initReporter is called in the patched entry point of Discord
34+
// setImmediate to only start searching for lazy chunks after Discord initialized the app
35+
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
36+
};
37+
2338
await loadLazyChunksDone;
2439

2540
for (const patch of patches) {
@@ -28,6 +43,12 @@ async function runReporter() {
2843
}
2944
}
3045

46+
for (const [plugin, moduleId, match, totalTime] of Vencord.WebpackPatcher.patchTimings) {
47+
if (totalTime > 3) {
48+
new Logger("WebpackInterceptor").warn(`Patch by ${plugin} took ${Math.round(totalTime * 100) / 100}ms (Module id is ${String(moduleId)}): ${match}`);
49+
}
50+
}
51+
3152
for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
3253
let method = searchType;
3354

@@ -88,4 +109,6 @@ async function runReporter() {
88109
}
89110
}
90111

91-
runReporter();
112+
// Run after the Vencord object has been created.
113+
// We need to add extra properties to it, and it is only created after all of Vencord code has ran
114+
setTimeout(runReporter, 0);

src/globals.d.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,8 @@ declare global {
6464
export var Vesktop: any;
6565
export var VesktopNative: any;
6666

67-
interface Window {
68-
webpackChunkdiscord_app: {
69-
push(chunk: any): any;
70-
pop(): any;
71-
};
67+
interface Window extends Record<PropertyKey, any> {
7268
_: LoDashStatic;
73-
[k: string]: any;
7469
}
7570
}
7671

0 commit comments

Comments
 (0)