Created
September 18, 2025 22:44
-
-
Save notmike101/a79216e5d66dce72e200e2f7bbde5da9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @typedef {(...args: Parameters<T[M]>) => ReturnType<T[M]>} PatchFunction | |
| * @template {object} T | |
| * @template {keyof T} M | |
| */ | |
| /** | |
| * @typedef {(PatchFunction<T, M>) & { | |
| * readonly original: T[M]; | |
| * readonly isPatched: true; | |
| * readonly removePatch: () => void; | |
| * }} PatchReturn | |
| * @template {object} T | |
| * @template {keyof T} M | |
| */ | |
| /** | |
| * Monkey-patch a method on an object or class (prototype or instance). | |
| * | |
| * The returned function is callable like th eoriginal and also exposes: | |
| * - `original` - Reference to the unpatched function | |
| * - `isPatched` - `true` | |
| * - `removePatch()` - Restores the original method (descriptor-safe) | |
| * | |
| * @throws {ReferenceError} If `target`, `method`, or `patchFunction` is not provided. | |
| * @throws {TypeError} If `target` is not an object/function, `method` is not a string, `targetClass.targetMethod` does not exist, or `patchFunction` is not a function. | |
| * @throws {Error} If the target method is already patched or is non-writable/non-configurable. | |
| * | |
| * @example // Patch a method in common object | |
| * Math.abs(-1); // 1 | |
| * monkeyPatch(Math, 'abs', (x) => x); | |
| * Math.abs(-1); // -1 | |
| * Math.abs.original(-1); // 1 | |
| * Math.abs.removePatch(); | |
| * Math.abs(-1); // 1 | |
| * | |
| * @example // Patch a prototype | |
| * class Counter { inc(x) { return x + 1; }} | |
| * const counter = new Counter(); | |
| * counter.inc(1); // 2 | |
| * monkeyPatch(Counter.prototype, 'inc', (x = 1) => x + 2); | |
| * counter.inc(1); // 3 | |
| * counter.inc.original(1); // 2 | |
| * counter.inc.removePatch() | |
| * counter.inc(1); // 2 | |
| * | |
| * @template T | |
| * @template {keyof T} M | |
| * | |
| * @param {T} targetClass - Object or prototype that contains the method to patch | |
| * @param {M} targetMethod - Name of the method to patch. Must exist on `targetClass` (own or inherited). | |
| * @param {PatchFunction<T, M>} patchFunction - The patch implementation | |
| * | |
| * @returns {PatchReturn<T, M>} The patched function. Invocable like the original, plus `original`, `isPatched`, and `removePatch`. | |
| */ | |
| export const monkeyPatch = (targetClass, targetMethod, patchFunction) => { | |
| if (!targetClass) { | |
| throw new ReferenceError('Target class must be provided'); | |
| } else if (typeof targetClass !== 'object') { | |
| throw new TypeError(`Target class must be of type "object", got type ${typeof targetClass}`); | |
| } | |
| if (!targetMethod) { | |
| throw new ReferenceError('Target method name must be provided'); | |
| } else if (typeof targetMethod !== 'string') { | |
| throw new TypeError(`Target method name must be of type "string", got type ${typeof targetMethod}`); | |
| } else if (!(targetMethod in targetClass)) { | |
| throw new ReferenceError(`Target method ${targetMethod} does not exist in target class`); | |
| } | |
| if (!patchFunction) { | |
| throw new ReferenceError('Patch function must be provided'); | |
| } else if (typeof patchFunction !== 'function') { | |
| throw new TypeError(`Patch function must be of type "function", got type ${typeof patchFunction}`); | |
| } | |
| if (targetClass[targetMethod].isPatched === true) { | |
| throw new Error(`Function ${targetMethod} already patched. Call ${targetMethod}.revoke first`); | |
| } | |
| const ownDescription = Object.getOwnPropertyDescriptor(targetClass, targetMethod); | |
| if (ownDescription?.writable !== true && ownDescription.configurable !== true) { | |
| throw new Error(`Method ${targetMethod} is non-writable and non-configurable`); | |
| } | |
| const originalFunction = targetClass[targetMethod]; | |
| /** | |
| * Wrapper for the patch function | |
| * | |
| * @param {Parameters<T[M]>} args - Arguments passed to the targeted method | |
| * @returns {ReturnType<typeof patchFunction>} Return type of patch function | |
| */ | |
| const wrapper = function (...args) { | |
| return patchFunction.apply(this, args); | |
| }; | |
| Object.defineProperties(wrapper, { | |
| original: { | |
| /** @type {T[M]} */ | |
| value: originalFunction, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false, | |
| }, | |
| isPatched: { | |
| /** @type {boolean} */ | |
| value: true, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false, | |
| }, | |
| removePatch: { | |
| /** | |
| * Remove the patch | |
| */ | |
| value: () => { | |
| Object.defineProperty(targetClass, targetMethod, { | |
| ...ownDescription, | |
| value: originalFunction, | |
| }); | |
| }, | |
| configurable: false, | |
| enumerable: false, | |
| writable: false, | |
| }, | |
| }); | |
| Object.defineProperty(targetClass, targetMethod, { | |
| ...ownDescription, | |
| value: wrapper, | |
| }); | |
| return wrapper; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment