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

dn-ioc

v0.2.1

Published

A lightweight, type-safe Inversion of Control (IoC) dependency injection library for TypeScript, built with a functional programming paradigm.

Downloads

1,289

Readme

dn-ioc

dn-ioc is a lightweight, type-safe dependency injection library for TypeScript.

It keeps the functional core of the library, but uses a more Angular-like installation model:

  • bootstrapApp(fn, { providers }) is the app entry point
  • provide(factory, { providers }) installs local providers for a subtree
  • provideXxx() style helpers return provider bundles
  • token() and provideFor() are available for pure contracts and explicit binding

Installation

bun add dn-ioc

Quick Start

import { bootstrapApp, provide } from 'dn-ioc'

const configRef = provide(() => ({ greeting: 'hello' }))

const greeterRef = provide(({ inject }) => {
  const config = inject(configRef)

  return {
    greet(name: string) {
      return `${config.greeting}, ${name}`
    },
  }
})

await bootstrapApp(({ inject }) => {
  const greeter = inject(greeterRef)
  console.log(greeter.greet('world'))
})

Mental Model

dn-ioc is not a full container framework like Spring or Nest.

It is a DI kernel with two public installation points:

  • bootstrapApp(fn, { providers })
  • provide(factory, { providers })

The key rule is:

Providers are shared within the installation scope where they are installed.

That means:

  • install at app level -> shared by the app root subtree
  • install inside a provider -> shared only by that provider subtree
  • install again deeper -> the deeper subtree gets a new instance

Runtime Handles and Security Boundary

dn-ioc returns opaque frozen handles from token(), provide(), provideFor(), and bundleProviders().

The public handle objects do not expose internal fields such as factory, id, local providers, or bundle items. Normal user code cannot reassign those internals or forge a valid handle by copying an object shape.

This is still an in-process DI kernel for trusted application code. It is not a JavaScript sandbox and should not be used as the only isolation boundary for untrusted plugins or third-party code.

Ref vs Token

Ref and Token look similar from the outside, but they have different jobs.

  • Token<T>
    • pure contract
    • has no default implementation
    • must be installed explicitly with provideFor(...)
  • Ref<T>
    • self-providing key
    • has a default factory
    • if it is first resolved inside a scope that does not already bind it, that scope gets the default binding

This is why provide(...) is still the main abstraction for everyday code, while token(...) is better for framework helpers and pure contracts.

How Scope Is Chosen

When inject(key) runs:

  1. dn-ioc first looks upward for an existing binding.
  2. If the key is a Token, it must find an explicit binding or it throws.
  3. If the key is a Ref, it can install its own default factory into the current active installation scope.
  4. The resulting instance is cached inside that scope.

This keeps the rule simple:

  • explicit installation decides visibility
  • first resolution decides where a self-providing Ref binds by default

Local providers do not retroactively change a consumer Ref that has already been bound or cached in a parent scope. If a local override must affect a consumer service, install or rebind both the dependency and that consumer Ref in the same local providers list.

Public API

interface Token<T> {}
interface Ref<T> extends Token<T> {}

type InjectKey<T> = Token<T> | Ref<T>

type ProviderInput = Ref<unknown> | ProviderDef<unknown> | ProviderBundle | readonly ProviderInput[]

type BootstrapAppFn<TResult> = (ctx: Context) => TResult | Promise<TResult>

interface BootstrapAppOptions {
  providers?: ProviderInput[]
}

interface Context {
  inject<T>(key: InjectKey<T>): T
}

function token<T>(description?: string): Token<T>

function provide<T>(
  factory: (ctx: Context) => T,
  options?: { providers?: ProviderInput[] },
): Ref<T>

function provideFor<T>(
  key: InjectKey<T>,
  factory: (ctx: Context) => T,
  options?: { providers?: ProviderInput[] },
): ProviderDef<T>

function bundleProviders(...inputs: ProviderInput[]): ProviderBundle

function bootstrapApp<TResult>(
  fn: BootstrapAppFn<TResult>,
  options?: BootstrapAppOptions,
): Promise<TResult>

Recommended Authoring Style

The default style is:

  • resolve dependencies once at the top of the factory
  • return an object that closes over those dependencies
const formatterRef = provide(({ inject }) => {
  const prefix = inject(prefixRef)
  const suffix = inject(suffixRef)

  return {
    format(value: string) {
      return `${prefix}${value}${suffix}`
    },
  }
})

This is the recommended shape for service objects, application services, repositories, and DDD-style orchestration code.

Method-level inject() is still valid, but it should be reserved for:

  • lazy loading a heavy dependency
  • intentionally reading from a deeper local binding
  • breaking a circular dependency

App-Level Installation

Use bootstrapApp(..., { providers }) when a provider should be visible to the whole app tree.

import { bootstrapApp, provide, provideFor, token } from 'dn-ioc'

const prefixToken = token<string>('Prefix')

