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

@wirely/core

v0.1.1

Published

Function-based Dependency Injection core for TypeScript and pure JavaScript — no decorators, no reflection.

Downloads

313

Readme

@wirely/core

Function based Dependency Injection for TypeScript and pure JavaScript. It takes ideas from NestJS, but it has no decorators and no reflection in the core. You write plain classes, and you wire them in modules.

Introduction

Wirely is a small container for Dependency Injection (DI). DI means you do not create your dependencies by hand. You declare them, and the container builds them for you.

The main goal of Wirely is simple. Your classes stay pure. They do not import anything from Wirely. All the wiring lives in the module file. Because of this:

  • The same code works in TypeScript and in plain JavaScript.
  • There is no decorator and no reflect-metadata.
  • Your classes are easy to test, because they are only classes.

If you know NestJS, the ideas are close: providers, modules, scopes, lifecycle hooks, and a way to break circular dependencies. The difference is that here you declare everything with plain functions, not decorators.

Installation

Install the package with npm:

npm install @wirely/core

You need Node.js 18 or newer. TypeScript is optional. If you use TypeScript, you get full types and autocomplete. If you use plain JavaScript, the same API still works.

The smallest program looks like this:

import { createContainer, defineModule } from "@wirely/core";

class Greeter {
	hello() {
		return "hi";
	}
}

const AppModule = defineModule({ providers: [Greeter] });

const container = createContainer(AppModule);
container.get(Greeter).hello(); // "hi"

Providers

A provider is something the container can build and give to you. Most of the time a provider is a class (a service, a repository, a use case). But it can also be a value or a factory.

You declare a provider with defineProvider, inside the providers list of a module. A class without dependencies can be added directly, without defineProvider.

Class provider (useClass)

Use it for a class that has dependencies. You pass the dependencies in inject, in the same order as the constructor.

class UserRepository {}

class UserService {
	constructor(private readonly repo: UserRepository) {}
}

defineProvider({ useClass: UserService, inject: [UserRepository] });

The class stays pure. It does not import Wirely. Only the module knows about defineProvider.

Value provider (useValue)

Use it for a ready value, like a config object or a constant. Here you must give a provide token.

defineProvider({ provide: "CONFIG", useValue: { region: "br" } });

Factory provider (useFactory)

Use it when you need to compute the value. The factory can also receive dependencies through inject. This is good to pick one implementation at runtime.

defineProvider({
	provide: "PAYMENT",
	inject: ["CONFIG", StripeGateway, PixGateway],
	useFactory: (config, stripe, pix) => (config.region === "br" ? pix : stripe),
});

Tokens

A token is the key that the container uses to find a provider. A token can be:

  • a class, which is also its own token (this is the common case),
  • a string,
  • a symbol.

You use the class for normal services. You use a string or symbol for values and factories, where there is no class.

Every token must be unique in the container. If the same token is registered twice, you get a TokenCollisionError at registration time. This is on purpose, so two providers never fight for the same token.

Modules

A module groups providers that belong together. You declare a module with defineModule.

import { defineModule, defineProvider } from "@wirely/core";

export const UsersModule = defineModule({
	providers: [
		UserRepository,
		defineProvider({ useClass: UserService, inject: [UserRepository] }),
	],
	exports: [UserService],
});

Feature modules

Put each feature in its own module. For example a UsersModule, a BillingModule, and so on. This keeps the code organized and easy to find.

Shared modules and exports

A module is private by default. Other modules can only use what the module puts in exports. In the example above, UserService is exported, but UserRepository stays inside. If another module imports UsersModule, it can use UserService but not UserRepository.

If you try to read a provider that is not exported, you get an EncapsulationError.

The root module

You can pass a feature module straight to createContainer. But in a bigger app it is better to have one root AppModule that imports the feature modules:

const AppModule = defineModule({ imports: [UsersModule, BillingModule] });

const container = createContainer(AppModule);

Global modules

Sometimes one module is needed almost everywhere, like a logger. Mark it with global: true, and the tokens it exports become visible to every module, without an import. The global module still needs to be imported one time, so the container can register it.

const PlatformModule = defineModule({
	global: true,
	providers: [Logger],
	exports: [Logger],
});

Injection scopes

A scope decides how many times the container builds a provider.

  • singleton is the default. The container builds it one time and gives the same instance to everybody.
  • transient builds a new instance every time you ask for it.
defineProvider({ useClass: RequestContext, scope: "transient" });

If you do not set a scope, it is singleton.

Circular dependency

A circular dependency is when class A needs class B, and class B also needs class A. With plain constructor injection this cannot work, because neither one can be built first.

When there is a real cycle and you do nothing, the container throws a CircularDependencyError and shows you the full path.

To break the cycle, wrap one side with forwardRef. The class that uses it receives a small proxy. The proxy resolves the real instance the first time you touch it, so both classes can be built.

import { forwardRef } from "@wirely/core";

defineProvider({ useClass: ServiceA, inject: [forwardRef(() => ServiceB)] });
defineProvider({ useClass: ServiceB, inject: [ServiceA] });

Small note. The dependency behind forwardRef is a proxy. It works like the real object, but it is not the same reference. Also, if you enumerate it (Object.keys, spread) and the object is frozen, you can get a proxy error. Access by property or method is always fine.

Lifecycle events

The container has three phases: starting, running, and stopping. You can run code in the start phase and in the stop phase, with two hooks. The hooks are duck typed, so you only add the method on your class.

| Hook | Phase | When it runs | | --- | --- | --- | | onInit() | starting | after the dependencies of the provider are ready | | onDestroy() | stopping | when the container is disposed, in reverse order |

class OrderRepository {
	onInit() {
		// open a connection
	}
	onDestroy() {
		// close the connection
	}
}

You drive the lifecycle with two methods:

  • await container.init() builds all singletons and runs onInit in dependency order, so a dependency starts before the class that uses it. If a hook throws, the container stays not initialized.
  • await container.dispose() runs onDestroy in reverse order, and then marks the container as disposed. After that, get and init throw a ContainerDisposedError.

The hooks can be async. The container waits for them. Only singletons run hooks. Value and transient providers are not tracked.

Container reference

The container has a small surface:

  • container.get(token) builds (if needed) and returns the instance for a token.
  • container.has(token) tells you if the token is registered. Note that has looks only at registration, not at visibility. A token can be registered but still not reachable from the root, because of module encapsulation.
  • await container.init() and await container.dispose() drive the lifecycle.

Errors

All errors extend WirelyError, so you can catch them in one place.

| Error | When it happens | | --- | --- | | TokenNotFoundError | you ask for a token that was never registered | | TokenCollisionError | the same token is registered more than one time | | EncapsulationError | you ask for a token that is registered but not visible to you | | CircularDependencyError | there is a cycle and no side uses forwardRef | | InvalidProviderError | a provider has no useClass, useValue, or useFactory | | ContainerDisposedError | you use the container after dispose() | | ResolutionDepthError | the dependency graph is too deep (guard against runaway recursion) |

Suggested project structure

Wirely does not force a layout, but a feature first layout works well. Keep the classes pure and put all the wiring in the *.module.ts file:

src/
  modules/
    users/
      users.module.ts        # defineModule and defineProvider (the only Wirely imports)
      user.service.ts        # pure class
      user.repository.ts     # pure class
      use-cases/
        register-user.use-case.ts   # pure class
    billing/
      billing.module.ts
      billing.service.ts
      use-cases/
  app.module.ts              # imports the feature modules
  main.ts                    # createContainer(AppModule)

License

MIT, Gabriel de Jesus Silva.