feat(lib): add @revenge-mod/patcher
This commit is contained in:
parent
60487ec890
commit
5add60b70f
9 changed files with 714 additions and 0 deletions
121
lib/patcher/LICENSE
Normal file
121
lib/patcher/LICENSE
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
||||||
7
lib/patcher/README.md
Normal file
7
lib/patcher/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# @revenge-mod/patcher
|
||||||
|
|
||||||
|
This is a JavaScript library that allows to monkey patch functions in a way that is easy to use and understand.
|
||||||
|
|
||||||
|
It is made by [ryan-0324](https://github.com/ryan-0324) [(source uploaded on Discord)](https://discord.com/channels/1205207689832038522/1284131216156655646/1316076586981789709) and licensed under [CC0](./LICENSE).
|
||||||
|
|
||||||
|
This source code is adapted to work with the Revenge Modification Project, and we do not claim any rights to this library. You may use it following the license.
|
||||||
16
lib/patcher/package.json
Normal file
16
lib/patcher/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "@revenge-mod/patcher",
|
||||||
|
"author": {
|
||||||
|
"name": "ryan-0324",
|
||||||
|
"email": "77452312+ryan-0324@users.noreply.github.com",
|
||||||
|
"url": "https://github.com/ryan-0324"
|
||||||
|
},
|
||||||
|
"license": "CC0-1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./src/index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
lib/patcher/src/hooks/after.ts
Normal file
69
lib/patcher/src/hooks/after.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {
|
||||||
|
createPatchedFunctionProxy,
|
||||||
|
patchedFunctionProxyStates,
|
||||||
|
unproxy,
|
||||||
|
type HookNode,
|
||||||
|
type PatchedFunctionProxyState,
|
||||||
|
} from '../internal'
|
||||||
|
|
||||||
|
function unpatchAfter<T extends UnknownFunction>(
|
||||||
|
state: PatchedFunctionProxyState<PropertyKey, T>,
|
||||||
|
hookNode: HookNode<BeforeHook<T>>,
|
||||||
|
) {
|
||||||
|
if (hookNode.unpatched) return
|
||||||
|
|
||||||
|
hookNode.unpatched = true
|
||||||
|
hookNode.hook = undefined
|
||||||
|
|
||||||
|
const { prev, next } = hookNode
|
||||||
|
if (prev === undefined) {
|
||||||
|
state.after = next
|
||||||
|
if (next === undefined) {
|
||||||
|
if (state.before === undefined && state.instead === undefined) unproxy(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prev.next = next
|
||||||
|
hookNode.prev = undefined
|
||||||
|
if (next === undefined) return
|
||||||
|
}
|
||||||
|
next.prev = prev
|
||||||
|
hookNode.next = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function after<Parent extends Record<Key, UnknownFunction>, Key extends PropertyKey>(
|
||||||
|
parent: Parent,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: AfterHook<Parent[Key]>,
|
||||||
|
): () => void
|
||||||
|
export function after<Key extends PropertyKey, Value extends UnknownFunction>(
|
||||||
|
parent: Record<Key, Value>,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: AfterHook<Value>,
|
||||||
|
) {
|
||||||
|
const target = parent[key]
|
||||||
|
|
||||||
|
let state = patchedFunctionProxyStates.get(target)
|
||||||
|
let hookNode: HookNode<typeof hook>
|
||||||
|
if (state !== undefined && state.parent === parent && state.key === key) {
|
||||||
|
const head = state.after
|
||||||
|
hookNode = {
|
||||||
|
unpatched: false,
|
||||||
|
hook,
|
||||||
|
prev: undefined,
|
||||||
|
next: head,
|
||||||
|
}
|
||||||
|
if (head !== undefined) head.prev = hookNode
|
||||||
|
state.after = hookNode
|
||||||
|
} else {
|
||||||
|
hookNode = {
|
||||||
|
unpatched: false,
|
||||||
|
hook,
|
||||||
|
prev: undefined,
|
||||||
|
next: undefined,
|
||||||
|
}
|
||||||
|
state = createPatchedFunctionProxy(target, parent, key, undefined, undefined, hookNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unpatchAfter.bind(undefined, state, hookNode)
|
||||||
|
}
|
||||||
69
lib/patcher/src/hooks/before.ts
Normal file
69
lib/patcher/src/hooks/before.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {
|
||||||
|
createPatchedFunctionProxy,
|
||||||
|
patchedFunctionProxyStates,
|
||||||
|
unproxy,
|
||||||
|
type HookNode,
|
||||||
|
type PatchedFunctionProxyState,
|
||||||
|
} from '../internal'
|
||||||
|
|
||||||
|
function unpatchBefore<T extends UnknownFunction>(
|
||||||
|
state: PatchedFunctionProxyState<PropertyKey, T>,
|
||||||
|
hookNode: HookNode<BeforeHook<T>>,
|
||||||
|
) {
|
||||||
|
if (hookNode.unpatched) return
|
||||||
|
|
||||||
|
hookNode.unpatched = true
|
||||||
|
hookNode.hook = undefined
|
||||||
|
|
||||||
|
const { prev, next } = hookNode
|
||||||
|
if (prev === undefined) {
|
||||||
|
state.before = next
|
||||||
|
if (next === undefined) {
|
||||||
|
if (state.instead === undefined && state.after === undefined) unproxy(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prev.next = next
|
||||||
|
hookNode.prev = undefined
|
||||||
|
if (next === undefined) return
|
||||||
|
}
|
||||||
|
next.prev = prev
|
||||||
|
hookNode.next = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function before<Parent extends Record<Key, UnknownFunction>, Key extends PropertyKey>(
|
||||||
|
parent: Parent,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: BeforeHook<Parent[Key]>,
|
||||||
|
): () => void
|
||||||
|
export function before<Key extends PropertyKey, Value extends UnknownFunction>(
|
||||||
|
parent: Record<Key, Value>,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: BeforeHook<Value>,
|
||||||
|
) {
|
||||||
|
const target = parent[key]
|
||||||
|
|
||||||
|
let state = patchedFunctionProxyStates.get(target)
|
||||||
|
let hookNode: HookNode<typeof hook>
|
||||||
|
if (state !== undefined && state.parent === parent && state.key === key) {
|
||||||
|
const head = state.before
|
||||||
|
hookNode = {
|
||||||
|
unpatched: false,
|
||||||
|
hook,
|
||||||
|
prev: undefined,
|
||||||
|
next: head,
|
||||||
|
}
|
||||||
|
if (head !== undefined) head.prev = hookNode
|
||||||
|
state.before = hookNode
|
||||||
|
} else {
|
||||||
|
hookNode = {
|
||||||
|
unpatched: false,
|
||||||
|
hook,
|
||||||
|
prev: undefined,
|
||||||
|
next: undefined,
|
||||||
|
}
|
||||||
|
state = createPatchedFunctionProxy(target, parent, key, hookNode, undefined, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unpatchBefore.bind(undefined, state, hookNode)
|
||||||
|
}
|
||||||
85
lib/patcher/src/hooks/instead.ts
Normal file
85
lib/patcher/src/hooks/instead.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import {
|
||||||
|
createPatchedFunctionProxy,
|
||||||
|
patchedFunctionProxyHandler,
|
||||||
|
patchedFunctionProxyStates,
|
||||||
|
unproxy,
|
||||||
|
type FunctionProxyState,
|
||||||
|
type InsteadHookNode,
|
||||||
|
type PatchedFunctionProxyState,
|
||||||
|
} from '../internal'
|
||||||
|
|
||||||
|
const insteadHookProxyHandler = {
|
||||||
|
...patchedFunctionProxyHandler,
|
||||||
|
apply<T extends Callable>(hookNode: InsteadHookNode<T>, receiver: ThisParameterType<T>, args: Parameters<T>) {
|
||||||
|
const { next } = hookNode
|
||||||
|
return Reflect.apply(hookNode.hook, receiver, [args, next === undefined ? hookNode.target : next.proxy])
|
||||||
|
},
|
||||||
|
construct<T extends AbstractNewable<never, object>>(
|
||||||
|
hookNode: InsteadHookNode<T>,
|
||||||
|
args: ConstructorParameters<T>,
|
||||||
|
ctor: AbstractNewable,
|
||||||
|
) {
|
||||||
|
const { next } = hookNode
|
||||||
|
return Reflect.construct(hookNode.hook, [args, next === undefined ? hookNode.target : next.proxy], ctor)
|
||||||
|
},
|
||||||
|
} as const satisfies Required<ProxyHandler<FunctionProxyState>>
|
||||||
|
|
||||||
|
function unpatchInstead<T extends UnknownFunction>(
|
||||||
|
state: PatchedFunctionProxyState<PropertyKey, T>,
|
||||||
|
hookNode: InsteadHookNode<T>,
|
||||||
|
) {
|
||||||
|
if (hookNode.unpatched) return
|
||||||
|
|
||||||
|
hookNode.unpatched = true
|
||||||
|
|
||||||
|
const { prev, next } = hookNode
|
||||||
|
if (prev === undefined) {
|
||||||
|
state.instead = next
|
||||||
|
if (next === undefined) {
|
||||||
|
if (state.before === undefined && state.after === undefined) unproxy(state)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prev.next = next
|
||||||
|
hookNode.prev = undefined
|
||||||
|
if (next === undefined) return
|
||||||
|
}
|
||||||
|
next.prev = prev
|
||||||
|
hookNode.next = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function instead<Parent extends Record<Key, UnknownFunction>, Key extends PropertyKey>(
|
||||||
|
parent: Parent,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: InsteadHook<Parent[Key]>,
|
||||||
|
): () => void
|
||||||
|
export function instead<Key extends PropertyKey, Value extends UnknownFunction>(
|
||||||
|
parent: Record<Key, Value>,
|
||||||
|
key: FiniteDomain<Key>,
|
||||||
|
hook: InsteadHook<Value>,
|
||||||
|
) {
|
||||||
|
const target = parent[key]
|
||||||
|
|
||||||
|
let state = patchedFunctionProxyStates.get(target)
|
||||||
|
|
||||||
|
// biome-ignore lint/complexity/useArrowFunction: We need a function that has a constructor
|
||||||
|
const hookNode = function () {}
|
||||||
|
// The handler makes the proxy behave as target.
|
||||||
|
hookNode.proxy = new Proxy<any>(hookNode, insteadHookProxyHandler) as Value
|
||||||
|
hookNode.target = target
|
||||||
|
hookNode.unpatched = false
|
||||||
|
hookNode.hook = hook
|
||||||
|
hookNode.prev = undefined
|
||||||
|
|
||||||
|
if (state !== undefined && state.parent === parent && state.key === key) {
|
||||||
|
const head = state.instead
|
||||||
|
hookNode.next = head
|
||||||
|
if (head !== undefined) head.prev = hookNode
|
||||||
|
state.instead = hookNode
|
||||||
|
} else {
|
||||||
|
hookNode.next = undefined
|
||||||
|
state = createPatchedFunctionProxy(target, parent, key, undefined, hookNode, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unpatchInstead.bind(undefined, state, hookNode)
|
||||||
|
}
|
||||||
47
lib/patcher/src/index.ts
Normal file
47
lib/patcher/src/index.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { after as _after } from './hooks/after'
|
||||||
|
import { before as _before } from './hooks/before'
|
||||||
|
import { instead as _instead } from './hooks/instead'
|
||||||
|
|
||||||
|
export * from './hooks/after'
|
||||||
|
export * from './hooks/before'
|
||||||
|
export * from './hooks/instead'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the patcher.
|
||||||
|
* @see {PatcherInstance}
|
||||||
|
*/
|
||||||
|
export function createPatcherInstance(): PatcherInstance {
|
||||||
|
const up: Array<() => void> = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
after: (p, k, h) => {
|
||||||
|
const u = _after(p, k, h)
|
||||||
|
up.push(u)
|
||||||
|
return u
|
||||||
|
},
|
||||||
|
before: (p, k, h) => {
|
||||||
|
const u = _before(p, k, h)
|
||||||
|
up.push(u)
|
||||||
|
return u
|
||||||
|
},
|
||||||
|
instead: (p, k, h) => {
|
||||||
|
const u = _instead(p, k, h)
|
||||||
|
up.push(u)
|
||||||
|
return u
|
||||||
|
},
|
||||||
|
unpatchAll: () => {
|
||||||
|
for (const u of up) u()
|
||||||
|
up.length = 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An instance of the patcher that allows reverting all patches on command.
|
||||||
|
*/
|
||||||
|
export interface PatcherInstance {
|
||||||
|
after: typeof _after
|
||||||
|
before: typeof _before
|
||||||
|
instead: typeof _instead
|
||||||
|
unpatchAll: () => void
|
||||||
|
}
|
||||||
133
lib/patcher/src/internal.ts
Normal file
133
lib/patcher/src/internal.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
167
lib/patcher/src/types.d.ts
vendored
Normal file
167
lib/patcher/src/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
type FiniteDomain<Key extends PropertyKey> = unknown extends (
|
||||||
|
Key extends unknown
|
||||||
|
? {} extends Record<Key, unknown>
|
||||||
|
? unknown
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
)
|
||||||
|
? never
|
||||||
|
: Key
|
||||||
|
|
||||||
|
type UnknownFunction = Callable | AbstractNewable
|
||||||
|
|
||||||
|
type Callable<This = never, Args extends readonly unknown[] = never, Return = unknown> = (
|
||||||
|
this: This,
|
||||||
|
...args: Args
|
||||||
|
) => Return
|
||||||
|
|
||||||
|
type AbstractNewable<Args extends readonly unknown[] = never, Return = unknown> = abstract new (...args: Args) => Return
|
||||||
|
|
||||||
|
type ConcreteNewable<Args extends readonly unknown[] = never, Return = unknown> = new (...args: Args) => Return
|
||||||
|
|
||||||
|
type UnionToIntersection<Union> = (Union extends unknown ? (arg: Union) => unknown : never) extends (
|
||||||
|
arg: infer Intersection,
|
||||||
|
) => unknown
|
||||||
|
? Intersection & Union
|
||||||
|
: never
|
||||||
|
|
||||||
|
type BeforeHook<T extends UnknownFunction = UnknownFunction> = OverloadUnion<T> extends infer U
|
||||||
|
? 0 extends 1 & U
|
||||||
|
? (args: any) => any
|
||||||
|
: UnionToIntersection<
|
||||||
|
U extends Callable<never, infer Args>
|
||||||
|
? (args: Args) => Args
|
||||||
|
: U extends ConcreteNewable<infer Args>
|
||||||
|
? (args: Args) => Args
|
||||||
|
: never
|
||||||
|
>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type InsteadHook<T extends UnknownFunction = UnknownFunction> = OverloadUnion<T> extends infer U
|
||||||
|
? 0 extends 1 & U
|
||||||
|
?
|
||||||
|
| ((this: any, args: any[], originalFunction: T) => any)
|
||||||
|
| (abstract new (
|
||||||
|
args: any[],
|
||||||
|
originalFunction: T,
|
||||||
|
) => any)
|
||||||
|
: UnionToIntersection<
|
||||||
|
U extends Callable<infer This, infer Args, infer Return>
|
||||||
|
? (this: This, args: Args, originalFunction: T) => Return
|
||||||
|
: U extends ConcreteNewable<infer Args, infer Return>
|
||||||
|
? abstract new (
|
||||||
|
args: Args,
|
||||||
|
originalFunction: T,
|
||||||
|
) => Return
|
||||||
|
: never
|
||||||
|
>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type AfterHook<T extends UnknownFunction = UnknownFunction> = OverloadUnion<T> extends infer U
|
||||||
|
? 0 extends 1 & U
|
||||||
|
? (result: any) => any
|
||||||
|
: UnionToIntersection<
|
||||||
|
U extends Callable<never, never, infer Return>
|
||||||
|
? (result: Return) => Return
|
||||||
|
: U extends ConcreteNewable<never, infer Return>
|
||||||
|
? (result: Return) => Return
|
||||||
|
: never
|
||||||
|
>
|
||||||
|
: never
|
||||||
|
|
||||||
|
/** @see {@link https://github.com/microsoft/TypeScript/issues/32164#issuecomment-1146737709} */
|
||||||
|
type OverloadUnion<Overload extends UnknownFunction, PartialOverload = OmitOverloads<Overload>> = 0 extends 1 & Overload
|
||||||
|
? any
|
||||||
|
: Overload extends Callable<infer CThis, infer CArgs, infer CReturn>
|
||||||
|
? Overload extends AbstractNewable<infer NArgs, infer NReturn>
|
||||||
|
? OverloadUnionCallableNewable<
|
||||||
|
Overload,
|
||||||
|
PartialOverload,
|
||||||
|
Callable<CThis, CArgs, CReturn>,
|
||||||
|
ConcreteNewable<NArgs, NReturn>
|
||||||
|
>
|
||||||
|
: OverloadUnionCallable<Overload, PartialOverload, Callable<CThis, CArgs, CReturn>>
|
||||||
|
: Overload extends AbstractNewable<infer NArgs, infer NReturn>
|
||||||
|
? OverloadUnionNewable<Overload, PartialOverload, ConcreteNewable<NArgs, NReturn>>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type OmitOverloads<T> = { [Key in keyof T]: T[Key] }
|
||||||
|
|
||||||
|
type OverloadUnionCallableNewable<
|
||||||
|
Overload extends Callable & AbstractNewable,
|
||||||
|
PartialOverload,
|
||||||
|
CurrentCallable extends Callable,
|
||||||
|
CurrentNewable extends ConcreteNewable,
|
||||||
|
> =
|
||||||
|
| OverloadUnionCallableNewableInner<
|
||||||
|
CurrentCallable & CurrentNewable & Overload,
|
||||||
|
CurrentCallable & CurrentNewable & PartialOverload,
|
||||||
|
CurrentCallable & CurrentNewable
|
||||||
|
>
|
||||||
|
| CurrentCallable
|
||||||
|
| CurrentNewable
|
||||||
|
|
||||||
|
type OverloadUnionCallableNewableInner<
|
||||||
|
Overload extends Callable & AbstractNewable,
|
||||||
|
PartialOverload,
|
||||||
|
PreviousOverload extends Callable & ConcreteNewable,
|
||||||
|
> = PartialOverload extends Overload
|
||||||
|
? never
|
||||||
|
: Overload extends Callable<infer CThis, infer CArgs, infer CReturn>
|
||||||
|
? Overload extends AbstractNewable<infer NArgs, infer NReturn>
|
||||||
|
? // Detects infinite recursion caused by generic overloads.
|
||||||
|
Callable<CThis, CArgs, CReturn> & ConcreteNewable<NArgs, NReturn> extends PreviousOverload
|
||||||
|
? PreviousOverload extends Callable<CThis, CArgs, CReturn> & ConcreteNewable<NArgs, NReturn>
|
||||||
|
? any
|
||||||
|
: OverloadUnionCallableNewable<
|
||||||
|
Overload,
|
||||||
|
PartialOverload,
|
||||||
|
Callable<CThis, CArgs, CReturn>,
|
||||||
|
ConcreteNewable<NArgs, NReturn>
|
||||||
|
>
|
||||||
|
: OverloadUnionCallableNewable<
|
||||||
|
Overload,
|
||||||
|
PartialOverload,
|
||||||
|
Callable<CThis, CArgs, CReturn>,
|
||||||
|
ConcreteNewable<NArgs, NReturn>
|
||||||
|
>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
type OverloadUnionCallable<Overload extends Callable, PartialOverload, CurrentCallable extends Callable> =
|
||||||
|
| OverloadUnionCallableInner<CurrentCallable & Overload, CurrentCallable & PartialOverload, CurrentCallable>
|
||||||
|
| CurrentCallable
|
||||||
|
|
||||||
|
type OverloadUnionCallableInner<
|
||||||
|
Overload extends Callable,
|
||||||
|
PartialOverload,
|
||||||
|
PreviousOverload extends Callable,
|
||||||
|
> = PartialOverload extends Overload
|
||||||
|
? never
|
||||||
|
: Overload extends Callable<infer This, infer Args, infer Return>
|
||||||
|
? // Detects infinite recursion caused by generic overloads.
|
||||||
|
Callable<This, Args, Return> extends PreviousOverload
|
||||||
|
? PreviousOverload extends Callable<This, Args, Return>
|
||||||
|
? any
|
||||||
|
: OverloadUnionCallable<Overload, PartialOverload, Callable<This, Args, Return>>
|
||||||
|
: OverloadUnionCallable<Overload, PartialOverload, Callable<This, Args, Return>>
|
||||||
|
: never
|
||||||
|
|
||||||
|
type OverloadUnionNewable<Overload extends AbstractNewable, PartialOverload, CurrentNewable extends ConcreteNewable> =
|
||||||
|
| OverloadUnionNewableInner<CurrentNewable & Overload, CurrentNewable & PartialOverload, CurrentNewable>
|
||||||
|
| CurrentNewable
|
||||||
|
|
||||||
|
type OverloadUnionNewableInner<
|
||||||
|
Overload extends AbstractNewable,
|
||||||
|
PartialOverload,
|
||||||
|
PreviousOverload extends ConcreteNewable,
|
||||||
|
> = PartialOverload extends Overload
|
||||||
|
? never
|
||||||
|
: Overload extends AbstractNewable<infer Args, infer Return>
|
||||||
|
? // Detects infinite recursion caused by generic overloads.
|
||||||
|
ConcreteNewable<Args, Return> extends PreviousOverload
|
||||||
|
? PartialOverload extends ConcreteNewable<Args, Return>
|
||||||
|
? any
|
||||||
|
: OverloadUnionNewable<Overload, PartialOverload, ConcreteNewable<Args, Return>>
|
||||||
|
: OverloadUnionNewable<Overload, PartialOverload, ConcreteNewable<Args, Return>>
|
||||||
|
: never
|
||||||
Loading…
Add table
Reference in a new issue