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

wyr-ts

v2.0.2-rc2

Published

Deterministic dependency graphs for TypeScript.

Readme

wyr-ts

Deterministic dependency graphs for TypeScript.

wyr-ts is a small dependency wiring library for explicit, immutable provider graphs. You declare providers up front, compose modules with merge, tree-shake with shake, and resolve everything with compile. The TypeScript type system validates missing dependencies, mismatched dependency types, and circular dependency graphs at the call site.

Installation

npm install wyr-ts
import { Module, toClass, toFactory, toValue } from 'wyr-ts';

API at a glance

| Export | Purpose | | --------------------- | ---------------------------------------------------------------------------------- | | Module(providers) | Creates an immutable module from a record of providers. | | toValue(value) | Registers a dependency-free constant or promise-backed value. | | toFactory(keys, fn) | Registers a factory whose positional arguments are resolved from keys. | | toClass(keys, ctor) | Registers a class constructor whose positional arguments are resolved from keys. | | GraphErr<Graph> | Type-level error map: keys with wiring problems mapped to their error details. | | AnyGraph | Base constraint for a record of providers; useful for generic utilities. |

A module exposes:

| Method | Purpose | | --------------- | -------------------------------------------------------------------------------------------- | | shake(keys) | Returns a new module scoped to the given keys and their transitive dependencies. | | compile() | Resolves all keys eagerly and returns a Promise<Container>. Each .get(key) is synchronous. | | merge(module) | Returns a new module where providers from the argument replace providers with the same keys. |

Defining keys

Provider keys can be any JavaScript PropertyKey: string, number, or symbol. Use literal keys (as const) or unique symbols when you want TypeScript to track the graph precisely.

const database = Symbol('database'); // symbol key
const answer = 42 as const;          // number key

const services = Module({
  // Inline string key
  myService: toValue({ ready: true }),

  // Symbol key
  [database]: toValue({ connected: true }),

  // Numeric key
  [answer]: toValue('the answer'),
});

Basic usage

import { Module, toFactory, toValue } from 'wyr-ts';

const database = Symbol('database');
const repo = Symbol('repo');

const app = Module({
  config: toValue({ url: 'postgres://localhost/app' }),

  [database]: toFactory(['config'], async (cfg: { url: string }) => ({
    query: async (sql: string) => ({ sql, url: cfg.url }),
  })),

  [repo]: toFactory(
    [database],
    (db: { query: (sql: string) => Promise<unknown> }) => ({
      findUser: (id: string) => db.query(`select * from users where id = ${id}`),
    }),
  ),
});

const container = await app.shake([repo]).compile();
const userRepo = container.get(repo);
await userRepo.findUser('42');

toFactory receives dependencies as positional parameters in the same order as its key tuple. Factories may be synchronous or asynchronous.

Tree-shaking with shake

shake(keys) returns a new module containing only the given keys and their transitive dependencies. Use it to avoid resolving providers you don't need.

const container = await app.shake([repo]).compile();
// Only 'config', 'database', and 'repo' are resolved.
// Any other keys in the module are excluded.

Calling compile() without shake resolves every key in the module.

Class providers

Use toClass when a provider should instantiate a class. Constructor arguments are resolved from the key tuple in order.

class Greeter {
  constructor(
    private readonly message: string,
    private readonly excited: boolean,
  ) {}

  shout(): string {
    return this.excited ? `${this.message}!` : this.message;
  }
}

const greetings = Module({
  message: toValue('hello'),
  excited: toValue(true),
  greeter: toClass(['message', 'excited'], Greeter),
});

const container = await greetings.shake(['greeter']).compile();
container.get('greeter').shout(); // "hello!"

Composing modules with merge

merge returns a new module. Providers from the module passed to merge replace providers with matching keys from the base module.

const app = Module({
  config: toValue({ url: 'postgres://localhost/app' }),
});

const testOverrides = Module({
  config: toValue({ url: 'postgres://localhost/test' }),
});

const testApp = app.merge(testOverrides);
const container = await testApp.compile();
container.get('config').url; // 'postgres://localhost/test'

The original modules are not mutated.

Type safety

wyr-ts encodes each provider's dependency input types and output type. Calling compile() is a compile error unless the full graph is valid. Calling shake(keys).compile() is a compile error unless the transitive subgraph for those keys is valid.

TypeScript checks that:

  • every requested key exists in the module,
  • every transitive dependency key exists,
  • dependency output types satisfy the factory parameter types, and
  • dependency graphs are not circular.
const broken = Module({
  greeting: toFactory(['name'], (s: string) => s),
  // 'name' is never provided
});

// @ts-expect-error — 'name' is missing from the module
broken.compile();

// But unrelated valid keys are still accessible after shake:
const partial = Module({
  count: toValue(42),
  greeting: toFactory(['name'], (s: string) => s),
});

partial.shake(['count']).compile(); // ok — 'count' has no deps

// @ts-expect-error — 'name' is transitively missing
partial.shake(['greeting']).compile();

The validity? phantom field on a module is typed as GraphErr<Graph>, which surfaces the full error map for invalid graphs — hover over a module variable in your IDE to inspect wiring problems per key.

Runtime guards still reject missing providers and circular dependencies if you bypass the type system with casts.

Runtime behavior

  • A module is immutable after creation.
  • Every compile call uses a fresh resolution container.
  • Within a single call, shared dependencies are resolved once (memoized promises).
  • Independent dependencies are resolved concurrently with Promise.all.
  • Factory errors are not swallowed; they reject the compile promise.

Development

npx vitest run   # run tests
npx eslint src   # lint
npx tsc --noEmit # type-check

License

MIT