mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-18 02:47:03 -04:00
Rewrite WebpackPatcher to support new features (#3157)
This commit is contained in:
parent
fcf8690d26
commit
e8639e2e16
22 changed files with 896 additions and 358 deletions
|
@ -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];
|
||||
|
||||
logger.debug(`${name} took ${end - start}ms`, args);
|
||||
};
|
||||
const totalTime = end - start;
|
||||
logger.debug(`${name} took ${totalTime}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);
|
||||
|
|
|
@ -8,23 +8,27 @@ 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...");
|
||||
|
||||
const validChunks = new Set<number>();
|
||||
const invalidChunks = new Set<number>();
|
||||
const deferredRequires = new Set<number>();
|
||||
const validChunks = new Set<PropertyKey>();
|
||||
const invalidChunks = new Set<PropertyKey>();
|
||||
const deferredRequires = new Set<PropertyKey>();
|
||||
|
||||
let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
|
||||
let chunksSearchingResolve: (value: void) => void;
|
||||
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
|
||||
|
||||
// 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;
|
||||
|
@ -34,12 +38,15 @@ export async function loadLazyChunks() {
|
|||
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
|
||||
|
||||
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
|
||||
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>();
|
||||
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
|
||||
|
||||
const shouldForceDefer = false;
|
||||
|
||||
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => Number(m[1])) : [];
|
||||
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Webpack.ChunkIdsRegex)).map(m => {
|
||||
const numChunkId = Number(m[1]);
|
||||
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
|
||||
}) : [];
|
||||
|
||||
if (chunkIds.length === 0) {
|
||||
return;
|
||||
|
@ -74,7 +81,8 @@ export async function loadLazyChunks() {
|
|||
}
|
||||
|
||||
if (!invalidChunkGroup) {
|
||||
validChunkGroups.add([chunkIds, Number(entryPoint)]);
|
||||
const numEntryPoint = Number(entryPoint);
|
||||
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
|
||||
}
|
||||
}));
|
||||
|
||||
|
@ -82,7 +90,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)))
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -94,7 +102,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);
|
||||
}
|
||||
|
@ -122,41 +130,44 @@ 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[];
|
||||
const allChunks = [] as PropertyKey[];
|
||||
|
||||
// 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;
|
||||
|
||||
allChunks.push(Number(id));
|
||||
const numId = Number(id);
|
||||
allChunks.push(Number.isNaN(numId) ? id : numId);
|
||||
}
|
||||
|
||||
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));
|
||||
});
|
||||
|
@ -166,12 +177,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);
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
|
@ -6,20 +6,35 @@
|
|||
|
||||
import { Logger } from "@utils/Logger";
|
||||
import * as Webpack from "@webpack";
|
||||
import { patches } from "plugins";
|
||||
import { addPatch, patches } from "plugins";
|
||||
|
||||
import { loadLazyChunks } from "./loadLazyChunks";
|
||||
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
async function runReporter() {
|
||||
const ReporterLogger = new Logger("Reporter");
|
||||
|
||||
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) {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue