Compare commits

...
Sign in to create a new pull request.

17 commits

Author SHA1 Message Date
Nuckyz
fb6aac19ea
of course 2025-01-30 21:42:11 -03:00
Nuckyz
f067cefd18
Clean it up 2025-01-30 21:07:43 -03:00
Nuckyz
8cd8334ada
Merge branch 'dev' into patcher-rewrite 2025-01-30 20:45:13 -03:00
Nuckyz
e3a378936e
Merge branch 'dev' into patcher-rewrite 2025-01-29 22:33:32 -03:00
Nuckyz
53010b0a8a
Merge branch 'dev' into patcher-rewrite 2025-01-29 17:21:11 -03:00
Nuckyz
7a85742850
attempt to make it faster 2025-01-29 14:45:33 -03:00
Nuckyz
eb5a3b5308
Webpack, not Discord 2025-01-29 14:43:10 -03:00
Nuckyz
8602ec2611
Avoid checking for default twice 2025-01-29 14:41:29 -03:00
Nuckyz
172c372e40
Merge branch 'dev' into patcher-rewrite 2025-01-29 02:21:43 -03:00
Nuckyz
f4c8efe4de
Merge branch 'dev' into patcher-rewrite 2025-01-26 20:36:32 -03:00
Nuckyz
4716b829c9
Merge branch 'dev' into patcher-rewrite 2025-01-24 21:19:50 -03:00
Nuckyz
494ddb8f19
forgot to rename this 2025-01-24 14:44:24 -03:00
Nuckyz
af5b5a0da3
Allow modifying pluginPath in addPatch
This is needed to later make Vesktop use this method
2025-01-24 14:43:34 -03:00
Nuckyz
4df87467a4
Merge branch 'dev' into patcher-rewrite 2025-01-24 00:44:57 -03:00
Nuckyz
1997d613bb
Merge branch 'dev' into patcher-rewrite 2025-01-23 21:06:58 -03:00
Nuckyz
973ab989e0
Remove trailing comma 2025-01-23 00:28:22 -03:00
Nuckyz
35a6a027fa
Rewrite webpack patcher 2025-01-23 00:23:45 -03:00
18 changed files with 805 additions and 345 deletions

View file

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

View file

