chore(lib/patcher): add tests and docs
This commit is contained in:
parent
5c54e3d8e5
commit
fc8067d599
5 changed files with 768 additions and 3 deletions
2
bun.lock
2
bun.lock
|
|
@ -8,7 +8,7 @@
|
||||||
"@parcel/watcher": "^2.5.1",
|
"@parcel/watcher": "^2.5.1",
|
||||||
"@react-native-clipboard/clipboard": "^1.16.2",
|
"@react-native-clipboard/clipboard": "^1.16.2",
|
||||||
"@swc/core": "^1.11.31",
|
"@swc/core": "^1.11.31",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.2.15",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
"@unbound-mod/hermesc": "^1.0.3",
|
"@unbound-mod/hermesc": "^1.0.3",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,268 @@ This is a JavaScript library that allows to monkey patch functions in a way that
|
||||||
|
|
||||||
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).
|
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.
|
This source code is polished and adapted to work with Revenge. We do not claim any rights to this library. You may use it granted you follow the license.
|
||||||
|
|
||||||
|
## How does this work?
|
||||||
|
|
||||||
|
Think of hooks as separate functions that are called `before`, `after`, or `instead` of the original function.
|
||||||
|
You can use them to modify the behavior of the original function, or to add new functionality.
|
||||||
|
|
||||||
|
### Before hooks
|
||||||
|
|
||||||
|
Before hooks allow you to modify the arguments passed to the original function, or to perform some action before the original function is called.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { before } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => console.log('Original method called with:', a)
|
||||||
|
}
|
||||||
|
|
||||||
|
before(obj, 'method', ([a]) => {
|
||||||
|
console.log('Before method called with:', a)
|
||||||
|
// Modify arguments by returning new array
|
||||||
|
return [a + 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
obj.method(2)
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Before method called with: 2
|
||||||
|
// Original method called with: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### After hooks
|
||||||
|
|
||||||
|
After hooks allow you to modify the return value of the original function, or to perform some action after the original function is called.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { after } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => a * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
after(obj, 'method', (result) => {
|
||||||
|
console.log('After method called with result:', result)
|
||||||
|
// Modify return value
|
||||||
|
return result + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(obj.method(2)) // 5
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// After method called with result: 4
|
||||||
|
// 5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instead hooks
|
||||||
|
|
||||||
|
Instead hooks allow you to completely replace the original function with a new one, while still being able to call the original function if needed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { instead } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => {
|
||||||
|
console.log('Original method called with:', a)
|
||||||
|
return 'original result'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instead(obj, 'method', ([a], original) => {
|
||||||
|
console.log('Instead method called with:', a)
|
||||||
|
// Call the original function if needed
|
||||||
|
const originalResult = original('modified')
|
||||||
|
console.log('Original method was called')
|
||||||
|
|
||||||
|
// Return a new value
|
||||||
|
return 'new value'
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(obj.method('test')) // 'new value'
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Instead method called with: test
|
||||||
|
// Original method called with: modified
|
||||||
|
// Original method was called
|
||||||
|
// new value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining hooks
|
||||||
|
|
||||||
|
You can combine multiple hooks on the same function.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { before, after, instead } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => a * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
before(obj, 'method', ([a]) => {
|
||||||
|
console.log('Before method called with:', a)
|
||||||
|
return [a + 1] // 2 becomes 3
|
||||||
|
})
|
||||||
|
|
||||||
|
instead(obj, 'method', ([a], original) => {
|
||||||
|
console.log('Instead method called with:', a)
|
||||||
|
const result = original(a) // Call original with modified arg (3)
|
||||||
|
console.log('Original method returned:', result)
|
||||||
|
return result + 1 // 6 becomes 7
|
||||||
|
})
|
||||||
|
|
||||||
|
after(obj, 'method', (result) => {
|
||||||
|
console.log('After method called with result:', result)
|
||||||
|
return result + 1 // 7 becomes 8
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(obj.method(2)) // 8
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Before method called with: 2
|
||||||
|
// Instead method called with: 3
|
||||||
|
// Original method returned: 6
|
||||||
|
// After method called with result: 7
|
||||||
|
// 8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stacking hooks
|
||||||
|
|
||||||
|
Stacking multiple hooks of same type will execute them in a specific order:
|
||||||
|
|
||||||
|
1. **Before hooks** (execute in reverse order - last added runs first)
|
||||||
|
2. **Instead hooks** (only the last added executes, unless calling original)
|
||||||
|
3. **After hooks** (execute in order - first added runs first)
|
||||||
|
|
||||||
|
Using multiple `before` hooks will execute them in **reverse order** (LIFO - Last In, First Out):
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { before } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => console.log('Original method called with:', a)
|
||||||
|
}
|
||||||
|
|
||||||
|
before(obj, 'method', ([a]) => {
|
||||||
|
console.log('First before hook called with:', a)
|
||||||
|
return [a + 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
before(obj, 'method', ([a]) => {
|
||||||
|
console.log('Second before hook called with:', a)
|
||||||
|
return [a + 2]
|
||||||
|
})
|
||||||
|
|
||||||
|
obj.method(2)
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Second before hook called with: 2
|
||||||
|
// First before hook called with: 4
|
||||||
|
// Original method called with: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple `after` hooks execute in **normal order** (FIFO - First In, First Out):
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { after } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => a * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
after(obj, 'method', (result) => {
|
||||||
|
console.log('First after hook called with result:', result)
|
||||||
|
return result + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
after(obj, 'method', (result) => {
|
||||||
|
console.log('Second after hook called with result:', result)
|
||||||
|
return result + 2
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(obj.method(2)) // 7
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// First after hook called with result: 4
|
||||||
|
// Second after hook called with result: 5
|
||||||
|
// 7
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple `instead` hooks will only execute the **last one added**. To chain them, you must call the original function:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { instead } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => `original-${a}`
|
||||||
|
}
|
||||||
|
|
||||||
|
instead(obj, 'method', ([a], original) => {
|
||||||
|
console.log('First instead hook called with:', a)
|
||||||
|
return `first-${original(a)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
instead(obj, 'method', ([a], original) => {
|
||||||
|
console.log('Second instead hook called with:', a)
|
||||||
|
// Calling original will execute the previous instead hook
|
||||||
|
return `second-${original(a)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(obj.method('test')) // 'second-first-original-test'
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Second instead hook called with: test
|
||||||
|
// First instead hook called with: test
|
||||||
|
// second-first-original-test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removing hooks
|
||||||
|
|
||||||
|
Each hook returns an unpatch function that removes the hook:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { before } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
method: (a) => console.log('Original method called with:', a)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpatch = before(obj, 'method', ([a]) => {
|
||||||
|
console.log('Before method called with:', a)
|
||||||
|
return [a + 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
obj.method(2)
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Before method called with: 2
|
||||||
|
// Original method called with: 3
|
||||||
|
|
||||||
|
unpatch() // Remove the hook
|
||||||
|
|
||||||
|
obj.method(2)
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Original method called with: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Unpatch functions are safe to call multiple times and won't throw errors.
|
||||||
|
|
||||||
|
### Constructor patching
|
||||||
|
|
||||||
|
You can also patch constructor functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { before } from '@revenge-mod/patcher'
|
||||||
|
|
||||||
|
class MyClass {
|
||||||
|
constructor(value) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exports = { MyClass }
|
||||||
|
|
||||||
|
before(exports, 'MyClass', ([value]) => {
|
||||||
|
console.log('Constructor called with:', value)
|
||||||
|
return [`modified-${value}`]
|
||||||
|
})
|
||||||
|
|
||||||
|
const instance = new exports.MyClass('test')
|
||||||
|
console.log(instance.value) // 'modified-test'
|
||||||
|
// CONSOLE OUTPUT:
|
||||||
|
// Constructor called with: test
|
||||||
|
// modified-test
|
||||||
|
```
|
||||||
|
|
|
||||||
472
lib/patcher/src/index.test.ts
Normal file
472
lib/patcher/src/index.test.ts
Normal file
|
|
@ -0,0 +1,472 @@
|
||||||
|
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||||
|
import { after, before, instead } from '.'
|
||||||
|
|
||||||
|
describe('Patcher', () => {
|
||||||
|
let originalMethod: (...args: any[]) => string
|
||||||
|
let obj: { method: typeof originalMethod }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalMethod = mock(s => s ?? 'original')
|
||||||
|
obj = {
|
||||||
|
method: originalMethod,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('before', () => {
|
||||||
|
test('should execute callback before original method', () => {
|
||||||
|
const beforeCallback = mock(args => args)
|
||||||
|
before(obj, 'method', beforeCallback)
|
||||||
|
|
||||||
|
obj.method('arg1', 'arg2')
|
||||||
|
|
||||||
|
expect(beforeCallback).toHaveBeenCalledWith(['arg1', 'arg2'])
|
||||||
|
expect(originalMethod).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow modifying arguments', () => {
|
||||||
|
before(obj, 'method', args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
obj.method('original')
|
||||||
|
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('modified')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should unpatch correctly', () => {
|
||||||
|
const beforeCallback = mock(args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
const unpatch = before(obj, 'method', beforeCallback)
|
||||||
|
|
||||||
|
// Test that patch is working
|
||||||
|
obj.method('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('modified')
|
||||||
|
|
||||||
|
// Unpatch
|
||||||
|
unpatch()
|
||||||
|
|
||||||
|
// Test that patch is removed
|
||||||
|
obj.method('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('original')
|
||||||
|
expect(beforeCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle multiple before hooks and unpatch correctly', () => {
|
||||||
|
const firstHook = mock(args => {
|
||||||
|
args[0] = 'first'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
const secondHook = mock(args => {
|
||||||
|
args[0] = `${args[0]}-second`
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
const unpatch2 = before(obj, 'method', secondHook)
|
||||||
|
const unpatch1 = before(obj, 'method', firstHook)
|
||||||
|
|
||||||
|
obj.method('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('first-second')
|
||||||
|
|
||||||
|
// Unpatch second hook
|
||||||
|
unpatch2()
|
||||||
|
|
||||||
|
obj.method('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('first')
|
||||||
|
|
||||||
|
// Unpatch first hook
|
||||||
|
unpatch1()
|
||||||
|
|
||||||
|
obj.method('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalledWith('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not crash when unpatch is called multiple times', () => {
|
||||||
|
const beforeCallback = mock(args => args)
|
||||||
|
const unpatch = before(obj, 'method', beforeCallback)
|
||||||
|
|
||||||
|
unpatch()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('after', () => {
|
||||||
|
test('should execute callback after original method', () => {
|
||||||
|
const afterCallback = mock(() => 'original')
|
||||||
|
after(obj, 'method', afterCallback)
|
||||||
|
|
||||||
|
const result = obj.method()
|
||||||
|
|
||||||
|
expect(afterCallback).toHaveBeenCalledWith('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow modifying return value', () => {
|
||||||
|
after(obj, 'method', () => 'modified')
|
||||||
|
|
||||||
|
const result = obj.method()
|
||||||
|
|
||||||
|
expect(result).toBe('modified')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should unpatch correctly', () => {
|
||||||
|
const afterCallback = mock(() => 'modified')
|
||||||
|
const unpatch = after(obj, 'method', afterCallback)
|
||||||
|
|
||||||
|
// Test that patch is working
|
||||||
|
let result = obj.method()
|
||||||
|
expect(result).toBe('modified')
|
||||||
|
expect(afterCallback).toHaveBeenCalledWith('original')
|
||||||
|
|
||||||
|
// Unpatch
|
||||||
|
unpatch()
|
||||||
|
|
||||||
|
// Test that patch is removed
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('original')
|
||||||
|
expect(afterCallback).toHaveBeenCalledTimes(1) // Only called before unpatch
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle multiple after hooks and unpatch correctly', () => {
|
||||||
|
const firstHook = mock(result => `${result}-first`)
|
||||||
|
const secondHook = mock(result => `${result}-second`)
|
||||||
|
|
||||||
|
const unpatch1 = after(obj, 'method', firstHook)
|
||||||
|
const unpatch2 = after(obj, 'method', secondHook)
|
||||||
|
|
||||||
|
let result = obj.method()
|
||||||
|
expect(result).toBe('original-second-first')
|
||||||
|
|
||||||
|
// Unpatch second hook (most recent)
|
||||||
|
unpatch2()
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('original-first')
|
||||||
|
|
||||||
|
// Unpatch first hook
|
||||||
|
unpatch1()
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not crash when unpatch is called multiple times', () => {
|
||||||
|
const afterCallback = mock(result => result)
|
||||||
|
const unpatch = after(obj, 'method', afterCallback)
|
||||||
|
|
||||||
|
unpatch()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('instead', () => {
|
||||||
|
test('should replace original method', () => {
|
||||||
|
const replacement = mock(() => 'replaced')
|
||||||
|
instead(obj, 'method', replacement)
|
||||||
|
|
||||||
|
const result = obj.method('arg1')
|
||||||
|
|
||||||
|
expect(replacement).toHaveBeenCalledWith(
|
||||||
|
['arg1'],
|
||||||
|
expect.any(Function),
|
||||||
|
)
|
||||||
|
expect(originalMethod).not.toHaveBeenCalled()
|
||||||
|
expect(result).toBe('replaced')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should provide access to original method', () => {
|
||||||
|
instead(obj, 'method', (_args, original) => {
|
||||||
|
return `${original()} modified`
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = obj.method()
|
||||||
|
|
||||||
|
expect(result).toBe('original modified')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should unpatch correctly', () => {
|
||||||
|
const replacement = mock(() => 'replaced')
|
||||||
|
const unpatch = instead(obj, 'method', replacement)
|
||||||
|
|
||||||
|
// Test that patch is working
|
||||||
|
let result = obj.method()
|
||||||
|
expect(result).toBe('replaced')
|
||||||
|
expect(originalMethod).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Unpatch
|
||||||
|
unpatch()
|
||||||
|
|
||||||
|
// Test that patch is removed
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('original')
|
||||||
|
expect(originalMethod).toHaveBeenCalled()
|
||||||
|
expect(replacement).toHaveBeenCalledTimes(1) // Only called before unpatch
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle multiple instead hooks and unpatch correctly', () => {
|
||||||
|
const firstHook = mock((_args, original) => `first-${original()}`)
|
||||||
|
const secondHook = mock((_args, original) => `second-${original()}`)
|
||||||
|
|
||||||
|
const unpatch1 = instead(obj, 'method', firstHook)
|
||||||
|
const unpatch2 = instead(obj, 'method', secondHook)
|
||||||
|
|
||||||
|
let result = obj.method()
|
||||||
|
expect(result).toBe('second-first-original')
|
||||||
|
|
||||||
|
// Unpatch second hook (most recent)
|
||||||
|
unpatch2()
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('first-original')
|
||||||
|
|
||||||
|
// Unpatch first hook
|
||||||
|
unpatch1()
|
||||||
|
result = obj.method()
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not crash when unpatch is called multiple times', () => {
|
||||||
|
const replacement = mock(() => 'replaced')
|
||||||
|
const unpatch = instead(obj, 'method', replacement)
|
||||||
|
|
||||||
|
unpatch()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
expect(() => unpatch()).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mixed hooks', () => {
|
||||||
|
test('should handle before + after', () => {
|
||||||
|
const beforeHook = mock(args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
const afterHook = mock(result => `${result}-after`)
|
||||||
|
|
||||||
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
||||||
|
const unpatchAfter = after(obj, 'method', afterHook)
|
||||||
|
|
||||||
|
let result = obj.method('original')
|
||||||
|
expect(result).toBe('modified-after')
|
||||||
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
||||||
|
expect(afterHook).toHaveBeenCalledWith('modified')
|
||||||
|
|
||||||
|
// Unpatch after hook
|
||||||
|
unpatchAfter()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('modified')
|
||||||
|
|
||||||
|
// Unpatch before hook
|
||||||
|
unpatchBefore()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
test('should handle instead + before', () => {
|
||||||
|
const insteadHook = mock(
|
||||||
|
(args, original) => `${original(...args)}-instead`,
|
||||||
|
)
|
||||||
|
const beforeHook = mock(args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
||||||
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
||||||
|
|
||||||
|
let result = obj.method('original')
|
||||||
|
expect(result).toBe('modified-instead')
|
||||||
|
expect(insteadHook).toHaveBeenCalledWith(
|
||||||
|
['modified'],
|
||||||
|
expect.any(Function),
|
||||||
|
)
|
||||||
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
||||||
|
|
||||||
|
// Unpatch before hook
|
||||||
|
unpatchBefore()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original-instead')
|
||||||
|
|
||||||
|
// Unpatch instead hook
|
||||||
|
unpatchInstead()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle instead + after', () => {
|
||||||
|
const insteadHook = mock(() => 'oops')
|
||||||
|
const afterHook = mock(result => `${result}-after`)
|
||||||
|
|
||||||
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
||||||
|
const unpatchAfter = after(obj, 'method', afterHook)
|
||||||
|
|
||||||
|
let result = obj.method('original')
|
||||||
|
expect(result).toBe('oops-after')
|
||||||
|
expect(insteadHook).toHaveBeenCalledWith(
|
||||||
|
['original'],
|
||||||
|
expect.any(Function),
|
||||||
|
)
|
||||||
|
expect(afterHook).toHaveBeenCalledWith('oops')
|
||||||
|
|
||||||
|
// Unpatch after hook
|
||||||
|
unpatchAfter()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('oops')
|
||||||
|
|
||||||
|
// Unpatch instead hook
|
||||||
|
unpatchInstead()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle instead + before + after', () => {
|
||||||
|
const insteadHook = mock(
|
||||||
|
(args, original) => `${original(...args)}-instead`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const beforeHook = mock(args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
const afterHook = mock(result => `${result}-after`)
|
||||||
|
|
||||||
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
||||||
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
||||||
|
const unpatchAfter = after(obj, 'method', afterHook)
|
||||||
|
|
||||||
|
let result = obj.method('original')
|
||||||
|
expect(result).toBe('modified-instead-after')
|
||||||
|
expect(insteadHook).toHaveBeenCalledWith(
|
||||||
|
['modified'],
|
||||||
|
expect.any(Function),
|
||||||
|
)
|
||||||
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
||||||
|
expect(afterHook).toHaveBeenCalledWith('modified-instead')
|
||||||
|
|
||||||
|
// Unpatch after hook
|
||||||
|
unpatchAfter()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('modified-instead')
|
||||||
|
|
||||||
|
// Unpatch before hook
|
||||||
|
unpatchBefore()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original-instead')
|
||||||
|
|
||||||
|
// Unpatch instead hook
|
||||||
|
unpatchInstead()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle before + after + instead', () => {
|
||||||
|
const beforeHook = mock(args => {
|
||||||
|
args[0] = 'before-modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
const afterHook = mock(result => `${result}-after`)
|
||||||
|
|
||||||
|
const insteadHook = mock(
|
||||||
|
(args, original) => `${original(...args)}-instead`,
|
||||||
|
)
|
||||||
|
|
||||||
|
const unpatchBefore = before(obj, 'method', beforeHook)
|
||||||
|
const unpatchAfter = after(obj, 'method', afterHook)
|
||||||
|
const unpatchInstead = instead(obj, 'method', insteadHook)
|
||||||
|
|
||||||
|
let result = obj.method('original')
|
||||||
|
expect(result).toBe('before-modified-instead-after')
|
||||||
|
expect(beforeHook).toHaveBeenCalledWith(['before-modified'])
|
||||||
|
expect(afterHook).toHaveBeenCalledWith('before-modified-instead')
|
||||||
|
expect(insteadHook).toHaveBeenCalledWith(
|
||||||
|
['before-modified'],
|
||||||
|
expect.any(Function),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unpatch after hook
|
||||||
|
unpatchAfter()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('before-modified-instead')
|
||||||
|
|
||||||
|
// Unpatch before hook
|
||||||
|
unpatchBefore()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original-instead')
|
||||||
|
|
||||||
|
// Unpatch instead hook
|
||||||
|
unpatchInstead()
|
||||||
|
result = obj.method('original')
|
||||||
|
expect(result).toBe('original')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should restore original method when all hooks are unpatched', () => {
|
||||||
|
const originalRef = obj.method
|
||||||
|
|
||||||
|
const unpatch1 = before(obj, 'method', args => args)
|
||||||
|
const unpatch2 = after(obj, 'method', result => result)
|
||||||
|
|
||||||
|
// Method should be patched
|
||||||
|
expect(obj.method).not.toBe(originalRef)
|
||||||
|
|
||||||
|
unpatch1()
|
||||||
|
// Still patched because after hook remains
|
||||||
|
expect(obj.method).not.toBe(originalRef)
|
||||||
|
|
||||||
|
unpatch2()
|
||||||
|
// Should be restored to original
|
||||||
|
expect(obj.method).toBe(originalRef)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// describe('error handling', () => {
|
||||||
|
// test('should throw error for invalid parent', () => {
|
||||||
|
// expect(() => before(null as any, 'method', () => [])).toThrow()
|
||||||
|
// expect(() => before(undefined as any, 'method', () => [])).toThrow()
|
||||||
|
// expect(() => before('string' as any, 'method', () => [])).toThrow()
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test('should throw error for non-existent property', () => {
|
||||||
|
// expect(() => before(obj, 'nonExistent' as any, () => [])).toThrow(
|
||||||
|
// "Property 'nonExistent' does not exist on parent",
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test('should throw error for non-function property', () => {
|
||||||
|
// const objWithNonFunction = { prop: 'not a function' }
|
||||||
|
// expect(() =>
|
||||||
|
// before(objWithNonFunction, 'prop' as any, () => []),
|
||||||
|
// ).toThrow("Property 'prop' is not a function")
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test('should throw error for invalid hook', () => {
|
||||||
|
// expect(() => before(obj, 'method', null as any)).toThrow()
|
||||||
|
// expect(() => before(obj, 'method', 'string' as any)).toThrow()
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
describe('constructor patching', () => {
|
||||||
|
test('should patch constructor functions', () => {
|
||||||
|
class TestClass {
|
||||||
|
value: string
|
||||||
|
constructor(value: string) {
|
||||||
|
this.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = { TestClass }
|
||||||
|
|
||||||
|
const beforeHook = mock(args => {
|
||||||
|
args[0] = 'modified'
|
||||||
|
return args
|
||||||
|
})
|
||||||
|
|
||||||
|
before(obj, 'TestClass', beforeHook)
|
||||||
|
|
||||||
|
const instance = new obj.TestClass('original')
|
||||||
|
expect(instance.value).toBe('modified')
|
||||||
|
expect(beforeHook).toHaveBeenCalledWith(['modified'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
29
lib/patcher/tsconfig.json
Normal file
29
lib/patcher/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"types": ["bun"],
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"skipDefaultLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"@parcel/watcher": "^2.5.1",
|
"@parcel/watcher": "^2.5.1",
|
||||||
"@react-native-clipboard/clipboard": "^1.16.2",
|
"@react-native-clipboard/clipboard": "^1.16.2",
|
||||||
"@swc/core": "^1.11.31",
|
"@swc/core": "^1.11.31",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "^1.2.15",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
"@unbound-mod/hermesc": "^1.0.3",
|
"@unbound-mod/hermesc": "^1.0.3",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue