Rewrite WebpackPatcher to support new features (#3157)

This commit is contained in:
Nuckyz 2025-02-07 20:07:17 -03:00 committed by GitHub
parent fcf8690d26
commit e8639e2e16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 896 additions and 358 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ vencord_installer
.DS_Store .DS_Store
yarn.lock yarn.lock
bun.lock
package-lock.json package-lock.json
*.log *.log

View file

@ -312,7 +312,7 @@ export const commonOpts = {
logLevel: "info", logLevel: "info",
bundle: true, bundle: true,
watch, watch,
minify: !watch, minify: !watch && !IS_REPORTER,
sourcemap: watch ? "inline" : "", sourcemap: watch ? "inline" : "",
legalComments: "linked", legalComments: "linked",
banner, banner,

View file

@ -215,7 +215,7 @@ page.on("console", async e => {
switch (tag) { switch (tag) {
case "WebpackInterceptor:": 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; if (!patchFailMatch) break;
console.error(await getText()); console.error(await getText());
@ -226,7 +226,7 @@ page.on("console", async e => {
plugin, plugin,
type, type,
id, id,
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"), match: regex,
error: await maybeGetError(e.args()[3]) error: await maybeGetError(e.args()[3])
}); });
@ -292,7 +292,7 @@ page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => { page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return; 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); console.error("[Page Error]", e.message);
report.otherErrors.push(e.message); report.otherErrors.push(e.message);
} else { } else {

View file

@ -23,6 +23,7 @@ export * as Util from "./utils";
export * as QuickCss from "./utils/quickCss"; export * as QuickCss from "./utils/quickCss";
export * as Updater from "./utils/updater"; export * as Updater from "./utils/updater";
export * as Webpack from "./webpack"; export * as Webpack from "./webpack";
export * as WebpackPatcher from "./webpack/patchWebpack";
export { PlainSettings, Settings }; export { PlainSettings, Settings };
import "./utils/quickCss"; import "./utils/quickCss";

View file

@ -32,9 +32,10 @@ export interface Settings {
autoUpdate: boolean; autoUpdate: boolean;
autoUpdateNotification: boolean, autoUpdateNotification: boolean,
useQuickCss: boolean; useQuickCss: boolean;
eagerPatches: boolean;
enabledThemes: string[];
enableReactDevtools: boolean; enableReactDevtools: boolean;
themeLinks: string[]; themeLinks: string[];
enabledThemes: string[];
frameless: boolean; frameless: boolean;
transparent: boolean; transparent: boolean;
winCtrlQ: boolean; winCtrlQ: boolean;
@ -81,6 +82,7 @@ const DefaultSettings: Settings = {
autoUpdateNotification: true, autoUpdateNotification: true,
useQuickCss: true, useQuickCss: true,
themeLinks: [], themeLinks: [],
eagerPatches: IS_REPORTER,
enabledThemes: [], enabledThemes: [],
enableReactDevtools: false, enableReactDevtools: false,
frameless: false, frameless: false,

View file

@ -23,35 +23,61 @@ if (IS_DEV || IS_REPORTER) {
var logger = new Logger("Tracer", "#FFD166"); var logger = new Logger("Tracer", "#FFD166");
} }
const noop = function () { }; export const beginTrace = !(IS_DEV || IS_REPORTER) ? () => { } :
export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
function beginTrace(name: string, ...args: any[]) { function beginTrace(name: string, ...args: any[]) {
if (name in traces) if (name in traces) {
throw new Error(`Trace ${name} already exists!`); throw new Error(`Trace ${name} already exists!`);
}
traces[name] = [performance.now(), args]; traces[name] = [performance.now(), args];
}; };
export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) { export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
function finishTrace(name: string) {
const end = performance.now(); const end = performance.now();
const [start, args] = traces[name]; const [start, args] = traces[name];
delete 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 Func = (...args: any[]) => any;
type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string; type TraceNameMapper<F extends Func> = (...args: Parameters<F>) => string;
const noopTracer = function noopTracerWithResults<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) {
<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>) => 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) export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer ? noopTracer
: function traceFunction<F extends Func>(name: string, f: F, mapper?: TraceNameMapper<F>): F { : 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; const traceName = mapper?.(...args) ?? name;
beginTrace(traceName, ...arguments); beginTrace(traceName, ...arguments);

View file

@ -8,23 +8,27 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { wreq } from "@webpack"; import { wreq } from "@webpack";
import { AnyModuleFactory, ModuleFactory } from "webpack";
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
export async function loadLazyChunks() { export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
try { try {
LazyChunkLoaderLogger.log("Loading all chunks..."); LazyChunkLoaderLogger.log("Loading all chunks...");
const validChunks = new Set<number>(); const validChunks = new Set<PropertyKey>();
const invalidChunks = new Set<number>(); const invalidChunks = new Set<PropertyKey>();
const deferredRequires = new Set<number>(); 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); const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);
// True if resolved, false otherwise // True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>; 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); const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false; let foundCssDebuggingLoad = false;
@ -34,12 +38,15 @@ export async function loadLazyChunks() {
const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&")); const hasCssDebuggingLoad = foundCssDebuggingLoad ? false : (foundCssDebuggingLoad = factoryCode.includes(".cssDebuggingEnabled&&"));
const lazyChunks = factoryCode.matchAll(LazyChunkRegex); const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: number[], entryPoint: number]>(); const validChunkGroups = new Set<[chunkIds: PropertyKey[], entryPoint: PropertyKey]>();
const shouldForceDefer = false; const shouldForceDefer = false;
await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { 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) { if (chunkIds.length === 0) {
return; return;
@ -74,7 +81,8 @@ export async function loadLazyChunks() {
} }
if (!invalidChunkGroup) { 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( await Promise.all(
Array.from(validChunkGroups) Array.from(validChunkGroups)
.map(([chunkIds]) => .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; continue;
} }
if (wreq.m[entryPoint]) wreq(entryPoint as any); if (wreq.m[entryPoint]) wreq(entryPoint);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -122,41 +130,44 @@ export async function loadLazyChunks() {
}, 0); }, 0);
} }
Webpack.factoryListeners.add(factory => { function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false; let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); searchAndLoadLazyChunks(String(factory))
.then(() => isResolved = true)
.catch(() => isResolved = true);
chunksSearchPromises.push(() => isResolved); chunksSearchPromises.push(() => isResolved);
}); }
Webpack.factoryListeners.add(factoryListener);
for (const factoryId in wreq.m) { for (const factoryId in wreq.m) {
let isResolved = false; factoryListener(wreq.m[factoryId]);
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
} }
await chunksSearchingDone; await chunksSearchingDone;
Webpack.factoryListeners.delete(factoryListener);
// Require deferred entry points // Require deferred entry points
for (const deferredRequire of deferredRequires) { 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 // 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: // 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]; const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue; 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"); 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 => { const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id)); return !(validChunks.has(id) || invalidChunks.has(id));
}); });
@ -166,12 +177,9 @@ export async function loadLazyChunks() {
.then(r => r.text()) .then(r => r.text())
.then(t => t.includes("importScripts(")); .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) { if (!isWorkerAsset) {
await wreq.e(id as any); await wreq.e(id);
// 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);
} }
})); }));