@ -26,7 +26,7 @@
import { readFileSync } from "fs";
import pup, { JSHandle } from "puppeteer-core";
for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
for (const variable of ["CHROMIUM_BIN"]) {
if (!process.env[variable]) {
console.error(`Missing environment variable ${variable}`);
process.exit(1);
@ -215,7 +215,7 @@ page.on("console", async e => {
switch (tag) {
case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module|took [\d.]+?ms) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;
console.error(await getText());
@ -226,7 +226,7 @@ page.on("console", async e => {
plugin,
type,
id,
match: regex.replace(/\(\?:\[A-Za-z_\$\]\[\\w\$\]\*\)/g, "\\i"),
match: regex,
error: await maybeGetError(e.args()[3])
});
@ -292,7 +292,7 @@ page.on("error", e => console.error("[Error]", e.message));
page.on("pageerror", e => {
if (e.message.includes("Sentry successfully disabled")) return;
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) {
if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module") && !/^.{1,2}$/.test(e.message)) {
console.error("[Page Error]", e.message);
report.otherErrors.push(e.message);
} else {
@ -300,20 +300,9 @@ page.on("pageerror", e => {
}
});
async function reporterRuntime(token: string) {
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);
}
await page.evaluateOnNewDocument(`
if (location.host.endsWith("discord.com")) {
${readFileSync("./dist/browser.js", "utf-8")};
(${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
}
`);

View file

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

View file

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

View file

@ -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) {
export const finishTrace = !(IS_DEV || IS_REPORTER) ? () => 0 :
function finishTrace(name: string) {
const end = performance.now();
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);

View file

@ -8,10 +8,11 @@ import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import * as Webpack from "@webpack";
import { wreq } from "@webpack";
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
import { AnyModuleFactory, ModuleFactory } from "webpack";
export async function loadLazyChunks() {
const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
try {
LazyChunkLoaderLogger.log("Loading all chunks...");
@ -25,6 +26,9 @@ export async function loadLazyChunks() {
// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;
/* This regex loads all language packs which makes webpack finds testing extremely slow, so for now, lets use one which doesnt include those
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i(?:\.\i)?\.bind\(\i,"?([^)]+?)"?(?:,[^)]+?)?\)\)/g);
*/
const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/g);
let foundCssDebuggingLoad = false;
@ -82,7 +86,7 @@ export async function loadLazyChunks() {
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
Promise.all(chunkIds.map(id => wreq.e(id)))
)
);
@ -94,7 +98,7 @@ export async function loadLazyChunks() {
continue;
}
if (wreq.m[entryPoint]) wreq(entryPoint as any);
if (wreq.m[entryPoint]) wreq(entryPoint);
} catch (err) {
console.error(err);
}
@ -122,32 +126,33 @@ export async function loadLazyChunks() {
}, 0);
}
Webpack.factoryListeners.add(factory => {
function factoryListener(factory: AnyModuleFactory | ModuleFactory) {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
chunksSearchPromises.push(() => isResolved);
});
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].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) {
factoryListener(wreq.m[factoryId]);
}
await chunksSearchingDone;
Webpack.factoryListeners.delete(factoryListener);
// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
wreq(deferredRequire);
}
// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as number[];
// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
for (const currentMatch of String(wreq.u).matchAll(/(?:"([\deE]+?)"(?![,}]))|(?:([\deE]+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;
@ -156,7 +161,8 @@ export async function loadLazyChunks() {
if (allChunks.length === 0) throw new Error("Failed to get all chunks");
// Chunks that are not loaded (not used) by Discord code anymore
// Chunks which our regex could not catch to load
// It will always contain WebWorker assets, and also currently contains some language packs which are loaded differently
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});
@ -166,12 +172,9 @@ export async function loadLazyChunks() {
.then(r => r.text())
.then(t => t.includes("importScripts("));
// Loads and requires a chunk
// Loads the chunk. Currently this only happens with the language packs which are loaded differently
if (!isWorkerAsset) {
await wreq.e(id as any);
// Technically, the id of the chunk does not match the entry point
// But, still try it because we have no way to get the actual entry point
if (wreq.m[id]) wreq(id as any);
await wreq.e(id);
}
}));

View file

