Equicord/src/debug/loadLazyChunks.ts

207 lines
8.1 KiB
TypeScript
Raw Normal View History

2024-06-01 18:39:01 -03:00
/*
* 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 { ModuleFactory } from "@vencord/discord-types/webpack";
2024-06-01 18:39:01 -03:00
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
2026-03-19 09:32:30 +01:00
import pLimit from "p-limit";
import { AnyModuleFactory } from "webpack";
2024-06-01 18:39:01 -03:00
2025-10-01 23:12:33 +02:00
function getWebpackChunkMap() {
const sym = Symbol();
let v: Record<PropertyKey, string> | null = null;
Object.defineProperty(Object.prototype, sym, {
get() {
v = this;
return "";
},
configurable: true
});
wreq.u(sym);
delete Object.prototype[sym];
return v;
}
2024-06-01 18:39:01 -03:00
export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
2026-03-19 09:32:30 +01:00
const queue = pLimit(50);
2024-06-01 18:39:01 -03:00
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<PropertyKey>();
const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<PropertyKey>();
2024-06-01 18:39:01 -03:00
const { promise: chunksSearchingDone, resolve: chunksSearchingResolve } = Promise.withResolvers<void>();
2024-06-01 18:39:01 -03:00
// 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, we prioritize using the one which doesnt include those
const CompleteLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
const PartialLazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?((?:\i\.e\("?[^)]+?"?\),?)+?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
2024-06-01 18:39:01 -03:00
let foundCssDebuggingLoad = false;
2024-11-13 18:44:21 -03:00
2024-06-01 18:39:01 -03:00
async function searchAndLoadLazyChunks(factoryCode: string) {
2024-11-13 18:44:21 -03:00
// Workaround to avoid loading the CSS debugging chunk which turns the app pink
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
2024-11-13 18:44:21 -03:00
const lazyChunks = factoryCode.matchAll(hasCssDebuggingLoad ? CompleteLazyChunkRegex : PartialLazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
2024-06-01 18:39:01 -03:00
const shouldForceDefer = false;
2024-06-01 18:39:01 -03:00
2026-03-19 09:32:30 +01:00
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => queue(async () => {
const chunkIds = rawChunkIds
?.matchAll(Webpack.ChunkIdsRegex)
.map(m => {
const numChunkId = Number(m[1]);
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
})
.toArray()
?? [];
2024-06-01 18:39:01 -03:00
if (chunkIds.length === 0) {
return;
}
let invalidChunkGroup = false;
for (const id of chunkIds) {
2024-11-13 18:44:21 -03:00
if (hasCssDebuggingLoad) {
if (chunkIds.length > 1) {
throw new Error("Found multiple chunks in factory that loads the CSS debugging chunk");
}
invalidChunks.add(id);
invalidChunkGroup = true;
break;
}
2024-06-01 18:39:01 -03:00
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
2024-06-01 18:39:01 -03:00
.then(r => r.text())
.then(t => /importScripts\(|self\.postMessage/.test(t));
2024-06-01 18:39:01 -03:00
if (isWorkerAsset) {
2024-06-01 18:39:01 -03:00
invalidChunks.add(id);
invalidChunkGroup = true;
continue;
}
validChunks.add(id);
}
if (!invalidChunkGroup) {
const numEntryPoint = Number(entryPoint);
validChunkGroups.add([chunkIds, Number.isNaN(numEntryPoint) ? entryPoint : numEntryPoint]);
2024-06-01 18:39:01 -03:00
}
2026-03-19 09:32:30 +01:00
})));
2024-06-01 18:39:01 -03:00
// Loads all found valid chunk groups
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
2026-03-19 09:32:30 +01:00
Promise.all(chunkIds.map(id => queue(() => wreq.e(id))))
2024-06-01 18:39:01 -03:00
)
);
// 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);
2024-06-01 18:39:01 -03:00
} 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);
}
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
2024-06-01 18:39:01 -03:00
let isResolved = false;
searchAndLoadLazyChunks(String(factory))
.then(() => isResolved = true)
.catch(() => isResolved = true);
2024-06-01 18:39:01 -03:00
chunksSearchPromises.push(() => isResolved);
}
2024-06-01 18:39:01 -03:00
Webpack.factoryListeners.add(factoryListener);
for (const moduleId in wreq.m) {
factoryListener(wreq.m[moduleId]);
2024-06-01 18:39:01 -03:00
}
await chunksSearchingDone;
Webpack.factoryListeners.delete(factoryListener);
2024-06-01 18:39:01 -03:00
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq(deferredRequire);
2024-06-01 18:39:01 -03:00
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
2025-10-01 23:12:33 +02:00
const chunkMap = getWebpackChunkMap();
if (!chunkMap) throw new Error("Failed to get chunk map");
2024-06-01 18:39:01 -03:00
2025-10-01 23:12:33 +02:00
const allChunks = Object.keys(chunkMap).map(id => Number.isNaN(Number(id)) ? id : Number(id));
2024-06-01 18:39:01 -03:00
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// 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
2024-06-01 18:39:01 -03:00
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
2026-03-19 09:32:30 +01:00
await Promise.all(chunksLeft.map(async id => queue(async () => {
const isWorkerAsset = await fetch(wreq.p + wreq.u(id))
2024-06-01 18:39:01 -03:00
.then(r => r.text())
.then(t => /importScripts\(|self\.postMessage/.test(t));
2024-06-01 18:39:01 -03:00
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
await wreq.e(id);
2024-06-01 18:39:01 -03:00
}
2026-03-19 09:32:30 +01:00
})));
2024-06-01 18:39:01 -03:00
LazyChunkLoaderLogger.log("Finished loading all chunks!");
} catch (e) {
LazyChunkLoaderLogger.log("A fatal error occurred:", e);
}
}