UNPKG

@steambrew/client

Version:
169 lines (149 loc) 5.25 kB
// 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<PropertyKey, any> = Record<PropertyKey, any>, 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<Target>, ret: ReturnType<Target>) => any) | ((args: Parameters<Target>) => any); unpatch(): void; } // let patches = new Set<Patch>(); export function beforePatch<Object extends Record<PropertyKey, any>, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters<Target>) => void, options: PatchOptions = {}, ): Patch<Object, Property, Target> { const orig = object[property]; object[property] = function (this: any, ...args: Parameters<Target>) { 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<Object extends Record<PropertyKey, any>, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters<Target>, ret: ReturnType<Target>) => ReturnType<Target>, options: PatchOptions = {}, ): Patch<Object, Property, Target> { const orig = object[property]; object[property] = function (this: any, ...args: Parameters<Target>) { 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<Object extends Record<PropertyKey, any>, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: (args: Parameters<Target>) => ReturnType<Target>, options: PatchOptions = {}, ): Patch<Object, Property, Target> { const orig = object[property]; object[property] = function (this: any, ...args: Parameters<Target>) { 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<Object extends Record<PropertyKey, any>, Property extends keyof Object, Target extends Object[Property]>( object: Object, property: Property, handler: ((args: Parameters<Target>, ret: ReturnType<Target>) => any) | ((args: Parameters<Target>) => any), patchedFunction: Target, original: Target, ): Patch<Object, Property, Target> { // 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<Object extends Record<PropertyKey, any>>(patch: Patch<Object>): 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, }); }