Separate wrapFactory and patchFactory
This commit is contained in:
parent
83b09a8073
commit
afe6d9f7b3
|
@ -12,7 +12,7 @@ import { PatchReplacement } from "@utils/types";
|
|||
|
||||
import { traceFunction } from "../debug/Tracer";
|
||||
import { patches } from "../plugins";
|
||||
import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, PatchedModuleFactories, PatchedModuleFactory, subscriptions, WebpackRequire, wreq } from ".";
|
||||
import { _initWebpack, AnyModuleFactory, AnyWebpackRequire, factoryListeners, moduleListeners, subscriptions, WebpackRequire, WrappedModuleFactory, wreq } from ".";
|
||||
|
||||
const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||
|
||||
|
@ -85,7 +85,7 @@ define(Function.prototype, "O", {
|
|||
}
|
||||
|
||||
notifyFactoryListeners(this.m[id]);
|
||||
defineModulesFactoryGetter(id, Settings.eagerPatches ? patchFactory(id, this.m[id]) : this.m[id]);
|
||||
defineModulesFactoryGetter(id, Settings.eagerPatches ? wrapAndPatchFactory(id, this.m[id]) : this.m[id]);
|
||||
}
|
||||
|
||||
define(this.m, Symbol.toStringTag, {
|
||||
|
@ -104,7 +104,7 @@ define(Function.prototype, "O", {
|
|||
}
|
||||
});
|
||||
|
||||
const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
|
||||
const moduleFactoriesHandler: ProxyHandler<AnyWebpackRequire["m"]> = {
|
||||
/*
|
||||
If Discord 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
|
||||
|
@ -132,7 +132,7 @@ const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
|
|||
}
|
||||
|
||||
notifyFactoryListeners(newValue);
|
||||
defineModulesFactoryGetter(p, Settings.eagerPatches ? patchFactory(p, newValue) : newValue);
|
||||
defineModulesFactoryGetter(p, Settings.eagerPatches ? wrapAndPatchFactory(p, newValue) : newValue);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
|
|||
* @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<PatchedModuleFactory> | undefined;
|
||||
let existingFactory: TypedPropertyDescriptor<AnyModuleFactory> | undefined;
|
||||
for (const wreq of allWebpackInstances) {
|
||||
if (ignoreExistingInTarget && wreq.m === moduleFactoriesTarget) continue;
|
||||
|
||||
|
@ -194,7 +194,7 @@ function notifyFactoryListeners(factory: AnyModuleFactory) {
|
|||
* @param id The id of the module
|
||||
* @param factory The original or patched module factory
|
||||
*/
|
||||
function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) {
|
||||
function defineModulesFactoryGetter(id: PropertyKey, factory: WrappedModuleFactory) {
|
||||
// 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) {
|
||||
|
@ -205,7 +205,7 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto
|
|||
return factory;
|
||||
}
|
||||
|
||||
return (factory = patchFactory(id, factory));
|
||||
return (factory = wrapAndPatchFactory(id, factory));
|
||||
},
|
||||
set(v: AnyModuleFactory) {
|
||||
if (factory.$$vencordOriginal != null) {
|
||||
|
@ -220,128 +220,21 @@ function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFacto
|
|||
}
|
||||
|
||||
/**
|
||||
* Patches a module factory.
|
||||
* Wraps and patches a module factory.
|
||||
*
|
||||
* The factory argument will become the patched version of the 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 patchFactory(id: PropertyKey, factory: AnyModuleFactory) {
|
||||
const originalFactory = factory;
|
||||
|
||||
const patchedBy = new Set<string>();
|
||||
|
||||
// 0, prefix to turn it into an expression: 0,function(){} would be invalid syntax without the 0,
|
||||
let code: string = "0," + String(factory);
|
||||
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const patch = patches[i];
|
||||
if (patch.predicate && !patch.predicate()) continue;
|
||||
|
||||
const moduleMatches = typeof patch.find === "string"
|
||||
? code.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
||||
|
||||
if (!moduleMatches) continue;
|
||||
|
||||
patchedBy.add(patch.plugin);
|
||||
|
||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => 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[]) {
|
||||
if (replacement.predicate && !replacement.predicate()) continue;
|
||||
|
||||
const lastCode = code;
|
||||
const lastFactory = factory;
|
||||
|
||||
canonicalizeReplacement(replacement, patch.plugin);
|
||||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
if (newCode === code) {
|
||||
if (!patch.noWarn) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
code = previousCode;
|
||||
factory = previousFactory;
|
||||
patchedBy.delete(patch.plugin);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
code = newCode;
|
||||
factory = (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 ${String(id)}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
const match = lastCode.match(replacement.match)!;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
const context = lastCode.slice(start, end);
|
||||
const patchedContext = code.slice(start, endPatched);
|
||||
|
||||
// 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[];
|
||||
for (const d of diff) {
|
||||
const color = d.removed
|
||||
? "red"
|
||||
: d.added
|
||||
? "lime"
|
||||
: "grey";
|
||||
fmt += "%c%s";
|
||||
elements.push("color:" + color, d.value);
|
||||
}
|
||||
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
|
||||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
|
||||
patchedBy.delete(patch.plugin);
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
code = previousCode;
|
||||
factory = previousFactory;
|
||||
break;
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
factory = lastFactory;
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) patches.splice(i--, 1);
|
||||
}
|
||||
function wrapAndPatchFactory(id: PropertyKey, originalFactory: AnyModuleFactory) {
|
||||
const patchedFactory = patchFactory(id, originalFactory);
|
||||
|
||||
// The patched factory wrapper, define it in an object to preserve the name after minification
|
||||
const patchedFactory: PatchedModuleFactory = {
|
||||
const wrappedFactory: WrappedModuleFactory = {
|
||||
PatchedFactory(...args: Parameters<AnyModuleFactory>) {
|
||||
// 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: patchedFactory.$$vencordOriginal });
|
||||
define(wreq.m, id, { value: wrappedFactory.$$vencordOriginal });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
|
@ -367,22 +260,22 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) {
|
|||
}
|
||||
|
||||
if (IS_DEV) {
|
||||
return patchedFactory.$$vencordOriginal?.apply(this, args);
|
||||
return wrappedFactory.$$vencordOriginal?.apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
let factoryReturn: unknown;
|
||||
try {
|
||||
// Call the patched factory
|
||||
factoryReturn = factory.apply(this, args);
|
||||
factoryReturn = patchedFactory.apply(this, args);
|
||||
} catch (err) {
|
||||
// Just re-throw Discord errors
|
||||
if (factory === originalFactory) {
|
||||
if (patchedFactory === originalFactory) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.error("Error in patched module factory", err);
|
||||
return patchedFactory.$$vencordOriginal?.apply(this, args);
|
||||
return wrappedFactory.$$vencordOriginal?.apply(this, args);
|
||||
}
|
||||
|
||||
// Webpack sometimes sets the value of module.exports directly, so assign exports to it to make sure we properly handle it
|
||||
|
@ -427,8 +320,126 @@ function patchFactory(id: PropertyKey, factory: AnyModuleFactory) {
|
|||
}
|
||||
}.PatchedFactory;
|
||||
|
||||
patchedFactory.toString = originalFactory.toString.bind(originalFactory);
|
||||
patchedFactory.$$vencordOriginal = originalFactory;
|
||||
wrappedFactory.toString = originalFactory.toString.bind(originalFactory);
|
||||
wrappedFactory.$$vencordOriginal = originalFactory;
|
||||
|
||||
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];
|
||||
if (patch.predicate && !patch.predicate()) continue;
|
||||
|
||||
const moduleMatches = typeof patch.find === "string"
|
||||
? code.includes(patch.find)
|
||||
: (patch.find.global && (patch.find.lastIndex = 0), patch.find.test(code));
|
||||
|
||||
if (!moduleMatches) continue;
|
||||
|
||||
patchedBy.add(patch.plugin);
|
||||
|
||||
const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => 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[]) {
|
||||
if (replacement.predicate && !replacement.predicate()) continue;
|
||||
|
||||
const lastCode = code;
|
||||
const lastFactory = factory;
|
||||
|
||||
canonicalizeReplacement(replacement, patch.plugin);
|
||||
|
||||
try {
|
||||
const newCode = executePatch(replacement.match, replacement.replace as string);
|
||||
if (newCode === code) {
|
||||
if (!patch.noWarn) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
patchedBy.delete(patch.plugin);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
code = newCode;
|
||||
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 ${String(id)}): ${replacement.match}\n`, err);
|
||||
|
||||
if (IS_DEV) {
|
||||
const changeSize = code.length - lastCode.length;
|
||||
const match = lastCode.match(replacement.match)!;
|
||||
|
||||
// Use 200 surrounding characters of context
|
||||
const start = Math.max(0, match.index! - 200);
|
||||
const end = Math.min(lastCode.length, match.index! + match[0].length + 200);
|
||||
// (changeSize may be negative)
|
||||
const endPatched = end + changeSize;
|
||||
|
||||
const context = lastCode.slice(start, end);
|
||||
const patchedContext = code.slice(start, endPatched);
|
||||
|
||||
// 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[];
|
||||
for (const d of diff) {
|
||||
const color = d.removed
|
||||
? "red"
|
||||
: d.added
|
||||
? "lime"
|
||||
: "grey";
|
||||
fmt += "%c%s";
|
||||
elements.push("color:" + color, d.value);
|
||||
}
|
||||
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context);
|
||||
logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext);
|
||||
const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff");
|
||||
logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements);
|
||||
}
|
||||
|
||||
patchedBy.delete(patch.plugin);
|
||||
|
||||
if (patch.group) {
|
||||
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
|
||||
code = previousCode;
|
||||
patchedFactory = previousFactory;
|
||||
break;
|
||||
}
|
||||
|
||||
code = lastCode;
|
||||
patchedFactory = lastFactory;
|
||||
}
|
||||
}
|
||||
|
||||
if (!patch.all) patches.splice(i--, 1);
|
||||
}
|
||||
|
||||
return patchedFactory;
|
||||
}
|
||||
|
|
4
src/webpack/wreq.d.ts
vendored
4
src/webpack/wreq.d.ts
vendored
|
@ -193,8 +193,8 @@ export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Par
|
|||
/** exports can be anything, however initially it is always an empty object */
|
||||
export type AnyModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void;
|
||||
|
||||
export type PatchedModuleFactory = AnyModuleFactory & {
|
||||
export type WrappedModuleFactory = AnyModuleFactory & {
|
||||
$$vencordOriginal?: AnyModuleFactory;
|
||||
};
|
||||
|
||||
export type PatchedModuleFactories = Record<PropertyKey, PatchedModuleFactory>;
|
||||
export type WrappedModuleFactories = Record<PropertyKey, WrappedModuleFactory>;
|
||||
|
|
Loading…
Reference in a new issue