const formatterRef = provide(({ inject }) => {
  const prefix = inject(prefixToken)

  return {
    format(value: string) {
      return `${prefix}${value}`
    },
  }
})

await bootstrapApp(
  ({ inject }) => {
    const formatter = inject(formatterRef)
    console.log(formatter.format('demo'))
  },
  {
    providers: [provideFor(prefixToken, () => '[app] ')],
  },
)

Local Installation

Use provide(factory, { providers }) when a dependency should be visible only to one subtree.

import { bootstrapApp, provide, provideFor, token } from 'dn-ioc'

const labelToken = token<string>('Label')

const rendererRef = provide(({ inject }) => {
  const label = inject(labelToken)
  return { label }
})

const localDemoRef = provide(
  ({ inject }) => {
    const renderer = inject(rendererRef)
    return {
      label: inject(labelToken),
      renderer,
    }
  },
  {
    providers: [provideFor(labelToken, () => 'local-demo')],
  },
)

await bootstrapApp(({ inject }) => {
  const demo = inject(localDemoRef)
  console.log(demo.label)
  console.log(demo.renderer.label)
})

The local labelToken binding is only visible inside localDemoRef and its descendants.

Installation Functions

Angular-style installation helpers should return a provider bundle.

import { bundleProviders, provideFor, token } from 'dn-ioc'

const themeToken = token<{ palette: string }>('Theme')
const themeOptionsToken = token<{ palette: string }>('ThemeOptions')

export function provideDemoTheme(options: { palette: string }) {
  return bundleProviders(
    provideFor(themeOptionsToken, () => options),
    provideFor(themeToken, ({ inject }) => {
      const themeOptions = inject(themeOptionsToken)
      return {
        palette: themeOptions.palette,
      }
    }),
  )
}

Install it at app level:

const previewRef = provide(({ inject }) => {
  const theme = inject(themeToken)
  return { theme }
})

await bootstrapApp(
  ({ inject }) => {
    const preview = inject(previewRef)
    console.log(preview.theme.palette)
  },
  {
    providers: [provideDemoTheme({ palette: 'ocean' })],
  },
)

Or install it locally:

const sandboxRef = provide(
  ({ inject }) => {
    const theme = inject(themeToken)
    return { theme }
  },
  {
    providers: [provideDemoTheme({ palette: 'sunset' })],
  },
)

If a lower subtree needs a new instance, install provideDemoTheme(...) again in that subtree.

Testing

provideFor() is the main test replacement API.

import { bootstrapApp, provide, provideFor } from 'dn-ioc'

const storageRef = provide(() => ({
  read(key: string) {
    return `prod:${key}`
  },
}))

const serviceRef = provide(({ inject }) => {
  const storage = inject(storageRef)

  return {
    load(key: string) {
      return storage.read(key)
    },
  }
})

const result = await bootstrapApp(
  ({ inject }) => inject(serviceRef).load('demo'),
  {
    providers: [
      provideFor(storageRef, () => ({
        read(key: string) {
          return `mock:${key}`
        },
      })),
    ],
  },
)

Async Inject Semantics

inject() always returns the real value produced by the factory.

  • sync factory -> sync value
  • async factory -> promise

That means users can write:

const settingsRef = provide(async () => {
  return { ready: true }
})

await bootstrapApp(async ({ inject }) => {
  const settings = await inject(settingsRef)
  console.log(settings.ready)
})

The same rule also applies to provideFor(...).

For a pure async contract, put the promise in the token type:

type Settings = { ready: boolean }

const settingsToken = token<Promise<Settings>>('Settings')

await bootstrapApp(
  async ({ inject }) => {
    const settings = await inject(settingsToken)
    console.log(settings.ready)
  },
  {
    providers: [
      provideFor(settingsToken, async () => {
        return { ready: true }
      }),
    ],
  },
)

Using Token<Settings> with an async factory is a type error, because inject(settingsToken) would otherwise look synchronous while actually returning a promise.

Async results are cached inside the same installation scope. If an async provider rejects, the failure is not cached and a later injection can retry.

Migration Notes

This version intentionally moves away from the old API:

  • runInInjectionContext -> replaced by bootstrapApp
  • resetGlobalInstances -> removed
  • overrides -> replaced by provideFor
  • mode: 'global' | 'standalone' -> removed from the public API

The new rule is:

  • install at the root when you want app-level sharing
  • install locally when you want subtree sharing
  • reinstall deeper when you want a new subtree instance

Why This Shape

The design borrows the most useful ideas from mainstream DI systems:

  • Angular: hierarchical installation and provideXxx() helpers
  • Spring / .NET: instances belong to a container or scope, not to the whole program
  • lightweight TS DI libraries: factory-first, explicit bindings, no heavy reflection

At the same time, it avoids heavier container features such as:

  • module graphs
  • public service locator APIs
  • decorator-driven registration
  • large lifecycle matrices

That keeps dn-ioc small, explicit, and easy to reason about.