// 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('DECKY_CALL_ORIGINAL'); export interface PatchOptions { singleShot?: boolean; } type GenericPatchHandler = (args: any[], ret?: any) => any; export interface Patch { original: Function; property: string; object: any; patchedFunction: any; hasUnpatched: boolean; handler: GenericPatchHandler; unpatch: () => void; } // let patches = new Set(); export function beforePatch( object: any, property: string, handler: (args: any[]) => any, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (...args: any[]) { handler.call(this, args); const ret = patch.original.call(this, ...args); if (options.singleShot) { patch.unpatch(); } return ret; }; const patch = processPatch(object, property, handler, object[property], orig); return patch; } export function afterPatch( object: any, property: string, handler: (args: any[], ret: any) => any, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (...args: any[]) { let ret = patch.original.call(this, ...args); ret = handler.call(this, args, ret); if (options.singleShot) { patch.unpatch(); } return ret; }; const patch = processPatch(object, property, handler, object[property], orig); return patch; } export function replacePatch( object: any, property: string, handler: (args: any[]) => any, options: PatchOptions = {}, ): Patch { const orig = object[property]; object[property] = function (...args: any[]) { const ret = handler.call(this, args); if (ret == callOriginal) return patch.original.call(this, ...args); if (options.singleShot) { patch.unpatch(); } return ret; }; const patch = processPatch(object, property, handler, object[property], orig); return patch; } function processPatch( object: any, property: any, handler: GenericPatchHandler, patchedFunction: any, original: any, ): 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], '__deckyOrig', { get: () => patch.original, set: (val: any) => (patch.original = val), }); // Build a Patch object of this patch const patch: Patch = { object, property, handler, patchedFunction, original, hasUnpatched: false, unpatch: () => unpatch(patch), }; object[property].__deckyPatch = 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].__deckyPatch; realProp = 'original'; console.debug('[Patcher] moved to next', { realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction, }); } realObject[realProp] = realObject[realProp].__deckyPatch.original; patch.hasUnpatched = true; console.debug('[Patcher] unpatched', { realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction, }); }