revenge-bundle-next/lib/patcher/src/_internal.ts

133 lines
5.2 KiB
TypeScript

export interface FunctionProxyState<T extends UnknownFunction = UnknownFunction> {
readonly proxy: T
readonly target: T
}
export interface PatchedFunctionProxyState<
Key extends PropertyKey = PropertyKey,
Value extends UnknownFunction = UnknownFunction,
> extends FunctionProxyState<Value> {
/** Must have a function valued property whose key is {@link key}. */
readonly parent: Record<Key, Value>
readonly key: Key
before: HookNode<BeforeHook<Value>> | undefined
instead: InsteadHookNode<Value> | undefined
after: HookNode<AfterHook<Value>> | undefined
}
export interface HookNode<Hook extends UnknownFunction = UnknownFunction> {
/** Checked so that hook node lists are not cleared if an unpatch function is called more than once. */
unpatched: boolean
/** Can be undefined only in instances that are not in a hook node list. */
hook: Hook | undefined
prev: HookNode<Hook> | undefined
next: HookNode<Hook> | undefined
}
export interface InsteadHookNode<T extends UnknownFunction = UnknownFunction>
extends HookNode<InsteadHook<T>>,
FunctionProxyState<T> {
readonly hook: InsteadHook<T>
prev: InsteadHookNode<T> | undefined
next: InsteadHookNode<T> | undefined
}
/** @throws {TypeError} When `hookNode` is defined, its hook and that of all other hook nodes in its list must also be defined. */
function applyHooks<T>(hookNode: HookNode<(arg: T) => T> | undefined, arg: T) {
while (hookNode !== undefined) {
// Save a reference to the next hook node in case the current hook calls unpatch.
const { next, hook } = hookNode
// Indirectly call the hook so as to not give it access to its hook node.
arg = hook!(arg)
hookNode = next
}
return arg
}
export const patchedFunctionProxyHandler = {
apply<T extends Callable>(
state: PatchedFunctionProxyState<PropertyKey, T>,
receiver: ThisParameterType<T>,
args: Parameters<T>,
) {
args = applyHooks(state.before, args)
const { instead } = state
let result: ReturnType<T> = Reflect.apply(instead === undefined ? state.target : instead.proxy, receiver, args)
result = applyHooks(state.after, result)
return result
},
construct<T extends AbstractNewable<never, object>>(
state: PatchedFunctionProxyState<PropertyKey, T>,
args: ConstructorParameters<T>,
ctor: AbstractNewable,
) {
args = applyHooks(state.before, args)
const { instead } = state
let result: InstanceType<T> = Reflect.construct(
instead === undefined ? state.target : instead.proxy,
args,
ctor,
)
result = applyHooks(state.after, result)
return result
},
defineProperty: (state, key, descriptor) => Reflect.defineProperty(state.target, key, descriptor),
deleteProperty: (state, key) => Reflect.deleteProperty(state.target, key),
get: (state, key, receiver: unknown) => Reflect.get(state.target, key, receiver),
getOwnPropertyDescriptor: (state, key) => Reflect.getOwnPropertyDescriptor(state.target, key),
getPrototypeOf: state => Reflect.getPrototypeOf(state.target),
has: (state, key) => Reflect.has(state.target, key),
isExtensible: state => Reflect.isExtensible(state.target),
ownKeys: state => Reflect.ownKeys(state.target),
preventExtensions: state => Reflect.preventExtensions(state.target),
set: (state, key, value: unknown, receiver: unknown) => Reflect.set(state.target, key, value, receiver),
setPrototypeOf: (state, prototype) => Reflect.setPrototypeOf(state.target, prototype),
} as const satisfies Required<ProxyHandler<FunctionProxyState>>
interface PatchedFunctionProxyStateMap extends WeakMap<UnknownFunction, any> {
readonly delete: (key: UnknownFunction) => boolean
readonly get: <K extends UnknownFunction>(key: K) => PatchedFunctionProxyState<PropertyKey, K> | undefined
readonly has: (key: UnknownFunction) => boolean
readonly set: <K extends UnknownFunction>(key: K, value: PatchedFunctionProxyState<PropertyKey, K>) => this
}
/** proxy -> state */
export const patchedFunctionProxyStates: PatchedFunctionProxyStateMap = new WeakMap<UnknownFunction>()
export function createPatchedFunctionProxy<Key extends PropertyKey, Value extends UnknownFunction>(
target: Value,
parent: Record<Key, Value>,
key: FiniteDomain<Key>,
before: HookNode<BeforeHook<Value>> | undefined,
instead: InsteadHookNode<Value> | undefined,
after: HookNode<AfterHook<Value>> | undefined,
): PatchedFunctionProxyState<Key, Value> {
// biome-ignore lint/complexity/useArrowFunction: We need a function that has a constructor
const state = function () {}
// The handler makes the proxy behave as state.target.
const proxy: Value = new Proxy(state, patchedFunctionProxyHandler) as any
state.proxy = proxy
state.target = target
state.parent = parent
state.key = key
state.before = before
state.instead = instead
state.after = after
patchedFunctionProxyStates.set(proxy, state)
parent[key] = proxy
return state
}
export function unproxy(state: PatchedFunctionProxyState) {
const { parent, key } = state
if (parent[key] === state.proxy) parent[key] = state.target
}