npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ghostry/extern

v0.0.11

Published

Dependency injection made seamless.

Readme

npm npmx jsr github typescript bun node

Provides the most seamless approach to typed dependency injection for test suites.

Example

Initialization (extern.ts)

import { initialize } from "@ghostry/extern";

/**
 * Initialize an instance of the library with optional
 * configuration.
 */
export const extern = await initialize({});

Source (source.ts)

/**
 * Import the library from the initialization module.
 */
import { extern } from "./extern.ts";

/**
 * The function to be mocked.
 *
 * For demonstration, it performs some simple math, but it represents
 * an interaction with an external system which would not be within
 * the intended scope of a unit test.
 */
function computeSomething(x: string) {
  return x + " brown";
}

/**
 * A function that represents interaction with an external system,
 * but unlike the other example function above, this does not return
 * anything useful to the calling code.
 */
function doSomething() {
  console.log("jumped over the lazy dog");
}

/**
 * A library-native type identity (or a Standard Schema) is used to:
 *
 * 1. Identify the wrapped code block and describe its return type.
 * 2. Associate with correctly-typed mocks in a test suite.
 */
export const identity = extern.T<string>();

export function example() {
  /**
   * Wrap external interactions that produce a value.
   */
  const result = extern.typed
    .by(identity)
    .will(() => computeSomething("quick"));

  /**
   * Wrap external interactions that perform an effect.
   */
  extern.effect.will(doSomething);

  return result + " fox";
}

If you wish to also perform data validation with this library, bring one or more of your choice of validation library that conforms to Standard Schema, and make these changes in the source code:

/**
 * Your chosen validation library.
 */
import * as S from "sury";

/**
 * Instead of a library-native type identity, use a schema.
 */
export const identity = S.string;

/**
 * Instead of `.typed`, use `.validated`.
 */
const result = extern.validated
  .by(identity)
  .will(() => computeSomething("quick"));

Test (test.ts)

import { extern } from "./extern.ts";
import { example, identity } from "./source.ts";
import { test, expect } from "bun:test";

test("example test", async () => {
  /**
   * By default, the function to test will still execute its
   * external interaction even though it was wrapped at its
   * source.  This prevents immediately breaking any existing
   * tests written for the function.
   */
  expect(example()).toEqual("quick brown fox");

  /**
   * Enable dependency-injection style mocking.
   *
   * This begins tracking uses of `extern` at runtime with
   * their associated schema definition.
   *
   * The `testing` function receives a function to mock uses
   * of `extern` with static test data.
   *
   * It identifies which external interaction to mock by the
   * schema definition used.  (In many cases, this will be
   * sufficient, but disambiguation is possible as necessary.)
   *
   * Effect blocks (unlike value blocks) do not need to be
   * mocked as they will always be skipped.  (Spying on
   * them is still possible though.)
   */
  await extern.testing((mock) => {
    /**
     * The mocking data must conform to the schema type.  This
     * is enforced by TypeScript.
     *
     * Each mock returns a dedicated spy which can be used
     * later to inspect executions that used the mock.
     *
     * The definition of mocks is the same whether using
     * `typed` or `validated` mode.
     */
    const spy = mock(identity).with("a");

    /**
     * The function will not execute the external interactions.
     * Instead, the mocked data will be returned in its place.
     * Thus, the result is different, as expected.
     */
    expect(example()).toEqual("a fox");

    /**
     * Assertions can be made about the mocked executions
     * that have occurred.
     */
    expect(spy.executions).toHaveLength(1);
  });

  /**
   * Outside of the testing block, all wrapped interactions
   * will run normally, including effects.
   */
  expect(example()).toEqual("quick brown fox");
});

Philosophy

This library was born from a philosophy of testing an application system in isolation, in contrast to testing it in the live context of other systems. In other words: "unit testing" individual systems of an application as opposed to "integration testing" the collection of them.

The conventional approach to automated testing of an application is to construct a testing environment where all of the relevant interdependent systems are brought online and configured to use each other.

As a result, test suites can often suffer in performance because that approach does not scale well as the test suite expands. With decent test coverage of a moderately complex application, there will be a lot of cross-system communication and a lot of repeated testing of the same code paths. Both of these can significantly slow the suite as a whole.

Of course, holistic, integrated, end-to-end testing does have its place, but most tests do not need to actually excercise common code paths and cross-system communication repeatedly for the test to be useful and effective. It can often be perfectly sufficient to know that cross-system communication is attempted rather than truly performed.

One approach to achieve this would be traditional dependency injection. However, that often pollutes the interfaces of source code by altering function signatures or requiring unnatural abstractions to get an injection to where it needs to be. This library aims to provide dependency injection transparently with the minimal amount of "source code pollution" necessary.

API

There are 2 primary APIs for wrapping code that works with external systems. Which one to use depends on whether the wrapped code will produce a value or not.

For code that produces a value

If the source code block produces a value that must be substituted during tests, use extern.validated or extern.typed.

validated vs typed

When wrapping a source code block, there are two modes available to determine how the provided identity is used.

Using validated causes the return data of the extern block to be validated by the associated schema. This ensures that the resulting data is of the type defined by the schema. If validation fails, an InvalidDataTypeError will be thrown. Due to the Standard Schema specification, schema validation may or may not return a Promise. To account for this, an extern.validated block will always return the result wrapped in a Promise.

Using typed will not invoke any runtime validation of the data returned by the extern block. The associated identity will only be used to declare the type in TypeScript, but the actual data returned by the block may not be of that type at all. This can be preferred if the data type is already being validated within the block, as there is no need to incur additional runtime overhead of revalidating it. This may also be ideal if you wish to perform validation manually. The result of the block function will be returned directly as-is.

Note that schemas can be used with extern.typed, but they will only function for validation of TypeScript types, not for data validation at runtime.

by

Determines the schema used for the extern block.

The schema must support Standard Schema v1, but there are no other requirements. You can use multiple different schema libraries if you wish.

The exact same schema object should be available to both source code and tests since there is no standardized way of determining equality between different schema objects that represent the same schema structure.

[!CAUTION]

Be careful with an empty object schema, especially with an async block function!

A Promise itself satisfies the empty interface regardless of what it may wrap which can cause type confusion with the resulting value.

See https://typescript-eslint.io/rules/no-empty-object-type

named

Names the extern block for the sole purpose of disambiguating its mock in a test suite.

given

An extern block will usually need to reference parameters outside of itself to perform the desired external interaction. One way of making these references is to make a closure over outside variables:

function comments(postId: number) {
  return extern.typed.by(schema).will(
    /**
     * Makes a closure over the `postId` variable.
     */
    () => fetchComments(postId),
  );
}

But you may wish to make assertions in your tests about the data that was provided to an extern block. So instead, you can pass data into the extern block function from the extern chain with given:

function comments(postId: number) {
  /**
   * Parameter for extern block function is provided without
   * the need for a closure.
   */
  return extern.typed.by(schema).given(postId).will(fetchComments);
}

The given data is then available for assertions in your tests:

await extern.testing((mock) => {
  const spy = mock(schema).with([]);

  comments(123);

  expect(spy.executions[0]).toMatchObject({ given: 123 });
});

will

Defines the extern block function to be executed according to the extern chain preceding it.

It will receive the given data as its only parameter.

The return value is subject to the type defined by the associated schema and the mode defined in its extern chain.

Mocking requirements

Within extern.testing(), all value-producing extern blocks must be mocked. If such a block is used without a registered mock, an error will be thrown, even if the test would not otherwise fail. The expectation of this library is that no external interactions will actually occur during tests since testing scopes should be isolated for the sake of performance and reliability. If external interactions do need to occur during a test, the requirement can be disabled as necessary via passthrough() (as a later section covers).

Also, by default, any defined mock must end up being used by the end of the extern.testing() block, otherwise an UnusedMocksError will be thrown, even if the test would not otherwise fail. This prevents superfluous mocking that results in confusion about what setup is actually needed to run a test. To disable this requirement, pass an options object as the final argument for a mock registration to allow that particular mock to go unused: { unused: "allow" }. This can be useful for asserting that the corresponding source code block did not get executed.

Schema requirements

The schema used between an extern block and its corresponding mock must be the same JavaScript object (satisfying SameValueZero comparison). Therefore, the schema should be defined separately and exported in a way to be accessible to both the source code and tests.

Disambiguating mocks

If a test will be executing multiple external interactions that use the same schema definition within a single extern.testing() block, mock registration for that schema definition may need to be disambiguated. Without disambiguation, the same data will be used for all extern blocks using that schema.

Currently, only named is supported for disambiguation. To use this, define the name on the extern block and on the mock:

extern.typed
  .by(schema)
  .named("abc")
  .will(() => 123);
/**
 * This mock targets all extern blocks using the schema,
 * regardless of any disambiguations that may be assigned
 * to the extern block.
 */
mock(schema).with(321);

/**
 * This mock targets all extern blocks using the schema
 * that have been named "abc", taking priority over
 * less-specific mocks that would otherwise apply.
 */
mock(schema).named("abc").with(789);

Registering more than one mock with the same disambiguation for the same schema will immediately throw an error.

Skipping mocks

In some tests, you may wish to run the original code instead of mocking it, but not defining a mock will throw an UnusedMocksError.

You can achieve this by explicitly skipping that mock:

/** Disables the mocking requirement and runs the original code block. */
mock(schema).skip();

/** Disambiguation is also supported here. */
mock(schema).named("abc").skip();

For code that only performs an effect

If the source code block does not produce a value, then wrapping it with extern.effect allows for a simpler integration.

extern.effect.will(() => sendMetric("login.success"));

Because there is no return data, no schema is involved and no by is needed. The wrapped function must return void or Promise<void>.

By default, source code wrapped with extern.effect will be skipped within extern.testing(). Unlike value-producing blocks, there is no requirement to register a mock for it; an unmocked effect simply does nothing.

Outside of extern.testing(), the wrapped function runs normally as if extern were not involved.

named

Names the effect block so that it can be spied on in tests:

extern.effect
  .named("send login metric")
  .will(() => sendMetric("login.success"));

Unnamed effect blocks cannot be observed in tests. They are always skipped within extern.testing() with no opportunity to inspect them.

given

As with value-producing blocks, given provides data to the block function from the extern chain instead of through a closure, which makes the data available for assertions in tests:

function trackView(postId: number) {
  return extern.effect.named("track view").given(postId).will(sendViewMetric);
}
await extern.testing((mock) => {
  const spy = mock.effect.named("track view").observe();

  trackView(123);

  expect(spy.executions[0]).toMatchObject({ given: 123 });
});

given is only available after named, since spying on the captured data requires that the block be identifiable.

will

Defines the effect block function to be executed according to the extern chain preceding it.

It will receive the given data as its only parameter.

The return type must be void or Promise<void>.

Spying on effects

Named effects can be observed during a test in one of two ways:

/**
 * Tracks executions and continues to suppress the original
 * function (the default behavior for effects in tests).
 */
const spy = mock.effect.named("track view").observe();

/**
 * Tracks executions and allows the original function to run.
 */
const spy = mock.effect.named("track view").passthrough();

Each returns a spy whose executions array records every call to the matching effect block, with any given data attached. As with value-producing mocks, a registered effect spy must be exercised at least once before the extern.testing() block ends, otherwise an UnusedMocksError will be thrown.