Option for eager patching
This commit is contained in:
parent
01a4ac9c13
commit
3ab68f9296
|
@ -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
|
||||||
|
|
|
@ -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,8 @@ 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", "");
|
// @ts-ignore
|
||||||
|
let src = String(mod.$$vencordOriginal ?? mod).replaceAll("\n", "");
|
||||||
|
|
||||||
if (src.startsWith("function(")) {
|
if (src.startsWith("function(")) {
|
||||||
src = "0," + src;
|
src = "0," + src;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* 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";
|
||||||
|
@ -16,23 +17,53 @@ import { _initWebpack, beforeInitListeners, factoryListeners, ModuleFactory, mod
|
||||||
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||||
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
|
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
|
||||||
|
|
||||||
|
const allProxiedModules = new Set<WebpackRequire["m"]>();
|
||||||
|
|
||||||
const modulesProxyhandler: ProxyHandler<WebpackRequire["m"]> = {
|
const modulesProxyhandler: ProxyHandler<WebpackRequire["m"]> = {
|
||||||
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
|
...Object.fromEntries(Object.getOwnPropertyNames(Reflect).map(propName =>
|
||||||
[propName, (...args: any[]) => Reflect[propName](...args)]
|
[propName, (...args: any[]) => Reflect[propName](...args)]
|
||||||
)),
|
)),
|
||||||
get: (target, p) => {
|
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
|
||||||
|
// $$vencordOriginal means the factory is already patched, $$vencordRequired means it has already been required
|
||||||
|
// and replaced with the original
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (mod == null || mod.$$vencordOriginal != null || Number.isNaN(Number(p))) return mod;
|
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) {
|
||||||
|
@ -63,7 +94,9 @@ Object.defineProperty(Function.prototype, "O", {
|
||||||
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;
|
const wreq = this;
|
||||||
|
@ -104,15 +137,17 @@ Object.defineProperty(Function.prototype, "O", {
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
|
@ -124,35 +159,47 @@ 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: PropertyKey, mod: ModuleFactory) {
|
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<string>();
|
const patchedBy = new Set<string>();
|
||||||
|
|
||||||
// Discords Webpack chunks for some ungodly reason contain random
|
// Discords Webpack chunks for some ungodly reason contain random
|
||||||
|
@ -164,7 +211,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
// 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];
|
||||||
|
@ -179,14 +226,14 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
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);
|
||||||
|
@ -203,7 +250,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
|
|
||||||
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;
|
||||||
|
@ -213,7 +260,7 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
code = newCode;
|
code = newCode;
|
||||||
mod = (0, eval)(`// Webpack Module ${String(id)} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${String(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 ${String(id)}): ${replacement.match}\n`, err);
|
logger.error(`Patch by ${patch.plugin} errored (Module id is ${String(id)}): ${replacement.match}\n`, err);
|
||||||
|
|
||||||
|
@ -254,12 +301,12 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,23 +315,29 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchedFactory: ModuleFactory = (module, exports, require) => {
|
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
|
||||||
|
@ -297,8 +350,8 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -326,9 +379,9 @@ function patchFactory(id: PropertyKey, mod: ModuleFactory) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue