Merge branch 'modules-proxy-patches' into immediate-finds-modules-proxy
This commit is contained in:
commit
4459d11a97
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Vencord, a Discord client mod
|
* Vencord, a Discord client mod
|
||||||
* Copyright (c) 2024 Vendicated and contributors
|
* Copyright (c) 2024 Vendicated, Nuckyz, and contributors
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -24,94 +24,27 @@ const logger = new Logger("WebpackInterceptor", "#8caaee");
|
||||||
|
|
||||||
/** A set with all the module factories objects */
|
/** A set with all the module factories objects */
|
||||||
const allModuleFactories = new Set<PatchedModuleFactories>();
|
const allModuleFactories = new Set<PatchedModuleFactories>();
|
||||||
|
/** Whether we tried to fallback to factory WebpackRequire, or disabled patches */
|
||||||
function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) {
|
let wreqFallbackApplied = false;
|
||||||
for (const moduleFactories of allModuleFactories) {
|
|
||||||
Reflect.defineProperty(moduleFactories, id, {
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
|
|
||||||
get() {
|
|
||||||
// $$vencordOriginal means the factory is already patched
|
|
||||||
if (factory.$$vencordOriginal != null) {
|
|
||||||
return factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This patches factories if eagerPatches are disabled
|
|
||||||
return (factory = patchFactory(id, factory));
|
|
||||||
},
|
|
||||||
set(v: ModuleFactory) {
|
|
||||||
if (factory.$$vencordOriginal != null) {
|
|
||||||
factory.$$vencordOriginal = v;
|
|
||||||
} else {
|
|
||||||
factory = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
|
|
||||||
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 Reflect.set(target, p, newValue, receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingFactory = Reflect.get(target, p, target);
|
|
||||||
|
|
||||||
if (!Settings.eagerPatches) {
|
|
||||||
// If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required
|
|
||||||
// so call Reflect.set with the new original and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
|
|
||||||
if (existingFactory != null) {
|
|
||||||
return Reflect.set(target, p, newValue, receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
defineModulesFactoryGetter(p, newValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this factory is already patched
|
|
||||||
if (existingFactory?.$$vencordOriginal != null) {
|
|
||||||
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 moduleFactories of allModuleFactories) {
|
|
||||||
Reflect.defineProperty(moduleFactories, p, {
|
|
||||||
value: patchedFactory,
|
|
||||||
configurable: true,
|
|
||||||
enumerable: true,
|
|
||||||
writable: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// wreq.m is the Webpack object containing module factories.
|
// wreq.m is the Webpack object containing module factories.
|
||||||
// This is pre-populated with module factories, and is also populated via webpackGlobal.push
|
// We wrap it with our proxy, which is responsible for patching the module factories when they are set, or definining getters for the patched versions.
|
||||||
// The sentry module also has their own Webpack with a pre-populated module factories object, so this also targets that
|
// If this is the main Webpack, we also set up the internal references to WebpackRequire.
|
||||||
// We wrap it with our proxy, which is responsible for patching the module factories, or setting up getters for them
|
// wreq.m is pre-populated with module factories, and is also populated via webpackGlobal.push
|
||||||
// If this is the main Webpack, we also set up the internal references to WebpackRequire
|
// The sentry module also has their own Webpack with a pre-populated wreq.m, so this also patches the sentry module factories.
|
||||||
Reflect.defineProperty(Function.prototype, "m", {
|
Reflect.defineProperty(Function.prototype, "m", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
||||||
set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) {
|
set(this: WebpackRequire, moduleFactories: PatchedModuleFactories) {
|
||||||
// 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 ones
|
// This ensures we actually got the right ones.
|
||||||
const { stack } = new Error();
|
const { stack } = new Error();
|
||||||
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) {
|
if ((stack?.includes("discord.com") || stack?.includes("discordapp.com")) && !Array.isArray(moduleFactories)) {
|
||||||
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
|
const fileName = stack.match(/\/assets\/(.+?\.js)/)?.[1];
|
||||||
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
|
logger.info("Found Webpack module factories" + interpolateIfDefined` in ${fileName}`);
|
||||||
|
|
||||||
// setImmediate to clear this property setter if this is not the main Webpack
|
// Define a setter for the bundlePath property of WebpackRequire. Only the main Webpack has this property.
|
||||||
// If this is the main Webpack, wreq.m will always be set before the timeout runs
|
// So if the setter is called, this means we can initialize the internal references to WebpackRequire.
|
||||||
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
|
|
||||||
Reflect.defineProperty(this, "p", {
|
Reflect.defineProperty(this, "p", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|
||||||
|
@ -130,15 +63,20 @@ Reflect.defineProperty(Function.prototype, "m", {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// setImmediate to clear this property setter if this is not the main Webpack.
|
||||||
|
// If this is the main Webpack, wreq.m will always be set before the timeout runs.
|
||||||
|
const setterTimeout = setTimeout(() => Reflect.deleteProperty(this, "p"), 0);
|
||||||
|
|
||||||
// This needs to be added before the loop below
|
// This needs to be added before the loop below
|
||||||
allModuleFactories.add(moduleFactories);
|
allModuleFactories.add(moduleFactories);
|
||||||
|
|
||||||
|
// Patch the pre-populated factories
|
||||||
for (const id in moduleFactories) {
|
for (const id in moduleFactories) {
|
||||||
// If we have eagerPatches enabled we have to patch the pre-populated factories
|
|
||||||
if (Settings.eagerPatches) {
|
if (Settings.eagerPatches) {
|
||||||
|
// Patches the factory directly
|
||||||
moduleFactories[id] = patchFactory(id, moduleFactories[id]);
|
moduleFactories[id] = patchFactory(id, moduleFactories[id]);
|
||||||
} else {
|
} else {
|
||||||
|
// Define a getter for the patched version
|
||||||
defineModulesFactoryGetter(id, moduleFactories[id]);
|
defineModulesFactoryGetter(id, moduleFactories[id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +87,8 @@ Reflect.defineProperty(Function.prototype, "m", {
|
||||||
writable: true,
|
writable: true,
|
||||||
enumerable: false
|
enumerable: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The proxy responsible for patching the module factories when they are set, or definining getters for the patched versions
|
||||||
moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler);
|
moduleFactories = new Proxy(moduleFactories, moduleFactoriesHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,8 +101,95 @@ Reflect.defineProperty(Function.prototype, "m", {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let wreqFallbackApplied = false;
|
/**
|
||||||
|
* Define the getter for returning the patched version of the module factory. This only executes and patches the factory when its accessed for the first time.
|
||||||
|
*
|
||||||
|
* It is what patches factories when eagerPatches are disabled.
|
||||||
|
*
|
||||||
|
* The factory argument will become the patched version of the factory once it is accessed.
|
||||||
|
* @param id The id of the module
|
||||||
|
* @param factory The original or patched module factory
|
||||||
|
*/
|
||||||
|
function defineModulesFactoryGetter(id: PropertyKey, factory: PatchedModuleFactory) {
|
||||||
|
// Define the getter in all the module factories objects. Patches are only executed once, so make sure all module factories object
|
||||||
|
// have the the patched version
|
||||||
|
for (const moduleFactories of allModuleFactories) {
|
||||||
|
Reflect.defineProperty(moduleFactories, id, {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
|
||||||
|
get() {
|
||||||
|
// $$vencordOriginal means the factory is already patched
|
||||||
|
if (factory.$$vencordOriginal != null) {
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (factory = patchFactory(id, factory));
|
||||||
|
},
|
||||||
|
set(v: ModuleFactory) {
|
||||||
|
if (factory.$$vencordOriginal != null) {
|
||||||
|
factory.$$vencordOriginal = v;
|
||||||
|
} else {
|
||||||
|
factory = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleFactoriesHandler: ProxyHandler<PatchedModuleFactories> = {
|
||||||
|
// 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 Reflect.set(target, p, newValue, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingFactory = Reflect.get(target, p, target);
|
||||||
|
|
||||||
|
if (!Settings.eagerPatches) {
|
||||||
|
// If existingFactory exists, its either wrapped in defineModuleFactoryGetter, or it has already been required
|
||||||
|
// so call Reflect.set with the new original and let the correct logic apply (normal set, or defineModuleFactoryGetter setter)
|
||||||
|
if (existingFactory != null) {
|
||||||
|
return Reflect.set(target, p, newValue, receiver);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eagerPatches are disabled, so set up the getter for the patched version
|
||||||
|
defineModulesFactoryGetter(p, newValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this factory is already patched
|
||||||
|
if (existingFactory?.$$vencordOriginal != null) {
|
||||||
|
existingFactory.$$vencordOriginal = newValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchedFactory = patchFactory(p, newValue);
|
||||||
|
|
||||||
|
// If multiple Webpack instances exist, when new a new module is loaded, it will be set in all the module factories objects.
|
||||||
|
// Because patches are only executed once, we need to set the patched version in all of them, to avoid the Webpack instance
|
||||||
|
// that uses the factory to contain the original factory instead of the patched, in case it was set first in another instance
|
||||||
|
for (const moduleFactories of allModuleFactories) {
|
||||||
|
Reflect.defineProperty(moduleFactories, p, {
|
||||||
|
value: patchedFactory,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 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: ModuleFactory) {
|
function patchFactory(id: PropertyKey, factory: ModuleFactory) {
|
||||||
const originalFactory = factory;
|
const originalFactory = factory;
|
||||||
|
|
||||||
|
@ -288,7 +315,10 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) {
|
||||||
if (!patch.all) patches.splice(i--, 1);
|
if (!patch.all) patches.splice(i--, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The patched factory wrapper
|
||||||
const patchedFactory: PatchedModuleFactory = function (...args: Parameters<ModuleFactory>) {
|
const patchedFactory: PatchedModuleFactory = function (...args: Parameters<ModuleFactory>) {
|
||||||
|
// Restore the original factory in all the module factories objects,
|
||||||
|
// because we want to make sure the original factory is restored properly, no matter what is the Webpack instance
|
||||||
for (const moduleFactories of allModuleFactories) {
|
for (const moduleFactories of allModuleFactories) {
|
||||||
Reflect.defineProperty(moduleFactories, id, {
|
Reflect.defineProperty(moduleFactories, id, {
|
||||||
value: patchedFactory.$$vencordOriginal,
|
value: patchedFactory.$$vencordOriginal,
|
||||||
|
@ -327,6 +357,7 @@ function patchFactory(id: PropertyKey, factory: ModuleFactory) {
|
||||||
|
|
||||||
let factoryReturn: unknown;
|
let factoryReturn: unknown;
|
||||||
try {
|
try {
|
||||||
|
// Call the patched factory
|
||||||
factoryReturn = factory.apply(this, args);
|
factoryReturn = factory.apply(this, args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Just re-throw Discord errors
|
// Just re-throw Discord errors
|
||||||
|
|
34
src/webpack/wreq.d.ts
vendored
34
src/webpack/wreq.d.ts
vendored
|
@ -15,23 +15,33 @@ export type Module = {
|
||||||
/** exports can be anything, however initially it is always an empty object */
|
/** 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 ModuleFactory = (this: ModuleExports, module: Module, exports: ModuleExports, require: WebpackRequire) => void;
|
||||||
|
|
||||||
|
export type WebpackQueues = unique symbol;
|
||||||
|
export type WebpackExports = unique symbol;
|
||||||
|
export type WebpackError = unique symbol;
|
||||||
|
|
||||||
|
export type AsyncModulePromise = Promise<ModuleExports> & {
|
||||||
|
[WebpackQueues]: (fnQueue: ((queue: any[]) => any)) => any;
|
||||||
|
[WebpackExports]: ModuleExports;
|
||||||
|
[WebpackError]?: any;
|
||||||
|
};
|
||||||
|
|
||||||
export type AsyncModuleBody = (
|
export type AsyncModuleBody = (
|
||||||
handleAsyncDependencies: (deps: Promise<any>[]) =>
|
handleAsyncDependencies: (deps: AsyncModulePromise[]) =>
|
||||||
Promise<() => any[]> | (() => any[]),
|
Promise<() => ModuleExports[]> | (() => ModuleExports[]),
|
||||||
asyncResult: (error?: any) => void
|
asyncResult: (error?: any) => void
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
export type ChunkHandlers = {
|
export type ChunkHandlers = {
|
||||||
/**
|
/**
|
||||||
* Ensures the js file for this chunk is loaded, or starts to load if it's not
|
* Ensures the js file for this chunk is loaded, or starts to load if it's not.
|
||||||
* @param chunkId The chunk id
|
* @param chunkId The chunk id
|
||||||
* @param promises The promises array to add the loading promise to.
|
* @param promises The promises array to add the loading promise to
|
||||||
*/
|
*/
|
||||||
j: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
|
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
|
* Ensures the css file for this chunk is loaded, or starts to load if it's not.
|
||||||
* @param chunkId The chunk id
|
* @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.
|
* @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,
|
css: (this: ChunkHandlers, chunkId: PropertyKey, promises: Promise<void[]>) => void,
|
||||||
};
|
};
|
||||||
|
@ -53,7 +63,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & {
|
||||||
* @example
|
* @example
|
||||||
* const fromObject = { a: 1 };
|
* const fromObject = { a: 1 };
|
||||||
* Object.keys(fromObject).forEach(key => {
|
* Object.keys(fromObject).forEach(key => {
|
||||||
* if (key !== "default" && !(key in toObject)) {
|
* if (key !== "default" && !Object.hasOwn(toObject, key)) {
|
||||||
* Object.defineProperty(toObject, key, {
|
* Object.defineProperty(toObject, key, {
|
||||||
* get: () => fromObject[key],
|
* get: () => fromObject[key],
|
||||||
* enumerable: true
|
* enumerable: true
|
||||||
|
@ -65,8 +75,8 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & {
|
||||||
es: (this: WebpackRequire, fromObject: Record<PropertyKey, any>, toObject: Record<PropertyKey, any>) => Record<PropertyKey, any>;
|
es: (this: WebpackRequire, fromObject: Record<PropertyKey, any>, toObject: Record<PropertyKey, any>) => Record<PropertyKey, any>;
|
||||||
/**
|
/**
|
||||||
* Creates an async module. A module that exports something that is a Promise, or requires an export from an async module.
|
* Creates an async module. A module that exports something that is a Promise, or requires an export from an async module.
|
||||||
* The body function must be an async function. "module.exports" will become a Promise.
|
* The body function must be an async function. "module.exports" will become a 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 to handle async depedencies:
|
* 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
|
* @example
|
||||||
* const factory = (module, exports, wreq) => {
|
* const factory = (module, exports, wreq) => {
|
||||||
* wreq.a(module, async (handleAsyncDependencies, asyncResult) => {
|
* wreq.a(module, async (handleAsyncDependencies, asyncResult) => {
|
||||||
|
@ -111,7 +121,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & {
|
||||||
* const exports = {};
|
* const exports = {};
|
||||||
* const definition = { exportName: () => someExportedValue };
|
* const definition = { exportName: () => someExportedValue };
|
||||||
* for (const key in definition) {
|
* for (const key in definition) {
|
||||||
* if (key in definition && !(key in exports)) {
|
* if (Object.hasOwn(definition, key) && !Object.hasOwn(exports, key)) {
|
||||||
* Object.defineProperty(exports, key, {
|
* Object.defineProperty(exports, key, {
|
||||||
* get: definition[key],
|
* get: definition[key],
|
||||||
* enumerable: true
|
* enumerable: true
|
||||||
|
@ -133,7 +143,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & {
|
||||||
/** Get the filename for the js part of a chunk */
|
/** Get the filename for the js part of a chunk */
|
||||||
u: (this: WebpackRequire, chunkId: PropertyKey) => string;
|
u: (this: WebpackRequire, chunkId: PropertyKey) => string;
|
||||||
/** The global object, will likely always be the window */
|
/** The global object, will likely always be the window */
|
||||||
g: Window;
|
g: typeof globalThis;
|
||||||
/** Harmony module decorator. Decorates a module as an ES Module, and prevents Node.js "module.exports" from being set */
|
/** 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;
|
hmd: (this: WebpackRequire, module: Module) => any;
|
||||||
/** Shorthand for Object.prototype.hasOwnProperty */
|
/** Shorthand for Object.prototype.hasOwnProperty */
|
||||||
|
@ -161,7 +171,7 @@ export type WebpackRequire = ((moduleId: PropertyKey) => Module) & {
|
||||||
*/
|
*/
|
||||||
O: OnChunksLoaded;
|
O: OnChunksLoaded;
|
||||||
/**
|
/**
|
||||||
* Instantiate a wasm instance with source using "wasmModuleHash", and importObject "importsObj", and then assign the exports of its instance to "exports"
|
* 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
|
* @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>;
|
v: (this: WebpackRequire, exports: ModuleExports, wasmModuleId: any, wasmModuleHash: string, importsObj?: WebAssembly.Imports) => Promise<any>;
|
||||||
|
|
Loading…
Reference in a new issue