View file

@ -6,20 +6,35 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { patches } from "plugins"; import { addPatch, patches } from "plugins";
import { loadLazyChunks } from "./loadLazyChunks"; import { loadLazyChunks } from "./loadLazyChunks";
const ReporterLogger = new Logger("Reporter");
async function runReporter() { async function runReporter() {
const ReporterLogger = new Logger("Reporter");
try { try {
ReporterLogger.log("Starting test..."); ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void; let loadLazyChunksResolve: (value: void) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r); 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; await loadLazyChunksDone;
for (const patch of patches) { 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) { for (const [searchType, args] of Webpack.lazyWebpackSearchHistory) {
let method = searchType; 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);

7
src/globals.d.ts vendored
View file

@ -64,13 +64,8 @@ declare global {
export var Vesktop: any; export var Vesktop: any;
export var VesktopNative: any; export var VesktopNative: any;
interface Window { interface Window extends Record<PropertyKey, any> {
webpackChunkdiscord_app: {
push(chunk: any): any;
pop(): any;
};
_: LoDashStatic; _: LoDashStatic;
[k: string]: any;
} }
} }

View file

@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types"; import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "webpack";
const settings = definePluginSettings({ const settings = definePluginSettings({
disableAnalytics: { disableAnalytics: {
@ -81,9 +82,9 @@ export default definePlugin({
Object.defineProperty(Function.prototype, "g", { Object.defineProperty(Function.prototype, "g", {
configurable: true, configurable: true,
set(v: any) { set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
Object.defineProperty(this, "g", { Object.defineProperty(this, "g", {
value: v, value: globalObj,
configurable: true, configurable: true,
enumerable: true, enumerable: true,
writable: true writable: true
@ -92,11 +93,11 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance. // 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 // 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(); 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; return;
} }
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0]; const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
if (!assetPath) { if (!assetPath) {
return; return;
} }
@ -106,7 +107,8 @@ export default definePlugin({
srcRequest.send(); srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance // 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; return;
} }

View file

@ -82,6 +82,8 @@ function makeShortcuts() {
wp: Webpack, wp: Webpack,
wpc: { getter: () => Webpack.cache }, wpc: { getter: () => Webpack.cache },
wreq: { getter: () => Webpack.wreq }, wreq: { getter: () => Webpack.wreq },
wpPatcher: { getter: () => Vencord.WebpackPatcher },
wpInstances: { getter: () => Vencord.WebpackPatcher.allWebpackInstances },
wpsearch: search, wpsearch: search,
wpex: extract, wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!), wpexs: (code: string) => extract(findModuleId(code)!),

View file

@ -160,7 +160,7 @@ function initWs(isManual = false) {
return reply("Expected exactly one 'find' matches, found " + keys.length); return reply("Expected exactly one 'find' matches, found " + keys.length);
const mod = candidates[keys[0]]; const mod = candidates[keys[0]];
let src = String(mod.original ?? mod).replaceAll("\n", ""); let src = String(mod).replaceAll("\n", "");
if (src.startsWith("function(")) { if (src.startsWith("function(")) {
src = "0," + src; src = "0," + src;

View file

@ -28,7 +28,7 @@ import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/Messag
import { Settings, SettingsStore } from "@api/Settings"; import { Settings, SettingsStore } from "@api/Settings";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches"; import { canonicalizeFind, canonicalizeReplacement } from "@utils/patches";
import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types"; import { Patch, Plugin, PluginDef, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common"; import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types"; import { FluxEvents } from "@webpack/types";
@ -58,7 +58,7 @@ export function isPluginEnabled(p: string) {
) ?? false; ) ?? false;
} }
export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) { export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string, pluginPath = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`) {
const patch = newPatch as Patch; const patch = newPatch as Patch;
patch.plugin = pluginName; patch.plugin = pluginName;
@ -74,10 +74,12 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
patch.replacement = [patch.replacement]; patch.replacement = [patch.replacement];
} }
for (const replacement of patch.replacement) {
canonicalizeReplacement(replacement, pluginPath);
if (IS_REPORTER) { if (IS_REPORTER) {
patch.replacement.forEach(r => { delete replacement.predicate;
delete r.predicate; }
});
} }
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate()); patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());

View file

@ -16,7 +16,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export const WEBPACK_CHUNK = "webpackChunkdiscord_app";
export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React";
export const VENBOT_USER_ID = "1017176847865352332"; export const VENBOT_USER_ID = "1017176847865352332";
export const VENCORD_GUILD_ID = "1015060230222131221"; export const VENCORD_GUILD_ID = "1015060230222131221";

View file

@ -100,6 +100,11 @@ export function pluralise(amount: number, singular: string, plural = singular +
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`; return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
} }
export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {
if (args.some(arg => arg == null)) return "";
return String.raw({ raw: strings }, ...args);
}
export function tryOrElse<T>(func: () => T, fallback: T): T { export function tryOrElse<T>(func: () => T, fallback: T): T {
try { try {
const res = func(); const res = func();

View file

@ -41,16 +41,17 @@ export function canonicalizeMatch<T extends RegExp | string>(match: T): T {
} }
const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`); const canonSource = partialCanon.replaceAll("\\i", String.raw`(?:[A-Za-z_$][\w$]*)`);
return new RegExp(canonSource, match.flags) as T; const canonRegex = new RegExp(canonSource, match.flags);
canonRegex.toString = match.toString.bind(match);
return canonRegex as T;
} }
export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginName: string): T { export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginPath: string): T {
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
if (typeof replace !== "function") if (typeof replace !== "function")
return replace.replaceAll("$self", self) as T; return replace.replaceAll("$self", pluginPath) as T;
return ((...args) => replace(...args).replaceAll("$self", self)) as T; return ((...args) => replace(...args).replaceAll("$self", pluginPath)) as T;
} }
export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) { export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>, canonicalize: (value: T) => T) {
@ -65,12 +66,12 @@ export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>
return descriptor; return descriptor;
} }
export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "match" | "replace">, plugin: string) { export function canonicalizeReplacement(replacement: Pick<PatchReplacement, "match" | "replace">, pluginPath: string) {
const descriptors = Object.getOwnPropertyDescriptors(replacement); const descriptors = Object.getOwnPropertyDescriptors(replacement);
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch); descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
descriptors.replace = canonicalizeDescriptor( descriptors.replace = canonicalizeDescriptor(
descriptors.replace, descriptors.replace,
replace => canonicalizeReplace(replace, plugin), replace => canonicalizeReplace(replace, pluginPath),
); );
Object.defineProperties(replacement, descriptors); Object.defineProperties(replacement, descriptors);
} }

View file

@ -43,6 +43,10 @@ export interface PatchReplacement {
replace: string | ReplaceFn; replace: string | ReplaceFn;
/** A function which returns whether this patch replacement should be applied */ /** A function which returns whether this patch replacement should be applied */
predicate?(): boolean; predicate?(): boolean;
/** The minimum build number for this patch to be applied */
fromBuild?: number;
/** The maximum build number for this patch to be applied */
toBuild?: number;
} }
export interface Patch { export interface Patch {
@ -59,6 +63,10 @@ export interface Patch {
group?: boolean; group?: boolean;
/** A function which returns whether this patch should be applied */ /** A function which returns whether this patch should be applied */
predicate?(): boolean; predicate?(): boolean;
/** The minimum build number for this patch to be applied */
fromBuild?: number;
/** The maximum build number for this patch to be applied */
toBuild?: number;
} }
export interface PluginAuthor { export interface PluginAuthor {

View file

@ -25,7 +25,7 @@ export const Menu = {} as t.Menu;
// Relies on .name properties added by the MenuItemDemanglerAPI // Relies on .name properties added by the MenuItemDemanglerAPI
waitFor(m => m.name === "MenuCheckboxItem", (_, id) => { waitFor(m => m.name === "MenuCheckboxItem", (_, id) => {
// we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module // we have to do this manual require by ID because m is in this case the MenuCheckBoxItem instead of the entire module
const module = wreq(id as any); const module = wreq(id);
for (const e of Object.values(module)) { for (const e of Object.values(module)) {
if (typeof e === "function" && e.name.startsWith("Menu")) { if (typeof e === "function" && e.name.startsWith("Menu")) {

View file

@ -18,3 +18,4 @@
export * as Common from "./common"; export * as Common from "./common";
export * from "./webpack"; export * from "./webpack";
export * from "./wreq.d";

View file

@ -1,189 +1,366 @@
/* /*
* Vencord, a modification for Discord's desktop app * Vencord, a Discord client mod
* Copyright (c) 2022 Vendicated and contributors * Copyright (c) 2024 Vendicated, Nuckyz, and contributors
* * SPDX-License-Identifier: GPL-3.0-or-later
* This program is free software: you can redistribute it and/or modify */
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { WEBPACK_CHUNK } from "@utils/constants"; import { Settings } from "@api/Settings";
import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { interpolateIfDefined } from "@utils/misc";
import { canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { PatchReplacement } from "@utils/types";
import { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer"; import { traceFunctionWithResults } from "../debug/Tracer";
import { patches } from "../plugins"; import { patches } from "../plugins";
import { _initWebpack, _shouldIgnoreModule, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from "."; import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, MaybeWrappedModuleFactory, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from ".";
export const SYM_ORIGINAL_FACTORY = Symbol("WebpackPatcher.originalFactory");
export const SYM_PATCHED_SOURCE = Symbol("WebpackPatcher.patchedSource");
export const SYM_PATCHED_BY = Symbol("WebpackPatcher.patchedBy");
/** A set with all the Webpack instances */
export const allWebpackInstances = new Set<AnyWebpackRequire>();
export const patchTimings = [] as Array<[plugin: string, moduleId: PropertyKey, match: string | RegExp, totalTime: number]>;
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
let wreqFallbackApplied = false;
/** Whether we should be patching factories.
*
* This should be disabled if we start searching for the module to get the build number, and then resumed once it's done.
* */
let shouldPatchFactories = true;
let webpackChunk: any[]; export const getBuildNumber = makeLazy(() => {
try {
shouldPatchFactories = false;
// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed try {
// This way we can patch the factory of everything being pushed to the modules array if (wreq.m[128014]?.toString().includes("Trying to open a changelog for an invalid build number")) {
Object.defineProperty(window, WEBPACK_CHUNK, { const hardcodedGetBuildNumber = wreq(128014).b as () => number;
configurable: true,
get: () => webpackChunk, if (typeof hardcodedGetBuildNumber === "function" && typeof hardcodedGetBuildNumber() === "number") {
set: v => { return hardcodedGetBuildNumber();
if (v?.push) {
if (!v.push.$$vencordOriginal) {
logger.info(`Patching ${WEBPACK_CHUNK}.push`);
patchPush(v);
// @ts-ignore
delete window[WEBPACK_CHUNK];
window[WEBPACK_CHUNK] = v;
} }
} }
} catch { }
webpackChunk = v; const moduleId = findModuleId("Trying to open a changelog for an invalid build number");
if (moduleId == null) {
return -1;
}
const exports = Object.values<ModuleExports>(wreq(moduleId));
if (exports.length !== 1 || typeof exports[0] !== "function") {
return -1;
}
const buildNumber = exports[0]();
return typeof buildNumber === "number" ? buildNumber : -1;
} catch {
return -1;
} finally {
shouldPatchFactories = true;
} }
}); });
// wreq.m is the webpack module factory. type Define = typeof Reflect.defineProperty;
// normally, this is populated via webpackGlobal.push, which we patch below. const define: Define = (target, p, attributes) => {
// However, Discord has their .m prepopulated. if (Object.hasOwn(attributes, "value")) {
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories attributes.writable = true;
Object.defineProperty(Function.prototype, "m", { }
configurable: true,
set(v: any) { return Reflect.defineProperty(target, p, {
Object.defineProperty(this, "m", {
value: v,
configurable: true, configurable: true,
enumerable: true, enumerable: true,
writable: true ...attributes
}); });
};
// When using react devtools or other extensions, we may also catch their webpack here. export function getOriginalFactory(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
// This ensures we actually got the right one const moduleFactory = webpackRequire.m[id];
return (moduleFactory?.[SYM_ORIGINAL_FACTORY] ?? moduleFactory) as AnyModuleFactory | undefined;
}
export function getFactoryPatchedSource(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
return webpackRequire.m[id]?.[SYM_PATCHED_SOURCE];
}
export function getFactoryPatchedBy(id: PropertyKey, webpackRequire = wreq as AnyWebpackRequire) {
return webpackRequire.m[id]?.[SYM_PATCHED_BY];
}
// wreq.m is the Webpack object containing module factories. It is pre-populated with module factories, and is also populated via webpackGlobal.push
// We use this setter to intercept when wreq.m is defined and apply the patching in its module factories.
// We wrap wreq.m with our proxy, which is responsible for patching the module factories when they are set, or defining getters for the patched versions.
// If this is the main Webpack, we also set up the internal references to WebpackRequire.
define(Function.prototype, "m", {
enumerable: false,
set(this: AnyWebpackRequire, originalModules: AnyWebpackRequire["m"]) {
define(this, "m", { value: originalModules });
// Ensure this is one of Discord main Webpack instances.
// We may catch Discord bundled libs, React Devtools or other extensions Webpack instances here.
const { stack } = new Error(); const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || Array.isArray(v)) { if (!stack?.includes("http") || stack.match(/at \d+? \(/) || !String(this).includes("exports:{}")) {
return; return;
} }
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""; const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
logger.info("Found Webpack module factory", fileName); logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
patchFactories(v); allWebpackInstances.add(this);
// Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property. // Define a setter for the ensureChunk property of WebpackRequire. Only the main Webpack (which is the only that includes chunk loading) has this property.
// So if the setter is called, this means we can initialize the internal references to WebpackRequire. // So if the setter is called, this means we can initialize the internal references to WebpackRequire.
Object.defineProperty(this, "p", { define(this, "e", {
configurable: true, enumerable: false,
set(this: WebpackInstance, bundlePath: string) {
Object.defineProperty(this, "p", {
value: bundlePath,
configurable: true,
enumerable: true,
writable: true
});
set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) {
define(this, "e", { value: ensureChunk });
clearTimeout(setterTimeout); clearTimeout(setterTimeout);
if (bundlePath !== "/assets/") return;
logger.info(`Main Webpack found in ${fileName}, initializing internal references to WebpackRequire`); logger.info("Main WebpackInstance found" + interpolateIfDefined` in ${fileName}` + ", initializing internal references to WebpackRequire");
_initWebpack(this); _initWebpack(this);
for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(this);
}
} }
}); });
// setImmediate to clear this property setter if this is not the main Webpack. // setImmediate to clear this property setter if this is not the main Webpack.
// If this is the main Webpack, wreq.p will always be set before the timeout runs. // If this is the main Webpack, wreq.e will always be set before the timeout runs.
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0); const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "e"), 0);
// Patch the pre-populated factories
for (const id in originalModules) {
if (updateExistingFactory(originalModules, id, originalModules[id], true)) {
continue;
}
notifyFactoryListeners(originalModules[id]);
defineModulesFactoryGetter(id, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(id, originalModules[id]) : originalModules[id]);
}
define(originalModules, Symbol.toStringTag, {
value: "ModuleFactories",
enumerable: false
});
// The proxy responsible for patching the module factories when they are set, or defining getters for the patched versions
const proxiedModuleFactories = new Proxy(originalModules, moduleFactoriesHandler);
/*
If Webpack ever decides to set module factories using the variable of the modules object directly, instead of wreq.m, switch the proxy to the prototype
Reflect.setPrototypeOf(originalModules, new Proxy(originalModules, moduleFactoriesHandler));
*/
define(this, "m", { value: proxiedModuleFactories });
} }
}); });
function patchPush(webpackGlobal: any) { const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
function handlePush(chunk: any) { /*
try { If Webpack ever decides to set module factories using the variable of the modules object directly instead of wreq.m, we need to switch the proxy to the prototype
patchFactories(chunk[1]); and that requires defining additional traps for keeping the object working
} catch (err) {
logger.error("Error in handlePush", err); // Proxies on the prototype don't intercept "get" when the property is in the object itself. But in case it isn't we need to return undefined,
// to avoid Reflect.get having no effect and causing a stack overflow
get(target, p, receiver) {
return undefined;
},
// Same thing as get
has(target, p) {
return false;
},
*/
// The set trap for patching or defining getters for the module factories when new module factories are loaded
set(target, p, newValue, receiver) {
if (updateExistingFactory(target, p, newValue)) {
return true;
} }
return handlePush.$$vencordOriginal.call(webpackGlobal, chunk); notifyFactoryListeners(newValue);
defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue);
return true;
}
};
/**
* Update a factory that exists in any Webpack instance with a new original factory.
*
* @target The module factories where this new original factory is being set
* @param id The id of the module
* @param newFactory The new original factory
* @param ignoreExistingInTarget Whether to ignore checking if the factory already exists in the moduleFactoriesTarget
* @returns Whether the original factory was updated, or false if it doesn't exist in any Webpack instance
*/
function updateExistingFactory(moduleFactoriesTarget: AnyWebpackRequire["m"], id: PropertyKey, newFactory: AnyModuleFactory, ignoreExistingInTarget: boolean = false) {
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
let moduleFactoriesWithFactory: AnyWebpackRequire["m"] | undefined;
for (const wreq of allWebpackInstances) {
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue;
if (Object.hasOwn(wreq.m, id)) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id);
moduleFactoriesWithFactory = wreq.m;
break;
}
} }
handlePush.$$vencordOriginal = webpackGlobal.push; if (existingFactory != null) {
handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal); // If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required.
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` // So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original,
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. // and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
// If we then repatched the new push, we would end up with recursive patching, which leads to our patches
// being applied multiple times.
// Thus, override bind to use the original push
handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args);
Object.defineProperty(webpackGlobal, "push", { if (moduleFactoriesWithFactory !== moduleFactoriesTarget) {
configurable: true, Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory);
get: () => handlePush,
set(v) {
handlePush.$$vencordOriginal = v;
} }
});
// Persist patched source and patched by in the new original factory, if the patched one has already been required
if (IS_DEV && existingFactory.value != null) {
newFactory[SYM_PATCHED_SOURCE] = existingFactory.value[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = existingFactory.value[SYM_PATCHED_BY];
}
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget);
}
return false;
} }
let webpackNotInitializedLogged = false; /**
* Notify all factory listeners.
function patchFactories(factories: Record<string, (module: any, exports: any, require: WebpackInstance) => void>) { *
for (const id in factories) { * @param factory The original factory to notify for
let mod = factories[id]; */
function notifyFactoryListeners(factory: AnyModuleFactory) {
const originalMod = mod; for (const factoryListener of factoryListeners) {
const patchedBy = new Set();
const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) {
if (wreq == null && IS_DEV) {
if (!webpackNotInitializedLogged) {
webpackNotInitializedLogged = true;
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
}
return void originalMod(module, exports, require);
}
try { try {
mod(module, exports, require); factoryListener(factory);
} catch (err) { } catch (err) {
// Just rethrow discord errors logger.error("Error in Webpack factory listener:\n", err, factoryListener);
if (mod === originalMod) throw err; }
}
}
logger.error("Error in patched module", err); /**
return void originalMod(module, exports, require); * Define the getter for returning the patched version of the module factory.
*
* If eagerPatches is enabled, the factory argument should already be the patched version, else it will be the original
* and only be patched when accessed for the first time.
*
* @param id The id of the module
* @param factory The original or patched module factory
*/
function defineModulesFactoryGetter(id: PropertyKey, factory: MaybeWrappedModuleFactory) {
const descriptor: PropertyDescriptor = {
get() {
// SYM_ORIGINAL_FACTORY means the factory is already patched
if (!shouldPatchFactories || factory[SYM_ORIGINAL_FACTORY] != null) {
return factory;
}
return (factory = wrapAndPatchFactory(id, factory));
},
set(newFactory: MaybeWrappedModuleFactory) {
if (IS_DEV) {
newFactory[SYM_PATCHED_SOURCE] = factory[SYM_PATCHED_SOURCE];
newFactory[SYM_PATCHED_BY] = factory[SYM_PATCHED_BY];
}
if (factory[SYM_ORIGINAL_FACTORY] != null) {
factory.toString = newFactory.toString.bind(newFactory);
factory[SYM_ORIGINAL_FACTORY] = newFactory;
} else {
factory = newFactory;
}
}
};
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
// have the patched version
for (const wreq of allWebpackInstances) {
define(wreq.m, id, descriptor);
}
}
/**
* Wraps and patches a module factory.
*
* @param id The id of the module
* @param factory The original or patched module factory
* @returns The wrapper for the patched module factory
*/
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
const [patchedFactory, patchedSource, patchedBy] = patchFactory(id, originalFactory);
const wrappedFactory: WrappedModuleFactory = function (...args) {
// Restore the original factory in all the module factories objects. We want to make sure the original factory is restored properly, no matter what is the Webpack instance
for (const wreq of allWebpackInstances) {
define(wreq.m, id, { value: wrappedFactory[SYM_ORIGINAL_FACTORY] });
}
// eslint-disable-next-line prefer-const
let [module, exports, require] = args;
if (wreq == null) {
if (!wreqFallbackApplied) {
wreqFallbackApplied = true;
// Make sure the require argument is actually the WebpackRequire function
if (typeof require === "function" && require.m != null) {
const { stack } = new Error();
const webpackInstanceFileName = stack?.match(/\/assets\/(.+?\.js)/)?.[1];
logger.warn(
"WebpackRequire was not initialized, falling back to WebpackRequire passed to the first called patched module factory (" +
`id: ${String(id)}` + interpolateIfDefined`, WebpackInstance origin: ${webpackInstanceFileName}` +
")"
);
_initWebpack(require as WebpackRequire);
} else if (IS_DEV) {
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
} else if (IS_DEV) {
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
}
}
let factoryReturn: unknown;
try {
// Call the patched factory
factoryReturn = patchedFactory.apply(this, args);
} catch (err) {
// Just re-throw Discord errors
if (patchedFactory === wrappedFactory[SYM_ORIGINAL_FACTORY]) {
throw err;
}
logger.error("Error in patched module factory:\n", err);
return wrappedFactory[SYM_ORIGINAL_FACTORY].apply(this, args);
} }
exports = module.exports; exports = module.exports;
if (exports == null) {
return factoryReturn;
}
if (!exports) return; if (typeof require === "function") {
if (require.c) {
const shouldIgnoreModule = _shouldIgnoreModule(exports); const shouldIgnoreModule = _shouldIgnoreModule(exports);
if (shouldIgnoreModule) { if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, id, { Object.defineProperty(require.c, id, {
value: require.c[id], value: require.c[id],
enumerable: false, enumerable: false,
configurable: true, configurable: true,
writable: true writable: true
}); });
}
return; return factoryReturn;
} }
} }
@ -195,11 +372,12 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
} }
} }
for (const [filter, callback] of subscriptions) { for (const [filter, callback] of waitForSubscriptions) {
try { try {
if (exports && filter(exports)) { if (filter(exports)) {
subscriptions.delete(filter); waitForSubscriptions.delete(filter);
callback(exports, id); callback(exports, id);
continue;
} }
if (typeof exports !== "object") { if (typeof exports !== "object") {
@ -207,66 +385,114 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
} }
for (const exportKey in exports) { for (const exportKey in exports) {
if (exports[exportKey] && filter(exports[exportKey])) { const exportValue = exports[exportKey];
subscriptions.delete(filter);
callback(exports[exportKey], id); if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, id);
break;
} }
} }
} catch (err) { } catch (err) {
logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
} as any as { toString: () => string, original: any, (...args: any[]): void; $$vencordPatchedSource?: string; };
factory.toString = originalMod.toString.bind(originalMod);
factory.original = originalMod;
for (const factoryListener of factoryListeners) {
try {
factoryListener(originalMod);
} catch (err) {
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
} }
} }
// Discords Webpack chunks for some ungodly reason contain random return factoryReturn;
// newlines. Cyn recommended this workaround and it seems to work fine, };
// however this could potentially break code, so if anything goes weird,
// this is probably why. wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
// Additionally, `[actual newline]` is one less char than "\n", so if Discord wrappedFactory[SYM_ORIGINAL_FACTORY] = originalFactory;
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues. if (IS_DEV && patchedFactory !== originalFactory) {
// wrappedFactory[SYM_PATCHED_SOURCE] = patchedSource;
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, wrappedFactory[SYM_PATCHED_BY] = patchedBy;
let code: string = "0," + mod.toString().replaceAll("\n", ""); originalFactory[SYM_PATCHED_SOURCE] = patchedSource;
originalFactory[SYM_PATCHED_BY] = patchedBy;
}
// @ts-expect-error Allow GC to get into action, if possible
originalFactory = undefined;
return wrappedFactory;
}
/**
* Patches a module factory.
*
* @param id The id of the module
* @param factory The original module factory
* @returns The patched module factory, the patched source of it, and the plugins that patched it
*/
function patchFactory(id: PropertyKey, factory: AnyModuleFactory): [patchedFactory: AnyModuleFactory, patchedSource: string, patchedBy: Set<string>] {
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + String(factory);
let patchedSource = code;
let patchedFactory = factory;
const patchedBy = new Set<string>();
for (let i = 0; i < patches.length; i++) { for (let i = 0; i < patches.length; i++) {
const patch = patches[i]; const patch = patches[i];
const moduleMatches = typeof patch.find === "string" const moduleMatches = typeof patch.find === "string"
? code.includes(patch.find) ? code.includes(patch.find)
: patch.find.test(code); : (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
if (!moduleMatches) continue; if (!moduleMatches) {
continue;
}
patchedBy.add(patch.plugin); const buildNumber = getBuildNumber();
const shouldCheckBuildId = !Settings.eagerPatches && buildNumber !== -1;
if (
shouldCheckBuildId &&
(patch.fromBuild != null && buildNumber < patch.fromBuild) ||
(patch.toBuild != null && buildNumber > patch.toBuild)
) {
continue;
}
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
if (typeof match !== "string" && match.global) {
match.lastIndex = 0;
}
return code.replace(match, replace);
});
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
const previousMod = mod;
const previousCode = code; const previousCode = code;
const previousFactory = factory;
let markedAsPatched = false;
// We change all patch.replacement to array in plugins/index // We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) { for (const replacement of patch.replacement as PatchReplacement[]) {
const lastMod = mod; if (
const lastCode = code; shouldCheckBuildId &&
(replacement.fromBuild != null && buildNumber < replacement.fromBuild) ||
(replacement.toBuild != null && buildNumber > replacement.toBuild)
) {
continue;
}
canonicalizeReplacement(replacement, patch.plugin); // TODO: remove once Vesktop has been updated to use addPatch
if (patch.plugin === "Vesktop") {
canonicalizeReplacement(replacement, "VCDP");
}
const lastCode = code;
const lastFactory = factory;
try { try {
const newCode = executePatch(replacement.match, replacement.replace as string); const [newCode, totalTime] = executePatch(replacement.match, replacement.replace as string);
if (IS_REPORTER) {
patchTimings.push([patch.plugin, id, replacement.match, totalTime]);
}
if (newCode === code) { if (newCode === code) {
if (!patch.noWarn) { if (!patch.noWarn) {
logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${String(id)}): ${replacement.match}`);
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); logger.debug("Function Source:\n", code);
} }
@ -274,9 +500,13 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
mod = previousMod;
code = previousCode; code = previousCode;
patchedFactory = previousFactory;
if (markedAsPatched) {
patchedBy.delete(patch.plugin); patchedBy.delete(patch.plugin);
}
break; break;
} }
@ -284,13 +514,46 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
} }
code = newCode; code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); patchedSource = `// Webpack Module ${String(id)} - Patched by ${[...patchedBy, patch.plugin].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`;
patchedFactory = (0, eval)(patchedSource);
if (!patchedBy.has(patch.plugin)) {
patchedBy.add(patch.plugin);
markedAsPatched = true;
}
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err);
if (IS_DEV) { if (IS_DEV) {
diffErroredPatch(code, lastCode, lastCode.match(replacement.match)!);
}
if (markedAsPatched) {
patchedBy.delete(patch.plugin);
}
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
code = previousCode;
patchedFactory = previousFactory;
break;
}
code = lastCode;
patchedFactory = lastFactory;
}
}
if (!patch.all) {
patches.splice(i--, 1);
}
}
return [patchedFactory, patchedSource, patchedBy];
}
function diffErroredPatch(code: string, lastCode: string, match: RegExpMatchArray) {
const changeSize = code.length - lastCode.length; const changeSize = code.length - lastCode.length;
const match = lastCode.match(replacement.match)!;
// Use 200 surrounding characters of context // Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200); const start = Math.max(0, match.index! - 200);
@ -301,10 +564,10 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
const context = lastCode.slice(start, end); const context = lastCode.slice(start, end);
const patchedContext = code.slice(start, endPatched); const patchedContext = code.slice(start, endPatched);
// inline require to avoid including it in !IS_DEV builds // Inline require to avoid including it in !IS_DEV builds
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
let fmt = "%c %s "; let fmt = "%c %s ";
const elements = [] as string[]; const elements: string[] = [];
for (const d of diff) { for (const d of diff) {
const color = d.removed const color = d.removed
? "red" ? "red"
@ -319,35 +582,4 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
}
patchedBy.delete(patch.plugin);
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
mod = previousMod;
code = previousCode;
break;
}
mod = lastMod;
code = lastCode;
}
}
if (!patch.all) patches.splice(i--, 1);
}
if (IS_DEV) {
if (mod !== originalMod) {
factory.$$vencordPatchedSource = String(mod);
} else if (wreq != null) {
const existingFactory = wreq.m[id];
if (existingFactory != null) {
factory.$$vencordPatchedSource = existingFactory.$$vencordPatchedSource;
}
}
}
}
} }

View file

@ -20,9 +20,9 @@ import { makeLazy, proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact"; import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches"; import { canonicalizeMatch } from "@utils/patches";
import type { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq";
const logger = new Logger("Webpack"); const logger = new Logger("Webpack");
@ -33,8 +33,8 @@ export let _resolveReady: () => void;
*/ */
export const onceReady = new Promise<void>(r => _resolveReady = r); export const onceReady = new Promise<void>(r => _resolveReady = r);
export let wreq: WebpackInstance; export let wreq: WebpackRequire;
export let cache: WebpackInstance["c"]; export let cache: WebpackRequire["c"];
export type FilterFn = (mod: any) => boolean; export type FilterFn = (mod: any) => boolean;
@ -89,16 +89,27 @@ export const filters = {
} }
}; };
export type CallbackFn = (mod: any, id: string) => void; export type CallbackFn = (module: ModuleExports, id: PropertyKey) => void;
export type FactoryListernFn = (factory: AnyModuleFactory) => void;
export const subscriptions = new Map<FilterFn, CallbackFn>(); export const waitForSubscriptions = new Map<FilterFn, CallbackFn>();
export const moduleListeners = new Set<CallbackFn>(); export const moduleListeners = new Set<CallbackFn>();
export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>(); export const factoryListeners = new Set<FactoryListernFn>();
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
export function _initWebpack(webpackRequire: WebpackRequire) {
if (webpackRequire.c == null) {
return;
}
export function _initWebpack(webpackRequire: WebpackInstance) {
wreq = webpackRequire; wreq = webpackRequire;
cache = webpackRequire.c; cache = webpackRequire.c;
Reflect.defineProperty(webpackRequire.c, Symbol.toStringTag, {
value: "ModuleCache",
configurable: true,
writable: true,
enumerable: false
});
} }
// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too // Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too
@ -531,7 +542,7 @@ export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
* @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
* @returns A promise that resolves with a boolean whether the chunks were loaded * @returns A promise that resolves with a boolean whether the chunks were loaded
*/ */
export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = DefaultExtractAndLoadChunksRegex) { export async function extractAndLoadChunks(code: CodeFilter, matcher = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code); const module = findModuleFactory(...code);
if (!module) { if (!module) {
const err = new Error("extractAndLoadChunks: Couldn't find module factory"); const err = new Error("extractAndLoadChunks: Couldn't find module factory");
@ -544,7 +555,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false; return false;
} }
const match = module.toString().match(canonicalizeMatch(matcher)); const match = String(module).match(canonicalizeMatch(matcher));
if (!match) { if (!match) {
const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code"); const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -557,8 +568,9 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
} }
const [, rawChunkIds, entryPointId] = match; const [, rawChunkIds, entryPointId] = match;
if (Number.isNaN(Number(entryPointId))) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number"); if (entryPointId == null) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array or the entry point id");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
// Strict behaviour in DevBuilds to fail early and make sure the issue is found // Strict behaviour in DevBuilds to fail early and make sure the issue is found
@ -568,12 +580,19 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false; return false;
} }
const numEntryPoint = Number(entryPointId);
const entryPoint = Number.isNaN(numEntryPoint) ? entryPointId : numEntryPoint;
if (rawChunkIds) { if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => Number(m[1])); const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map(m => {
const numChunkId = Number(m[1]);
return Number.isNaN(numChunkId) ? m[1] : numChunkId;
});
await Promise.all(chunkIds.map(id => wreq.e(id))); await Promise.all(chunkIds.map(id => wreq.e(id)));
} }
if (wreq.m[entryPointId] == null) { if (wreq.m[entryPoint] == null) {
const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load"); const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
logger.warn(err, "Code:", code, "Matcher:", matcher); logger.warn(err, "Code:", code, "Matcher:", matcher);
@ -584,7 +603,7 @@ export async function extractAndLoadChunks(code: CodeFilter, matcher: RegExp = D
return false; return false;
} }
wreq(Number(entryPointId)); wreq(entryPoint);
return true; return true;
} }
@ -621,7 +640,7 @@ export function waitFor(filter: string | PropsFilter | FilterFn, callback: Callb
if (existing) return void callback(existing, id); if (existing) return void callback(existing, id);
} }
subscriptions.set(filter, callback); waitForSubscriptions.set(filter, callback);
} }
/** /**
@ -637,7 +656,7 @@ export function search(...code: CodeFilter) {
const factories = wreq.m; const factories = wreq.m;
for (const id in factories) { for (const id in factories) {
const factory = factories[id].original ?? factories[id]; const factory = factories[id];
if (stringMatches(factory.toString(), code)) if (stringMatches(factory.toString(), code))
results[id] = factory; results[id] = factory;

211
src/webpack/wreq.d.ts vendored Normal file
View file

@ -0,0 +1,211 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated, Nuckyz and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack";
export type ModuleExports = any;
export type Module = {
id: PropertyKey;
loaded: boolean;
exports: ModuleExports;
};
/** exports can be anything, however initially it is always an empty object */
export type ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void;
export type WebpackQueues = unique symbol | "__webpack_queues__";
export type WebpackExports = unique symbol | "__webpack_exports__";
export type WebpackError = unique symbol | "__webpack_error__";
export type AsyncModulePromise = Promise<ModuleExports> & {
[WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any;
[WebpackExports]: ModuleExports;
[WebpackError]?: any;
};
export type AsyncModuleBody = (
handleAsyncDependencies: (deps: AsyncModulePromise[]) =>
Promise<() => ModuleExports[]> | (() => ModuleExports[]),
asyncResult: (error?: any) => void
) => Promise<void>;
export type ChunkHandlers = {
/**
* Ensures the js file for this chunk is loaded, or starts to load if it's not.
* @param chunkId The chunk id
* @param promises The promises array to add the loading promise to
*/
j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
/**
* Ensures the css file for this chunk is loaded, or starts to load if it's not.
* @param chunkId The chunk id
* @param promises The promises array to add the loading promise to. This array will likely contain the promise of the js file too
*/
css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
};
export type ScriptLoadDone = (event: Event) => void;
// export type OnChunksLoaded = ((this: WebpackRequire, result: any, chunkIds: PropertyKey[] | undefined | null, callback: () => any, priority: number) => any) & {
// /** Check if a chunk has been loaded */
// j: (this: OnChunksLoaded, chunkId: PropertyKey) => boolean;
// };
export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
m: Record<PropertyKey, ModuleFactory>;
/** The module cache, where all modules which have been WebpackRequire'd are stored */
c: Record<PropertyKey, Module>;
// /**
// * Export star. Sets properties of "fromObject" to "toObject" as getters that return the value from "fromObject", like this:
// * @example
// * const fromObject = { a: 1 };
// * Object.keys(fromObject).forEach(key => {
// * if (key !== "default" && !Object.hasOwn(toObject, key)) {
// * Object.defineProperty(toObject, key, {
// * get: () => fromObject[key],
// * enumerable: true
// * });
// * }
// * });
// * @returns fromObject
// */
// es: (this: WebpackRequire, fromObject: AnyRecord, toObject: AnyRecord) => AnyRecord;
/**
* Creates an async module. A module that which has top level await, or requires an export from an async module.
*
* The body function must be an async function. "module.exports" will become an {@link AsyncModulePromise}.
*
* The body function will be called with a function to handle requires that import from an async module, and a function to resolve this async module. An example on how to handle async dependencies:
* @example
* const factory = (module, exports, wreq) => {
* wreq.a(module, async (handleAsyncDependencies, asyncResult) => {
* try {
* const asyncRequireA = wreq(...);
*
* const asyncDependencies = handleAsyncDependencies([asyncRequire]);
* const [requireAResult] = asyncDependencies.then != null ? (await asyncDependencies)() : asyncDependencies;
*
* // Use the required module
* console.log(requireAResult);
*
* // Mark this async module as resolved
* asyncResult();
* } catch(error) {
* // Mark this async module as rejected with an error
* asyncResult(error);
* }
* }, false); // false because our module does not have an await after dealing with the async requires
* }
*/
a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void;
/** getDefaultExport function for compatibility with non-harmony modules */
n: (this: WebpackRequire, exports: any) => () => ModuleExports;
/**
* Create a fake namespace object, useful for faking an __esModule with a default export.
*
* mode & 1: Value is a module id, require it
*
* mode & 2: Merge all properties of value into the namespace
*
* mode & 4: Return value when already namespace object
*
* mode & 16: Return value when it's Promise-like
*
* mode & (8|1): Behave like require
*/
t: (this: WebpackRequire, value: any, mode: number) => any;
/**
* Define getter functions for harmony exports. For every prop in "definiton" (the module exports), set a getter in "exports" for the getter function in the "definition", like this:
* @example
* const exports = {};
* const definition = { exportName: () => someExportedValue };
* for (const key in definition) {
* if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {
* Object.defineProperty(exports, key, {
* get: definition[key],
* enumerable: true
* });
* }
* }
* // exports is now { exportName: someExportedValue } (but each value is actually a getter)
*/
d: (this: WebpackRequire, exports: AnyRecord, definiton: AnyRecord) => void;
/** The chunk handlers, which are used to ensure the files of the chunks are loaded, or load if necessary */
f: ChunkHandlers;
/**
* The ensure chunk function, it ensures a chunk is loaded, or loads if needed.
* Internally it uses the handlers in {@link WebpackRequire.f} to load/ensure the chunk is loaded.
*/
e: (this: WebpackRequire, chunkId: PropertyKey) => Promise<void[]>;
/** Get the filename for the css part of a chunk */
k: (this: WebpackRequire, chunkId: PropertyKey) => string;
/** Get the filename for the js part of a chunk */
u: (this: WebpackRequire, chunkId: PropertyKey) => string;
/** The global object, will likely always be the window */
g: typeof globalThis;
/** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */
hmd: (this: WebpackRequire, module: Module) => any;
/** Shorthand for Object.prototype.hasOwnProperty */
o: typeof Object.prototype.hasOwnProperty;
/**
* Function to load a script tag. "done" is called when the loading has finished or a timeout has occurred.
* "done" will be attached to existing scripts loading if src === url or data-webpack === `${uniqueName}:${key}`,
* so it will be called when that existing script finishes loading.
*/
l: (this: WebpackRequire, url: string, done: ScriptLoadDone, key?: string | number, chunkId?: PropertyKey) => void;
/** Defines __esModule on the exports, marking ES Modules compatibility as true */
r: (this: WebpackRequire, exports: ModuleExports) => void;
/** Node.js module decorator. Decorates a module as a Node.js module */
nmd: (this: WebpackRequire, module: Module) => any;
// /**
// * Register deferred code which will be executed when the passed chunks are loaded.
// *
// * If chunkIds is defined, it defers the execution of the callback and returns undefined.
// *
// * If chunkIds is undefined, and no deferred code exists or can be executed, it returns the value of the result argument.
// *
// * If chunkIds is undefined, and some deferred code can already be executed, it returns the result of the callback function of the last deferred code.
// *
// * When (priority & 1) it will wait for all other handlers with lower priority to be executed before itself is executed.
// */
// O: OnChunksLoaded;
/**
* Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports".
* @returns The exports argument, but now assigned with the exports of the wasm instance
*/
v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise<any>;
/** Bundle public path, where chunk files are stored. Used by other methods which load chunks to obtain the full asset url */
p: string;
/** The runtime id of the current runtime */
j: string;
/** Document baseURI or WebWorker location.href */
b: string;
};
// Utility section for Vencord
export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial<Omit<WebpackRequire, "m">> & {
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
m: Record<PropertyKey, AnyModuleFactory>;
};
/** exports can be anything, however initially it is always an empty object */
export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void) & {
[SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>;
};
export type WrappedModuleFactory = AnyModuleFactory & {
[SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
[SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>;
};
export type MaybeWrappedModuleFactory = AnyModuleFactory | WrappedModuleFactory;
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;