From 3b6627531ed754b23107fd38cb4129fcdfaa4549 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Sat, 12 Apr 2025 14:18:13 +0700 Subject: [PATCH] feat(lib/utils): add `objects` and `functions.objectDefineProperty` --- lib/utils/package.json | 9 ++++-- lib/utils/src/functions.ts | 1 + lib/utils/src/objects.ts | 61 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 lib/utils/src/objects.ts diff --git a/lib/utils/package.json b/lib/utils/package.json index 562c9f0..ca26035 100644 --- a/lib/utils/package.json +++ b/lib/utils/package.json @@ -15,11 +15,14 @@ "./logger": { "default": "./src/logger.ts" }, - "./types": { - "types": "./src/types.d.ts" - }, "./proxy": { "default": "./src/proxy.ts" + }, + "./objects": { + "default": "./src/objects.ts" + }, + "./types": { + "types": "./src/types.d.ts" } } } diff --git a/lib/utils/src/functions.ts b/lib/utils/src/functions.ts index 0512767..54a18a9 100644 --- a/lib/utils/src/functions.ts +++ b/lib/utils/src/functions.ts @@ -3,3 +3,4 @@ export const objectFreeze = Object.freeze export const objectSeal = Object.seal export const objectPreventExtensions = Object.preventExtensions +export const objectDefineProperty = Object.defineProperty diff --git a/lib/utils/src/objects.ts b/lib/utils/src/objects.ts new file mode 100644 index 0000000..55d497f --- /dev/null +++ b/lib/utils/src/objects.ts @@ -0,0 +1,61 @@ +import { objectDefineProperty as defineProperty } from './functions' + +// Heavily inspired by uwu/shelter +// https://github.com/uwu/shelter/blob/main/packages/shelter/src/exfiltrate.ts + +// Patch to fix intercepting properties on Object.prototype +Object.defineProperty = (target, p, d) => { + if (target === Object.prototype && _intercepting.has(p)) { + _intercepting.set(p, d) + return target + } + + return defineProperty(target, p, d) +} + +const _intercepting = new Map() + +export type InterceptPropertyCallback = (target: any, value: any) => any + +/** + * Intercept a property when set on an object to modify its value. **Only one callback can be set for a property at a time.** + * + * @param prop The property to intercept. + * @param callback The callback to call when the property is set, can return a non-null value to set the property to. + * @returns The function to unintercept the property. + */ +export function interceptProperty(prop: PropertyKey, callback: (target: object, value: any) => any) { + if (_intercepting.has(prop)) throw new Error(`Property ${String(prop)} is already being intercepted`) + + const proto = Object.prototype + _intercepting.set(prop, Object.getOwnPropertyDescriptor(proto, prop)) + + defineProperty(proto, prop, { + configurable: true, + set(value) { + if (this === proto) { + _intercepting.set(prop, { configurable: true, value }) + return + } + + defineProperty(this, prop, { + configurable: true, + writable: true, + enumerable: true, + value: callback(this, value) ?? value, + }) + }, + get() { + const desc = _intercepting.get(prop) + return desc && (desc.value ?? desc.get?.call(this)) + }, + }) + + return () => { + // @ts-expect-error + delete proto[prop] + const desc = _intercepting.get(prop) + if (desc) defineProperty(proto, prop, desc) + _intercepting.delete(prop) + } +}