// TODO: implement storing patches as an option so we can offer unpatchAll selectively // Return this in a replacePatch to call the original method (can still modify args). export let callOriginal = Symbol('MILLENNIUM_CALL_ORIGINAL'); export interface PatchOptions { singleShot?: boolean; } // Unused at this point, exported for backwards compatibility export type GenericPatchHandler = (args: any[], ret?: any) => any; export interface Patch< Object extends Record = Record, Property extends keyof Object = keyof Object, Target extends Object[Property] = Object[Property], > { object: Object; property: Property; original: Target; patchedFunction: Target; hasUnpatched: boolean; handler: ((args: Parameters, ret: ReturnType) => any) | ((args: Parameters) => any); unpatch(): void; } // let patches = new Set(); export function beforePatch, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters) => void, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (this: any, ...args: Parameters) { handler.call(this, args); const ret = patch.original.call(this, ...args); if (options.singleShot) { patch.unpatch(); } return ret; } as any; const patch = processPatch(object, property, handler, object[property], orig); return patch; } export function afterPatch, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters, ret: ReturnType) => ReturnType, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (this: any, ...args: Parameters) { let ret = patch.original.call(this, ...args); ret = handler.call(this, args, ret); if (options.singleShot) { patch.unpatch(); } return ret; } as any; const patch = processPatch(object, property, handler, object[property], orig); return patch; } export function replacePatch, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters) => ReturnType, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (this: any, ...args: Parameters) { const ret = handler.call(this, args); // console.debug('[Patcher] replacePatch', patch); if (ret == callOriginal) return patch.original.call(this, ...args); if (options.singleShot) { patch.unpatch(); } return ret; } as any; const patch = processPatch(object, property, handler, object[property], orig); return patch; } function processPatch, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: ((args: Parameters, ret: ReturnType) => any) | ((args: Parameters) => any), patchedFunction: Target, original: Target, ): Patch { // Assign all props of original function to new one Object.assign(object[property], original); // Allow toString webpack filters to continue to work object[property].toString = () => original.toString(); // HACK: for compatibility, remove when all plugins are using new patcher Object.defineProperty(object[property], '__millenniumOrig', { get: () => patch.original, set: (val: any) => (patch.original = val), }); // Build a Patch object of this patch const patch = { object, property, handler, patchedFunction, original, hasUnpatched: false, unpatch: (): void => unpatch(patch), }; object[property].__millenniumPatch = patch; return patch; } function unpatch>(patch: Patch): void { const { object, property, handler, patchedFunction, original } = patch; if (patch.hasUnpatched) throw new Error('Function is already unpatched.'); let realProp = property; let realObject = object; console.debug('[Patcher] unpatching', { realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction, }); // If another patch has been applied to this function after this one, move down until we find the correct patch while (realObject[realProp] && realObject[realProp] !== patchedFunction) { realObject = realObject[realProp].__millenniumPatch; realProp = 'original'; console.debug('[Patcher] moved to next', { realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction, }); } realObject[realProp] = realObject[realProp].__millenniumPatch.original; patch.hasUnpatched = true; console.debug('[Patcher] unpatched', { realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction, }); }