possess
v1.1.0
Published
Structured monkey-patching for JavaScript and TypeScript. Hook into any function or constructor with before, instead, and after patches.
Maintainers
Readme
Possess
Structured monkey-patching for JavaScript and TypeScript. Hook into any function with before, instead, and after patches, then cleanly revert when you're done.
Features
before,instead, andafterhooks for any function or constructor- Full type inference for arguments, return type, and
thiscontext - Scoped patchers via
createPatcher()for grouped cleanup - One-shot patches with
{ once: true } - Individual, per-caller, or global unpatch
- WeakRef-based, so patched objects can still be garbage collected
- Patches are isolated with try/catch. One bad callback won't break the rest.
- Preserves
.toString()and property descriptors on patched functions - Zero runtime dependencies
Install
npm install possessbun add possessQuick start
import { before } from 'possess';
const api = {
greet: (name: string) => `Hello ${name}!`
};
// Intercept arguments before execution
const unpatch = before(api, 'greet', (ctx) => {
ctx.args[0] = ctx.args[0].toUpperCase();
});
api.greet('world'); // "Hello WORLD!"
// Remove the patch
unpatch();
api.greet('world'); // "Hello world!"API
before(parent, method, callback, options?)
Runs before the original function. You can modify arguments here.
import { before } from 'possess';
// Modify arguments via ctx.args
before(api, 'greet', (ctx) => {
ctx.args[0] = 'Override';
});
// Or return a new arguments array
before(api, 'greet', (ctx) => {
return ['Override'];
});instead(parent, method, callback, options?)
Replaces the original function. The original is not called unless you call ctx.original() yourself.
import { instead } from 'possess';
// Complete replacement
instead(api, 'greet', (ctx) => {
return `Goodbye ${ctx.args[0]}!`;
});
// Delegate to original, preserving this and args
instead(api, 'greet', (ctx) => {
return ctx.original.apply(ctx.this, ctx.args);
});
// Modify arguments, then delegate
instead(api, 'greet', (ctx) => {
ctx.args[0] = ctx.args[0].toUpperCase();
return ctx.original.apply(ctx.this, ctx.args);
});after(parent, method, callback, options?)
Runs after the original (or any instead patches). You can read or change the result.
import { after } from 'possess';
// Override the result by returning a value
after(api, 'greet', (ctx) => {
return ctx.result + ' (modified)';
});
// Or modify ctx.result directly
after(api, 'greet', (ctx) => {
ctx.result = ctx.result + ' (modified)';
});createPatcher(caller)
Creates a scoped patcher. All patches share a caller ID, so you can remove them together.
import { createPatcher } from 'possess';
const patcher = createPatcher('my-plugin');
patcher.before(api, 'greet', (ctx) => {
console.log('greet called with:', ctx.args);
});
patcher.after(api, 'greet', (ctx) => {
console.log('greet returned:', ctx.result);
});
// Only removes patches from this patcher
patcher.unpatchAll();Also accepts an options object:
const patcher = createPatcher({ caller: 'my-plugin' });unpatchAll()
Removes every active patch globally.
import { unpatchAll } from 'possess';
unpatchAll();unpatchAllByCaller(caller)
Removes all patches with a given caller ID.
import { unpatchAllByCaller } from 'possess';
unpatchAllByCaller('my-plugin');PatchContext
Every callback receives a PatchContext object:
| Property | Type | Description |
|----------|------|-------------|
| ctx.args | Args | The function arguments. Mutable in before callbacks. |
| ctx.result | Res \| null | The return value. null before execution, populated in after. |
| ctx.original | (...args) => Res | The original unpatched function. |
| ctx.this | Self | The this binding of the current call. |
PatchOptions
| Option | Type | Description |
|--------|------|-------------|
| once | boolean | Remove the patch automatically after it runs once. |
| caller | string | Groups patches together. Used by unpatchAllByCaller(). |
import { before } from 'possess';
// Fires once, then removes itself
before(api, 'greet', (ctx) => {
console.log('First call only:', ctx.args);
}, { once: true });Type inference
Types are inferred automatically from the parent object. Given a typed module, ctx.args, ctx.result, and ctx.this are all fully typed without any extra work:
import { before, after } from 'possess';
const api = {
getUser(id: number): { name: string; age: number } {
return { name: 'Alice', age: 30 };
}
};
before(api, 'getUser', (ctx) => {
ctx.args; // [id: number]
ctx.this; // typeof api
});
after(api, 'getUser', (ctx) => {
ctx.result; // { name: string; age: number } | null (null in before/instead, populated in after)
});If you need to type the callback separately (e.g. when defining it outside the patch call), use PatchContext directly:
import { before, type PatchContext } from 'possess';
function logArgs(ctx: PatchContext<[id: number], { name: string; age: number }>) {
console.log('id:', ctx.args[0]); // number
console.log('result:', ctx.result); // { name: string; age: number } | null
}
before(api, 'getUser', logArgs);The three type parameters are PatchContext<Args, Result, Self>, all optional and default to any.
Class patching
Same API, works with constructors:
import { before, instead, after } from 'possess';
const module = {
User: class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
};
// Modify constructor arguments
before(module, 'User', () => {
return ['Jeff'];
});
// Intercept construction
instead(module, 'User', (ctx) => {
const instance = ctx.original(...ctx.args);
instance.name = 'Intercepted';
return instance;
});
// Modify the instance after construction
after(module, 'User', (ctx) => {
ctx.result.name = ctx.result.name.toUpperCase();
return ctx.result;
});Execution order
- All
beforepatches (registration order) - All
insteadpatches, or the original function if there are none - All
afterpatches (registration order)
Error handling
Each callback is wrapped in a try/catch. If a patch throws, the error goes to console.error and the next patch runs normally.
