npm.io
2.0.1 • Published 4d ago

named-patch

Licence
MIT
Version
2.0.1
Deps
0
Size
10 kB
Vulns
0
Weekly
0
Stars
9

named-patch

Higher-order function for patching named export functions in tests.

npm package License

Contents

Install

npm i named-patch

Example

// a.ts
import { patch } from 'named-patch';

export const randName = patch(<T extends string>(names: T[]) => names[Math.trunc(Math.random() * names.length)]);

// test.ts
import { patchKey } from 'named-patch';
import { randName } from './a.js';

randName(['foo', 'bar']); // 'foo' or 'bar'
// Still supports generics
randName<'abc' | 'xyz'>(['abc', 'xyz']); // 'abc' or 'xyz'

randName[patchKey] = () => '<custom>';
randName(['foo', 'bar']); // '<custom>'

Usage

named-patch is an ESM module. It must be imported. To load from a CJS module, use dynamic import: const { patch } = await import('named-patch').

The package exports two different implementations depending on the Node.js condition in use:

  • Default (no special condition): patch returns the function unchanged — zero overhead in production.
  • patchable condition: enables the wrapping behaviour along with patchKey and getPatched.

Enable the patchable condition in test environments:

node --conditions=patchable ./my-script.js
NODE_OPTIONS='--conditions=patchable' node ./my-script.js

Mocha users can set this via the node-option config key.

The key use case is replacing module mocking with runtime patching: wrap any function with patch() at the module level, then reassign fn[patchKey] in tests. Because patch() is idempotent and cached, any file that imports the same original function and passes it through patch() will receive the same patchable wrapper — making third-party functions patchable without re-exporting them.

API

patch(fn)

Returns a patchable wrapper around fn. By default the wrapper calls fn internally. Reassign wrapper[patchKey] to swap the implementation at runtime.

The wrapper preserves the full TypeScript type of fn, including generics, async, and this binding. Patching is idempotent: calling patch() on the same function or on an already-patched wrapper always returns the same wrapper object.

Parameters

Parameter Type Default Description
fn (...args: any[]) => any — Required. The function to make patchable.

Returns PatchableInterface<T> — a wrapper with the same signature as fn plus a writable [patchKey] property. The wrapper also exposes fn's own properties (e.g. static helpers), reading and writing through to fn.

import { patch } from 'named-patch';

const original = (x: number, y: number) => x + y;
const patched = patch(original);

patch(original) === patched; // true
patch(patched) === patched;  // true

patchKey

A unique symbol written onto every wrapper returned by patch. Assign a new function to wrapper[patchKey] to replace the implementation.

Only exported when the patchable condition is active.

import { patch, patchKey } from 'named-patch';
import { stub } from 'sinon';

const fn = patch((x: number) => x * 2);
stub(fn, patchKey).returns(99);
fn(5); // 99

getPatched(fn)

Returns the already-cached patchable wrapper for fn without creating one. Use this in tests when you want to assert that patch() was called on a function rather than silently creating the wrapper for the first time.

Only exported when the patchable condition is active.

Parameters

Parameter Type Default Description
fn (...args: any[]) => any — Required. The original (unpatched) function to look up.

Returns PatchableInterface<T> — the cached wrapper.

Throws Error — if fn has never been passed to patch(), or if fn is itself already a patched wrapper.

Keywords