Merge branch 'modules-proxy-patches' into immediate-finds-modules-proxy
This commit is contained in:
commit
22a75c4589
|
@ -41,7 +41,7 @@ const browser = await pup.launch({
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
|
||||||
|
|
||||||
function maybeGetError(handle: JSHandle) {
|
async function maybeGetError(handle: JSHandle) {
|
||||||
return (handle as JSHandle<Error>)?.getProperty("message")
|
return (handle as JSHandle<Error>)?.getProperty("message")
|
||||||
.then(m => m.jsonValue());
|
.then(m => m.jsonValue());
|
||||||
}
|
}
|
||||||
|
@ -285,7 +285,7 @@ async function runtime(token: string) {
|
||||||
Object.defineProperty(navigator, "languages", {
|
Object.defineProperty(navigator, "languages", {
|
||||||
get: function () {
|
get: function () {
|
||||||
return ["en-US", "en"];
|
return ["en-US", "en"];
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monkey patch Logger to not log with custom css
|
// Monkey patch Logger to not log with custom css
|
||||||
|
@ -324,6 +324,9 @@ async function runtime(token: string) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable eagerPatches to make all patches apply regardless of the module being required
|
||||||
|
Vencord.Settings.eagerPatches = true;
|
||||||
|
|
||||||
let wreq: typeof Vencord.Webpack.wreq;
|
let wreq: typeof Vencord.Webpack.wreq;
|
||||||
|
|
||||||
const { canonicalizeMatch, Logger } = Vencord.Util;
|
const { canonicalizeMatch, Logger } = Vencord.Util;
|
||||||
|
@ -383,7 +386,7 @@ async function runtime(token: string) {
|
||||||
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).catch(() => { })))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -395,7 +398,7 @@ async function runtime(token: string) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -456,17 +459,18 @@ async function runtime(token: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
await chunksSearchingDone;
|
await chunksSearchingDone;
|
||||||
|
wreq = wreq!;
|
||||||
|
|
||||||
// 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 string[];
|
const allChunks = [] as string[];
|
||||||
|
|
||||||
// Matches "id" or id:
|
// Matches "id" or id:
|
||||||
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
for (const currentMatch of wreq.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
|
||||||
const id = currentMatch[1] ?? currentMatch[2];
|
const id = currentMatch[1] ?? currentMatch[2];
|
||||||
if (id == null) continue;
|
if (id == null) continue;
|
||||||
|
|
||||||
|
@ -487,17 +491,11 @@ async function runtime(token: string) {
|
||||||
|
|
||||||
// Loads and requires a chunk
|
// Loads and requires a chunk
|
||||||
if (!isWasm) {
|
if (!isWasm) {
|
||||||
await wreq.e(id as any);
|
await wreq.e(id);
|
||||||
if (wreq.m[id]) wreq(id as any);
|
if (wreq.m[id]) wreq(id);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Call the getter for all the values in the modules object
|
|
||||||
// So modules that were not required get patched by our proxy
|
|
||||||
for (const id in wreq!.m) {
|
|
||||||
wreq!.m[id];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
console.log("[PUP_DEBUG]", "Finished loading all chunks!");
|
||||||
|
|
||||||
for (const patch of Vencord.Plugins.patches) {
|
for (const patch of Vencord.Plugins.patches) {
|
||||||
|
|
|
@ -32,9 +32,10 @@ export interface Settings {
|
||||||
autoUpdate: boolean;
|
autoUpdate: boolean;
|
||||||
autoUpdateNotification: boolean,
|
autoUpdateNotification: boolean,
|
||||||
useQuickCss: boolean;
|
useQuickCss: boolean;
|
||||||
enableReactDevtools: boolean;
|
|
||||||
themeLinks: string[];
|
themeLinks: string[];
|
||||||
|
eagerPatches: boolean;
|
||||||
enabledThemes: string[];
|
enabledThemes: string[];
|
||||||
|
enableReactDevtools: boolean;
|
||||||
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: false,
|
||||||
enabledThemes: [],
|
enabledThemes: [],
|
||||||
enableReactDevtools: false,
|
enableReactDevtools: false,
|
||||||
frameless: false,
|
frameless: false,
|
||||||
|
|
|
@ -66,6 +66,11 @@ function VencordSettings() {
|
||||||
title: "Enable React Developer Tools",
|
title: "Enable React Developer Tools",
|
||||||
note: "Requires a full restart"
|
note: "Requires a full restart"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "eagerPatches",
|
||||||
|
title: "Apply Vencord patches before they are needed",
|
||||||
|
note: "Increases startup timing, but may make app usage more fluid. Note that the difference of having this on or off is minimal."
|
||||||
|
},
|
||||||
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
|
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
|
||||||
key: "frameless",
|
key: "frameless",
|
||||||
title: "Disable the window frame",
|
title: "Disable the window frame",
|
||||||
|
|
|
@ -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 = mod.toString().replaceAll("\n", "");
|
||||||
|
|
||||||
if (src.startsWith("function(")) {
|
if (src.startsWith("function(")) {
|
||||||
src = "0," + src;
|
src = "0," + src;
|
||||||
|
|
|
@ -18,3 +18,4 @@
|
||||||
|
|
||||||
export * as Common from "./common";
|
export * as Common from "./common";
|
||||||
export * from "./webpack";
|
export * from "./webpack";
|
||||||
|
export * from "./wreq.d";
|
||||||
|
|
|
@ -4,35 +4,66 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Settings } from "@api/Settings";
|
||||||
import { Logger } from "@utils/Logger";
|
import { Logger } from "@utils/Logger";
|
||||||
import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc";
|
import { UNCONFIGURABLE_PROPERTIES } from "@utils/misc";
|
||||||
import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches";
|
import { canonicalizeMatch, 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 { traceFunction } from "../debug/Tracer";
|
||||||
import { patches } from "../plugins";
|
import { patches } from "../plugins";
|
||||||
import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, waitForSubscriptions, wreq } from ".";
|
import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, moduleListeners, OnChunksLoaded, waitForSubscriptions, WebpackRequire, wreq } from ".";
|
||||||
|
|
||||||
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||||
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
|
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
|
||||||
|
|
||||||
const modulesProxyhandler: ProxyHandler<any> = {
|
const allProxiedModules = new Set<WebpackRequire["m"]>();
|
||||||
|
|
||||||
|
const modulesProxyhandler: ProxyHandler<WebpackRequire["m"]> = {
|
||||||
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
|
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
|
||||||
[propName, (target: any, ...args: any[]) => Reflect[propName](target, ...args)]
|
[propName, (...args: any[]) => Reflect[propName](...args)]
|
||||||
)),
|
)),
|
||||||
get: (target, p: string) => {
|
get: (target, p) => {
|
||||||
const mod = Reflect.get(target, p);
|
const propValue = Reflect.get(target, p);
|
||||||
|
|
||||||
// If the property is not a module id, return the value of it without trying to patch
|
// If the property is not a number, we are not dealing with a module factory
|
||||||
if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod;
|
// $$vencordOriginal means the factory is already patched, $$vencordRequired means it has already been required
|
||||||
|
// and replaced with the original
|
||||||
|
// @ts-ignore
|
||||||
|
if (propValue == null || Number.isNaN(Number(p)) || propValue.$$vencordOriginal != null || propValue.$$vencordRequired === true) {
|
||||||
|
return propValue;
|
||||||
|
}
|
||||||
|
|
||||||
const patchedMod = patchFactory(p, mod);
|
// This patches factories if eagerPatches are disabled
|
||||||
Reflect.set(target, p, patchedMod);
|
const patchedFactory = patchFactory(p, propValue);
|
||||||
|
Reflect.set(target, p, patchedFactory);
|
||||||
|
|
||||||
return patchedMod;
|
return patchedFactory;
|
||||||
|
},
|
||||||
|
set: (target, p, newValue) => {
|
||||||
|
// $$vencordRequired means we are resetting the factory to its original after being required
|
||||||
|
// If the property is not a number, we are not dealing with a module factory
|
||||||
|
if (!Settings.eagerPatches || newValue?.$$vencordRequired === true || Number.isNaN(Number(p))) {
|
||||||
|
return Reflect.set(target, p, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFactory = Reflect.get(target, p);
|
||||||
|
|
||||||
|
// Check if this factory is already patched
|
||||||
|
// @ts-ignore
|
||||||
|
if (existingFactory?.$$vencordOriginal === newValue) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchedFactory = patchFactory(p, newValue);
|
||||||
|
|
||||||
|
// Modules are only patched once, so we need to set the patched factory on all the modules
|
||||||
|
for (const proxiedModules of allProxiedModules) {
|
||||||
|
Reflect.set(proxiedModules, p, patchedFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
set: (target, p, newValue) => Reflect.set(target, p, newValue),
|
|
||||||
ownKeys: target => {
|
ownKeys: target => {
|
||||||
const keys = Reflect.ownKeys(target);
|
const keys = Reflect.ownKeys(target);
|
||||||
for (const key of UNCONFIGURABLE_PROPERTIES) {
|
for (const key of UNCONFIGURABLE_PROPERTIES) {
|
||||||
|
@ -48,7 +79,7 @@ const modulesProxyhandler: ProxyHandler<any> = {
|
||||||
Object.defineProperty(Function.prototype, "O", {
|
Object.defineProperty(Function.prototype, "O", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
||||||
set(onChunksLoaded: any) {
|
set(this: WebpackRequire, onChunksLoaded: WebpackRequire["O"]) {
|
||||||
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
|
// When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here.
|
||||||
// This ensures we actually got the right one
|
// This ensures we actually got the right one
|
||||||
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
|
// this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it
|
||||||
|
@ -59,14 +90,16 @@ Object.defineProperty(Function.prototype, "O", {
|
||||||
delete (Function.prototype as any).O;
|
delete (Function.prototype as any).O;
|
||||||
|
|
||||||
const originalOnChunksLoaded = onChunksLoaded;
|
const originalOnChunksLoaded = onChunksLoaded;
|
||||||
onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) {
|
onChunksLoaded = function (result, chunkIds, callback, priority) {
|
||||||
if (callback != null && initCallbackRegex.test(callback.toString())) {
|
if (callback != null && initCallbackRegex.test(callback.toString())) {
|
||||||
Object.defineProperty(this, "O", {
|
Object.defineProperty(this, "O", {
|
||||||
value: originalOnChunksLoaded,
|
value: originalOnChunksLoaded,
|
||||||
configurable: true
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const wreq = this as WebpackInstance;
|
const wreq = this;
|
||||||
|
|
||||||
const originalCallback = callback;
|
const originalCallback = callback;
|
||||||
callback = function (this: unknown) {
|
callback = function (this: unknown) {
|
||||||
|
@ -85,36 +118,40 @@ Object.defineProperty(Function.prototype, "O", {
|
||||||
}
|
}
|
||||||
|
|
||||||
originalOnChunksLoaded.apply(this, arguments as any);
|
originalOnChunksLoaded.apply(this, arguments as any);
|
||||||
};
|
} as WebpackRequire["O"];
|
||||||
|
|
||||||
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
|
onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded);
|
||||||
|
|
||||||
// Returns whether a chunk has been loaded
|
// Returns whether a chunk has been loaded
|
||||||
Object.defineProperty(onChunksLoaded, "j", {
|
Object.defineProperty(onChunksLoaded, "j", {
|
||||||
set(v) {
|
configurable: true,
|
||||||
|
|
||||||
|
set(v: OnChunksLoaded["j"]) {
|
||||||
|
// @ts-ignore
|
||||||
delete onChunksLoaded.j;
|
delete onChunksLoaded.j;
|
||||||
onChunksLoaded.j = v;
|
onChunksLoaded.j = v;
|
||||||
originalOnChunksLoaded.j = v;
|
originalOnChunksLoaded.j = v;
|
||||||
},
|
}
|
||||||
configurable: true
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(this, "O", {
|
Object.defineProperty(this, "O", {
|
||||||
value: onChunksLoaded,
|
value: onChunksLoaded,
|
||||||
configurable: true
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// wreq.m is the webpack object containing module factories.
|
// wreq.m is the webpack object containing module factories.
|
||||||
// This is pre-populated with modules, and is also populated via webpackGlobal.push
|
// This is pre-populated with module factories, and is also populated via webpackGlobal.push
|
||||||
// The sentry module also has their own webpack with a pre-populated modules object, so this also targets that
|
// The sentry module also has their own webpack with a pre-populated module factories object, so this also targets that
|
||||||
// We replace its prototype with our proxy, which is responsible for returning patched module factories containing our patches
|
// We replace its prototype with our proxy, which is responsible for patching the module factories
|
||||||
Object.defineProperty(Function.prototype, "m", {
|
Object.defineProperty(Function.prototype, "m", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
||||||
set(originalModules: any) {
|
set(this: WebpackRequire, originalModules: WebpackRequire["m"]) {
|
||||||
// When using react devtools or other extensions, we may also catch their webpack here.
|
// When using react devtools or other extensions, we may also catch their webpack here.
|
||||||
// This ensures we actually got the right one
|
// This ensures we actually got the right one
|
||||||
const { stack } = new Error();
|
const { stack } = new Error();
|
||||||
|
@ -122,36 +159,48 @@ Object.defineProperty(Function.prototype, "m", {
|
||||||
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
|
logger.info("Found Webpack module factory", stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? "");
|
||||||
|
|
||||||
// The new object which will contain the factories
|
// The new object which will contain the factories
|
||||||
const modules = Object.assign({}, originalModules);
|
const proxiedModules: WebpackRequire["m"] = {};
|
||||||
|
|
||||||
// Clear the original object so pre-populated factories are patched
|
for (const id in originalModules) {
|
||||||
for (const propName in originalModules) {
|
// If we have eagerPatches enabled we have to patch the pre-populated factories
|
||||||
delete originalModules[propName];
|
if (Settings.eagerPatches) {
|
||||||
|
proxiedModules[id] = patchFactory(id, originalModules[id]);
|
||||||
|
} else {
|
||||||
|
proxiedModules[id] = originalModules[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the original object so pre-populated factories are patched if eagerPatches are disabled
|
||||||
|
delete originalModules[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.setPrototypeOf(originalModules, new Proxy(modules, modulesProxyhandler));
|
// @ts-ignore
|
||||||
|
originalModules.$$proxiedModules = proxiedModules;
|
||||||
|
allProxiedModules.add(proxiedModules);
|
||||||
|
Object.setPrototypeOf(originalModules, new Proxy(proxiedModules, modulesProxyhandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.defineProperty(this, "m", {
|
Object.defineProperty(this, "m", {
|
||||||
value: originalModules,
|
value: originalModules,
|
||||||
configurable: true
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let webpackNotInitializedLogged = false;
|
let webpackNotInitializedLogged = false;
|
||||||
|
|
||||||
function patchFactory(id: string, mod: (module: any, exports: any, require: WebpackInstance) => void) {
|
function patchFactory(id: PropertyKey, factory: ModuleFactory) {
|
||||||
for (const factoryListener of factoryListeners) {
|
for (const factoryListener of factoryListeners) {
|
||||||
try {
|
try {
|
||||||
factoryListener(mod);
|
factoryListener(factory);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
|
logger.error("Error in Webpack factory listener:\n", err, factoryListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalMod = mod;
|
const originalFactory = factory;
|
||||||
const patchedBy = new Set();
|
const patchedBy = new Set<string>();
|
||||||
|
|
||||||
// Discords Webpack chunks for some ungodly reason contain random
|
// Discords Webpack chunks for some ungodly reason contain random
|
||||||
// newlines. Cyn recommended this workaround and it seems to work fine,
|
// newlines. Cyn recommended this workaround and it seems to work fine,
|
||||||
|
@ -162,7 +211,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
// cause issues.
|
// cause issues.
|
||||||
//
|
//
|
||||||
// 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
// 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", "");
|
let code: string = "0," + factory.toString().replaceAll("\n", "");
|
||||||
|
|
||||||
for (let i = 0; i < patches.length; i++) {
|
for (let i = 0; i < patches.length; i++) {
|
||||||
const patch = patches[i];
|
const patch = patches[i];
|
||||||
|
@ -177,14 +226,14 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
patchedBy.add(patch.plugin);
|
patchedBy.add(patch.plugin);
|
||||||
|
|
||||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
|
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace));
|
||||||
const previousMod = mod;
|
const previousFactory = factory;
|
||||||
const previousCode = code;
|
const previousCode = code;
|
||||||
|
|
||||||
// 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[]) {
|
||||||
if (replacement.predicate && !replacement.predicate()) continue;
|
if (replacement.predicate && !replacement.predicate()) continue;
|
||||||
|
|
||||||
const lastMod = mod;
|
const lastFactory = factory;
|
||||||
const lastCode = code;
|
const lastCode = code;
|
||||||
|
|
||||||
canonicalizeReplacement(replacement, patch.plugin);
|
canonicalizeReplacement(replacement, patch.plugin);
|
||||||
|
@ -193,7 +242,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -201,7 +250,7 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
|
|
||||||
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;
|
factory = previousFactory;
|
||||||
code = previousCode;
|
code = previousCode;
|
||||||
patchedBy.delete(patch.plugin);
|
patchedBy.delete(patch.plugin);
|
||||||
break;
|
break;
|
||||||
|
@ -211,9 +260,9 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
}
|
}
|
||||||
|
|
||||||
code = newCode;
|
code = newCode;
|
||||||
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
|
factory = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(id)}`);
|
||||||
} 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) {
|
||||||
const changeSize = code.length - lastCode.length;
|
const changeSize = code.length - lastCode.length;
|
||||||
|
@ -252,12 +301,12 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
|
|
||||||
if (patch.group) {
|
if (patch.group) {
|
||||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||||
mod = previousMod;
|
factory = previousFactory;
|
||||||
code = previousCode;
|
code = previousCode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod = lastMod;
|
factory = lastFactory;
|
||||||
code = lastCode;
|
code = lastCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,24 +314,30 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
if (!patch.all) patches.splice(i--, 1);
|
if (!patch.all) patches.splice(i--, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchedFactory(module: any, exports: any, require: WebpackInstance) {
|
const patchedFactory: ModuleFactory = (module, exports, require) => {
|
||||||
|
// @ts-ignore
|
||||||
|
originalFactory.$$vencordRequired = true;
|
||||||
|
for (const proxiedModules of allProxiedModules) {
|
||||||
|
proxiedModules[id] = originalFactory;
|
||||||
|
}
|
||||||
|
|
||||||
if (wreq == null && IS_DEV) {
|
if (wreq == null && IS_DEV) {
|
||||||
if (!webpackNotInitializedLogged) {
|
if (!webpackNotInitializedLogged) {
|
||||||
webpackNotInitializedLogged = true;
|
webpackNotInitializedLogged = true;
|
||||||
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
|
logger.error("WebpackRequire was not initialized, running modules without patches instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return void originalMod(module, exports, require);
|
return void originalFactory(module, exports, require);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mod(module, exports, require);
|
factory(module, exports, require);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Just rethrow Discord errors
|
// Just rethrow Discord errors
|
||||||
if (mod === originalMod) throw err;
|
if (factory === originalFactory) throw err;
|
||||||
|
|
||||||
logger.error("Error in patched module", err);
|
logger.error("Error in patched module", err);
|
||||||
return void originalMod(module, exports, require);
|
return void originalFactory(module, exports, require);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it
|
// Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it
|
||||||
|
@ -295,8 +350,8 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
Object.defineProperty(require.c, id, {
|
Object.defineProperty(require.c, id, {
|
||||||
value: require.c[id],
|
value: require.c[id],
|
||||||
configurable: true,
|
configurable: true,
|
||||||
writable: true,
|
enumerable: false,
|
||||||
enumerable: false
|
writable: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -322,11 +377,11 @@ function patchFactory(id: string, mod: (module: any, exports: any, require: Webp
|
||||||
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
|
logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
patchedFactory.toString = originalMod.toString.bind(originalMod);
|
patchedFactory.toString = originalFactory.toString.bind(originalFactory);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
patchedFactory.$$vencordOriginal = originalMod;
|
patchedFactory.$$vencordOriginal = originalFactory;
|
||||||
|
|
||||||
return patchedFactory;
|
return patchedFactory;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ import { Logger } from "@utils/Logger";
|
||||||
import { canonicalizeMatch } from "@utils/patches";
|
import { canonicalizeMatch } from "@utils/patches";
|
||||||
import { ProxyInner, proxyInner, proxyInnerValue } from "@utils/proxyInner";
|
import { ProxyInner, proxyInner, proxyInnerValue } from "@utils/proxyInner";
|
||||||
import { AnyObject } from "@utils/types";
|
import { AnyObject } from "@utils/types";
|
||||||
import type { WebpackInstance } from "discord-types/other";
|
|
||||||
|
|
||||||
import { traceFunction } from "../debug/Tracer";
|
import { traceFunction } from "../debug/Tracer";
|
||||||
import { GenericStore } from "./common";
|
import { GenericStore } from "./common";
|
||||||
|
import { ModuleExports, ModuleFactory, WebpackRequire } from "./wreq";
|
||||||
|
|
||||||
const logger = new Logger("Webpack");
|
const logger = new Logger("Webpack");
|
||||||
|
|
||||||
|
@ -24,10 +24,10 @@ 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 = (module: ModuleExports) => boolean;
|
||||||
|
|
||||||
export const filters = {
|
export const filters = {
|
||||||
byProps: (...props: string[]): FilterFn => {
|
byProps: (...props: string[]): FilterFn => {
|
||||||
|
@ -77,15 +77,15 @@ export const filters = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModCallbackFn = (mod: any) => void;
|
export type ModCallbackFn = (mod: ModuleExports) => void;
|
||||||
export type ModCallbackFnWithId = (mod: any, id: string) => void;
|
export type ModCallbackFnWithId = (mod: ModuleExports, id: PropertyKey) => void;
|
||||||
|
|
||||||
export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>();
|
export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>();
|
||||||
export const moduleListeners = new Set<ModCallbackFnWithId>();
|
export const moduleListeners = new Set<ModCallbackFnWithId>();
|
||||||
export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
|
export const factoryListeners = new Set<(factory: ModuleFactory) => void>();
|
||||||
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
|
export const beforeInitListeners = new Set<(wreq: WebpackRequire) => void>();
|
||||||
|
|
||||||
export function _initWebpack(webpackRequire: WebpackInstance) {
|
export function _initWebpack(webpackRequire: WebpackRequire) {
|
||||||
wreq = webpackRequire;
|
wreq = webpackRequire;
|
||||||
cache = webpackRequire.c;
|
cache = webpackRequire.c;
|
||||||
}
|
}
|
||||||
|
@ -390,7 +390,7 @@ export function cacheFindAll(filter: FilterFn) {
|
||||||
if (typeof filter !== "function")
|
if (typeof filter !== "function")
|
||||||
throw new Error("Invalid filter. Expected a function got " + typeof filter);
|
throw new Error("Invalid filter. Expected a function got " + typeof filter);
|
||||||
|
|
||||||
const ret = [] as any[];
|
const ret: ModuleExports[] = [];
|
||||||
for (const key in cache) {
|
for (const key in cache) {
|
||||||
const mod = cache[key];
|
const mod = cache[key];
|
||||||
if (!mod?.exports) continue;
|
if (!mod?.exports) continue;
|
||||||
|
@ -431,7 +431,7 @@ export const cacheFindBulk = traceFunction("cacheFindBulk", function cacheFindBu
|
||||||
}
|
}
|
||||||
|
|
||||||
let found = 0;
|
let found = 0;
|
||||||
const results = Array(length);
|
const results: ModuleExports[] = Array(length);
|
||||||
|
|
||||||
outer:
|
outer:
|
||||||
for (const key in cache) {
|
for (const key in cache) {
|
||||||
|
@ -726,7 +726,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac
|
||||||
* @returns Mapping of found modules
|
* @returns Mapping of found modules
|
||||||
*/
|
*/
|
||||||
export function search(...filters: Array<string | RegExp>) {
|
export function search(...filters: Array<string | RegExp>) {
|
||||||
const results = {} as Record<number, Function>;
|
const results: WebpackRequire["m"] = {};
|
||||||
const factories = wreq.m;
|
const factories = wreq.m;
|
||||||
outer:
|
outer:
|
||||||
for (const id in factories) {
|
for (const id in factories) {
|
||||||
|
@ -750,18 +750,18 @@ export function search(...filters: Array<string | RegExp>) {
|
||||||
* so putting breakpoints or similar will have no effect.
|
* so putting breakpoints or similar will have no effect.
|
||||||
* @param id The id of the module to extract
|
* @param id The id of the module to extract
|
||||||
*/
|
*/
|
||||||
export function extract(id: string | number) {
|
export function extract(id: PropertyKey) {
|
||||||
const mod = wreq.m[id] as Function;
|
const mod = wreq.m[id];
|
||||||
if (!mod) return null;
|
if (!mod) return null;
|
||||||
|
|
||||||
const code = `
|
const code = `
|
||||||
// [EXTRACTED] WebpackModule${id}
|
// [EXTRACTED] WebpackModule${String(id)}
|
||||||
// WARNING: This module was extracted to be more easily readable.
|
// WARNING: This module was extracted to be more easily readable.
|
||||||
// This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!!
|
// This module is NOT ACTUALLY USED! This means putting breakpoints will have NO EFFECT!!
|
||||||
|
|
||||||
0,${mod.toString()}
|
0,${mod.toString()}
|
||||||
//# sourceURL=ExtractedWebpackModule${id}
|
//# sourceURL=ExtractedWebpackModule${String(id)}
|
||||||
`;
|
`;
|
||||||
const extracted = (0, eval)(code);
|
const extracted: ModuleFactory = (0, eval)(code);
|
||||||
return extracted as Function;
|
return extracted;
|
||||||
}
|
}
|
||||||
|
|
141
src/webpack/wreq.d.ts
vendored
Normal file
141
src/webpack/wreq.d.ts
vendored
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated 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 = (module: Module, exports: ModuleExports, require: WebpackRequire) => void;
|
||||||
|
|
||||||
|
export type AsyncModuleBody = (
|
||||||
|
handleDependencies: (deps: Promise<any>[]) => Promise<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) => Module) & {
|
||||||
|
/** 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.defineProperty(fromObject, "a", {
|
||||||
|
* get: () => fromObject["a"]
|
||||||
|
* });
|
||||||
|
* @returns fromObject
|
||||||
|
*/
|
||||||
|
es: (this: WebpackRequire, fromObject: Record<PropertyKey, any>, toObject: Record<PropertyKey, any>) => Record<PropertyKey, any>;
|
||||||
|
/**
|
||||||
|
* Creates an async module. The body function must be a async function.
|
||||||
|
* "module.exports" will be decorated with an AsyncModulePromise.
|
||||||
|
* The body function will be called.
|
||||||
|
* To handle async dependencies correctly do this inside the body: "([a, b, c] = await handleDependencies([a, b, c]));".
|
||||||
|
* If "hasAwaitAfterDependencies" is truthy, "handleDependencies()" must be called at the end of the body function.
|
||||||
|
*/
|
||||||
|
a: (this: WebpackRequire, module: Module, body: AsyncModuleBody, hasAwaitAfterDependencies?: boolean) => void;
|
||||||
|
/** getDefaultExport function for compatibility with non-harmony modules */
|
||||||
|
n: (this: WebpackRequire, module: Module) => () => 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 property getters. For every prop in "definiton", set a getter in "exports" for the value in "definitiion", like this:
|
||||||
|
* @example
|
||||||
|
* const exports = {};
|
||||||
|
* const definition = { a: 1 };
|
||||||
|
* for (const key in definition) {
|
||||||
|
* Object.defineProperty(exports, key, { get: definition[key] }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
d: (this: WebpackRequire, exports: Record<PropertyKey, any>, definiton: Record<PropertyKey, any>) => 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 name 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: Window;
|
||||||
|
/** 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;
|
||||||
|
/** Document baseURI or WebWorker location.href */
|
||||||
|
b: string;
|
||||||
|
};
|
Loading…
Reference in a new issue