@ -6,7 +6,7 @@
import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack";
import { patches } from "plugins";
import { addPatch, patches } from "plugins";
import { loadLazyChunks } from "./loadLazyChunks";
@ -16,10 +16,25 @@ async function runReporter() {
try {
ReporterLogger.log("Starting test...");
let loadLazyChunksResolve: (value: void | PromiseLike<void>) => void;
let loadLazyChunksResolve: (value: void) => void;
const loadLazyChunksDone = new Promise<void>(r => loadLazyChunksResolve = r);
Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
// The main patch for starting the reporter chunk loading
addPatch({
find: '"Could not find app-mount"',
replacement: {
match: /(?<="use strict";)/,
replace: "Vencord.Webpack._initReporter();"
}
}, "Vencord Reporter");
// @ts-ignore
Vencord.Webpack._initReporter = function () {
// initReporter is called in the patched entry point of Discord
// setImmediate to only start searching for lazy chunks after Discord initialized the app
setTimeout(() => loadLazyChunks().then(loadLazyChunksResolve), 0);
};
await loadLazyChunksDone;
for (const patch of patches) {
@ -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);

View file

@ -20,6 +20,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { WebpackRequire } from "webpack";
const settings = definePluginSettings({
disableAnalytics: {
@ -81,9 +82,9 @@ export default definePlugin({
Object.defineProperty(Function.prototype, "g", {
configurable: true,
set(v: any) {
set(this: WebpackRequire, globalObj: WebpackRequire["g"]) {
Object.defineProperty(this, "g", {
value: v,
value: globalObj,
configurable: true,
enumerable: true,
writable: true
@ -92,11 +93,11 @@ export default definePlugin({
// Ensure this is most likely the Sentry WebpackInstance.
// Function.g is a very generic property and is not uncommon for another WebpackInstance (or even a React component: <g></g>) to include it
const { stack } = new Error();
if (!(stack?.includes("discord.com") || stack?.includes("discordapp.com")) || !String(this).includes("exports:{}") || this.c != null) {
if (this.c != null || !stack?.includes("http") || !String(this).includes("exports:{}")) {
return;
}
const assetPath = stack?.match(/\/assets\/.+?\.js/)?.[0];
const assetPath = stack.match(/http.+?(?=:\d+?:\d+?$)/m)?.[0];
if (!assetPath) {
return;
}
@ -106,7 +107,8 @@ export default definePlugin({
srcRequest.send();
// Final condition to see if this is the Sentry WebpackInstance
if (!srcRequest.responseText.includes("window.DiscordSentry=")) {
// This is matching window.DiscordSentry=, but without `window` to avoid issues on some proxies
if (!srcRequest.responseText.includes(".DiscordSentry=")) {
return;
}

View file

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

View file

@ -27,7 +27,7 @@ import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendLi
import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover";
import { Settings, SettingsStore } from "@api/Settings";
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 { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@ -57,7 +57,7 @@ export function isPluginEnabled(p: string) {
) ?? 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;
patch.plugin = pluginName;
@ -73,10 +73,12 @@ export function addPatch(newPatch: Omit<Patch, "plugin">, pluginName: string) {
patch.replacement = [patch.replacement];
}
for (const replacement of patch.replacement) {
canonicalizeReplacement(replacement, pluginPath);
if (IS_REPORTER) {
patch.replacement.forEach(r => {
delete r.predicate;
});
delete replacement.predicate;
}
}
patch.replacement = patch.replacement.filter(({ predicate }) => !predicate || predicate());

View file

@ -100,6 +100,11 @@ export function pluralise(amount: number, singular: string, plural = singular +
return amount === 1 ? `${amount} ${singular}` : `${amount} ${plural}`;
}
export function interpolateIfDefined(strings: TemplateStringsArray, ...args: any[]) {
if (args.some(arg => arg == null)) return "";
return strings.reduce((acc, str, i) => `${acc}${str}${args[i] ?? ""}`, "");
}
export function tryOrElse<T>(func: () => T, fallback: T): T {
try {
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$]*)`);
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 {
const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`;
export function canonicalizeReplace<T extends string | ReplaceFn>(replace: T, pluginPath: string): T {
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) {
@ -65,12 +66,12 @@ export function canonicalizeDescriptor<T>(descriptor: TypedPropertyDescriptor<T>
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);
descriptors.match = canonicalizeDescriptor(descriptors.match, canonicalizeMatch);
descriptors.replace = canonicalizeDescriptor(
descriptors.replace,
replace => canonicalizeReplace(replace, plugin),
replace => canonicalizeReplace(replace, pluginPath),
);
Object.defineProperties(replacement, descriptors);
}

View file

@ -43,6 +43,10 @@ export interface PatchReplacement {
replace: string | ReplaceFn;
/** A function which returns whether this patch replacement should be applied */
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 {
@ -59,6 +63,10 @@ export interface Patch {
group?: boolean;
/** A function which returns whether this patch should be applied */
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 {

View file

@ -25,7 +25,7 @@ export const Menu = {} as t.Menu;
// Relies on .name properties added by the MenuItemDemanglerAPI
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
const module = wreq(id as any);
const module = wreq(id);
for (const e of Object.values(module)) {
if (typeof e === "function" && e.name.startsWith("Menu")) {

View file

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

View file

@ -1,189 +1,346 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* 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/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated, Nuckyz, and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { WEBPACK_CHUNK } from "@utils/constants";
import { Settings } from "@api/Settings";
import { makeLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { canonicalizeReplacement } from "@utils/patches";
import { interpolateIfDefined } from "@utils/misc";
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 { _initWebpack, _shouldIgnoreModule, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from ".";
import { _initWebpack, _shouldIgnoreModule, AnyModuleFactory, AnyWebpackRequire, factoryListeners, findModuleId, ModuleExports, moduleListeners, waitForSubscriptions, WebpackRequire, WrappedModuleFactory, wreq } from ".";
const logger = new Logger("WebpackInterceptor", "#8caaee");
let webpackChunk: any[];
/** 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]>;
// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed
// This way we can patch the factory of everything being pushed to the modules array
Object.defineProperty(window, WEBPACK_CHUNK, {
configurable: true,
/** 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;
get: () => webpackChunk,
set: v => {
if (v?.push) {
if (!v.push.$$vencordOriginal) {
logger.info(`Patching ${WEBPACK_CHUNK}.push`);
patchPush(v);
const getBuildNumber = makeLazy(() => {
try {
shouldPatchFactories = false;
// @ts-ignore
delete window[WEBPACK_CHUNK];
window[WEBPACK_CHUNK] = v;
}
const hardcodedModuleAttempt = wreq(128014)?.b;
if (typeof hardcodedModuleAttempt === "function" && typeof hardcodedModuleAttempt() === "number") {
return hardcodedModuleAttempt() as number;
}
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.
// normally, this is populated via webpackGlobal.push, which we patch below.
// However, Discord has their .m prepopulated.
// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories
Object.defineProperty(Function.prototype, "m", {
configurable: true,
type Define = typeof Reflect.defineProperty;
const define: Define = (target, p, attributes) => {
if (Object.hasOwn(attributes, "value")) {
attributes.writable = true;
}
set(v: any) {
Object.defineProperty(this, "m", {
value: v,
return Reflect.defineProperty(target, p, {
configurable: true,
enumerable: true,
writable: true
...attributes
});
};
// When using react devtools or other extensions, we may also catch their webpack here.
// This ensures we actually got the right one
// 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 definining 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();
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;
}
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "";
logger.info("Found Webpack module factory", fileName);
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
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.
Object.defineProperty(this, "p", {
configurable: true,
set(this: WebpackInstance, bundlePath: string) {
Object.defineProperty(this, "p", {
value: bundlePath,
configurable: true,
enumerable: true,
writable: true
});
define(this, "e", {
enumerable: false,
set(this: WebpackRequire, ensureChunk: WebpackRequire["e"]) {
define(this, "e", { value: ensureChunk });
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);
for (const beforeInitListener of beforeInitListeners) {
beforeInitListener(this);
}
}
});
// 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.
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
// If this is the main Webpack, wreq.e will always be set before the timeout runs.
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 definining 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) {
function handlePush(chunk: any) {
try {
patchFactories(chunk[1]);
} catch (err) {
logger.error("Error in handlePush", err);
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
/*
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
and that requires defining additional traps for keeping the object working
// Proxies on the prototype dont 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 the property is not a number, we are not dealing with a module factory
if (Number.isNaN(Number(p))) {
return define(target, p, { value: newValue });
}
return handlePush.$$vencordOriginal.call(webpackGlobal, chunk);
if (updateExistingFactory(target, p, newValue)) {
return true;
}
handlePush.$$vencordOriginal = webpackGlobal.push;
handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal);
// Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));`
// it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush.
// 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);
notifyFactoryListeners(newValue);
defineModulesFactoryGetter(p, Settings.eagerPatches && shouldPatchFactories ? wrapAndPatchFactory(p, newValue) : newValue);
Object.defineProperty(webpackGlobal, "push", {
configurable: true,
get: () => handlePush,
set(v) {
handlePush.$$vencordOriginal = v;
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 (Reflect.getOwnPropertyDescriptor(wreq.m, id) != null) {
existingFactory = Reflect.getOwnPropertyDescriptor(wreq.m, id);
moduleFactoriesWithFactory = wreq.m;
break;
}
}
if (existingFactory != null) {
// If existingFactory exists in any Webpack instance, it's either wrapped in defineModuleFactoryGetter, or it has already been required.
// So define the descriptor of it on this current Webpack instance (if it doesn't exist already), call Reflect.set with the new original,
// and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
if (moduleFactoriesWithFactory !== moduleFactoriesTarget) {
Reflect.defineProperty(moduleFactoriesTarget, id, existingFactory);
}
// Persist $$vencordPatchedSource in the new original factory, if the patched one has already been required
if (IS_DEV && existingFactory.value != null) {
newFactory.$$vencordPatchedSource = existingFactory.value.$$vencordPatchedSource;
}
return Reflect.set(moduleFactoriesTarget, id, newFactory, moduleFactoriesTarget);
}
return false;
}
let webpackNotInitializedLogged = false;
function patchFactories(factories: Record<string, (module: any, exports: any, require: WebpackInstance) => void>) {
for (const id in factories) {
let mod = factories[id];
const originalMod = mod;
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);
}
/**
* Notify all factory listeners.
*
* @param factory The original factory to notify for
*/
function notifyFactoryListeners(factory: AnyModuleFactory) {
for (const factoryListener of factoryListeners) {
try {
mod(module, exports, require);
factoryListener(factory);
} catch (err) {
// Just rethrow discord errors
if (mod === originalMod) throw err;
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
}
}
}
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: WrappedModuleFactory) {
const descriptor: PropertyDescriptor = {
get() {
// $$vencordOriginal means the factory is already patched
if (!shouldPatchFactories || factory.$$vencordOriginal != null) {
return factory;
}
return (factory = wrapAndPatchFactory(id, factory));
},
set(newFactory: AnyModuleFactory) {
if (IS_DEV && factory.$$vencordPatchedSource != null) {
newFactory.$$vencordPatchedSource = factory.$$vencordPatchedSource;
}
if (factory.$$vencordOriginal != null) {
factory.toString = newFactory.toString.bind(newFactory);
factory.$$vencordOriginal = 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 = 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.$$vencordOriginal });
}
// 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.$$vencordOriginal!.apply(this, args);
}
} else if (IS_DEV) {
return wrappedFactory.$$vencordOriginal!.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 === originalFactory) {
throw err;
}
logger.error("Error in patched module factory:\n", err);
return wrappedFactory.$$vencordOriginal!.apply(this, args);
}
exports = module.exports;
if (exports == null) return factoryReturn;
if (!exports) return;
if (require.c) {
if (typeof require === "function") {
const shouldIgnoreModule = _shouldIgnoreModule(exports);
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, id, {
value: require.c[id],
enumerable: false,
configurable: true,
writable: true
});
}
return;
return factoryReturn;
}
}
@ -195,11 +352,13 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
}
}
for (const [filter, callback] of subscriptions) {
for (const [filter, callback] of waitForSubscriptions) {
try {
if (exports && filter(exports)) {
subscriptions.delete(filter);
if (filter(exports)) {
waitForSubscriptions.delete(filter);
callback(exports, id);
continue;
}
if (typeof exports !== "object") {
@ -207,66 +366,101 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
}
for (const exportKey in exports) {
if (exports[exportKey] && filter(exports[exportKey])) {
subscriptions.delete(filter);
callback(exports[exportKey], id);
const exportValue = exports[exportKey];
if (exportValue != null && filter(exportValue)) {
waitForSubscriptions.delete(filter);
callback(exportValue, id);
break;
}
}
} catch (err) {
logger.error("Error while firing callback for Webpack 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);
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
}
}
// Discords Webpack chunks for some ungodly reason contain random
// 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.
// Additionally, `[actual newline]` is one less char than "\n", so if Discord
// ever targets newer browsers, the minifier could potentially use this trick and
// cause issues.
//
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + mod.toString().replaceAll("\n", "");
return factoryReturn;
};
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
wrappedFactory.$$vencordOriginal = originalFactory;
if (IS_DEV && patchedFactory !== originalFactory) {
const patchedSource = String(patchedFactory);
wrappedFactory.$$vencordPatchedSource = patchedSource;
originalFactory.$$vencordPatchedSource = patchedSource;
}
return wrappedFactory;
}
/**
* Patches a module factory.
*
* @param id The id of the module
* @param factory The original module factory
* @returns The patched module factory
*/
function patchFactory(id: PropertyKey, factory: AnyModuleFactory) {
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
let code: string = "0," + String(factory);
let patchedFactory = factory;
const patchedBy = new Set<string>();
for (let i = 0; i < patches.length; i++) {
const patch = patches[i];
const moduleMatches = typeof patch.find === "string"
? code.includes(patch.find)
: patch.find.test(code);
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
if (!moduleMatches) continue;
if (
!Settings.eagerPatches &&
(patch.fromBuild != null && getBuildNumber() !== -1 && getBuildNumber() < patch.fromBuild) ||
(patch.toBuild != null && getBuildNumber() !== -1 && getBuildNumber() > patch.toBuild)
) {
continue;
}
patchedBy.add(patch.plugin);
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
const previousMod = mod;
const executePatch = traceFunctionWithResults(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => {
if (match instanceof RegExp && match.global) {
match.lastIndex = 0;
}
return code.replace(match, replace);
});
const previousCode = code;
const previousFactory = factory;
// We change all patch.replacement to array in plugins/index
for (const replacement of patch.replacement as PatchReplacement[]) {
const lastMod = mod;
const lastCode = code;
if (
!Settings.eagerPatches &&
(replacement.fromBuild != null && getBuildNumber() !== -1 && getBuildNumber() < replacement.fromBuild) ||
(replacement.toBuild != null && getBuildNumber() !== -1 && getBuildNumber() > replacement.toBuild)
) {
continue;
}
canonicalizeReplacement(replacement, patch.plugin);
const lastCode = code;
const lastFactory = factory;
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 (!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) {
logger.debug("Function Source:\n", code);
}
@ -274,8 +468,8 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
mod = previousMod;
code = previousCode;
patchedFactory = previousFactory;
patchedBy.delete(patch.plugin);
break;
}
@ -284,9 +478,9 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
}
code = newCode;
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
patchedFactory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`);
} 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) {
const changeSize = code.length - lastCode.length;
@ -304,7 +498,7 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
// inline require to avoid including it in !IS_DEV builds
const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext);
let fmt = "%c %s ";
const elements = [] as string[];
const elements: string[] = [];
for (const d of diff) {
const color = d.removed
? "red"
@ -325,29 +519,18 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
mod = previousMod;
code = previousCode;
patchedFactory = previousFactory;
break;
}
mod = lastMod;
code = lastCode;
patchedFactory = lastFactory;
}
}
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;
}
}
}
}
return patchedFactory;
}

View file

@ -20,9 +20,9 @@ import { makeLazy, proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/lazyReact";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch } from "@utils/patches";
import type { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer";
import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq";
const logger = new Logger("Webpack");
@ -33,8 +33,8 @@ export let _resolveReady: () => void;
*/
export const onceReady = new Promise<void>(r => _resolveReady = r);
export let wreq: WebpackInstance;
export let cache: WebpackInstance["c"];
export let wreq: WebpackRequire;
export let cache: WebpackRequire["c"];
export type FilterFn = (mod: any) => boolean;
@ -86,16 +86,25 @@ 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 factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
export const factoryListeners = new Set<FactoryListernFn>();
export function _initWebpack(webpackRequire: WebpackInstance) {
export function _initWebpack(webpackRequire: WebpackRequire) {
wreq = webpackRequire;
if (webpackRequire.c == null) return;
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
@ -618,7 +627,7 @@ export function waitFor(filter: string | PropsFilter | FilterFn, callback: Callb
if (existing) return void callback(existing, id);
}
subscriptions.set(filter, callback);
waitForSubscriptions.set(filter, callback);
}
/**
@ -634,7 +643,7 @@ export function search(...code: CodeFilter) {
const factories = wreq.m;
for (const id in factories) {
const factory = factories[id].original ?? factories[id];
const factory = factories[id];
if (stringMatches(factory.toString(), code))
results[id] = factory;

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

@ -0,0 +1,205 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated, Nuckyz and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
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) & {
$$vencordPatchedSource?: string;
};
export type WrappedModuleFactory = AnyModuleFactory & {
$$vencordOriginal?: AnyModuleFactory;
$$vencordPatchedSource?: string;
};
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;