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

@no22/dison

v1.6.2

Published

An experimental DSL that transpiles to TypeScript, with property injection built into the language itself.

Readme

Dison

Dison is an experimental DSL that transpiles to TypeScript. It builds property injection directly into the language, so dependency injection reads like ordinary class syntax instead of framework boilerplate.

The transpiled output happens to look like a ServiceLocator-style registry under the hood — but that's an implementation detail of the generated code, hidden below Dison's own language semantics. As a Dison user you write injectable, override, bind, and configuration declarations; you never touch the registry directly.

Install

npm install @no22/dison

(The package is published as @no22/dison — npm's name-similarity policy rejected the unscoped name dison as too close to existing packages. The CLI command itself is still just dison.)

This gives you the dison CLI (via npx dison) and, for multi-file projects, a shared runtime importable as @no22/dison/runtime.

Quick start

// user-service.dis
class SqlUserRepository {
  findById(id: string) {
    return { id, name: "real user" };
  }
}

class MockUserRepository extends SqlUserRepository {
  findById(id: string) {
    return { id, name: "mock user" };
  }
}

class UserService {
  injectable repo: SqlUserRepository;
}

configuration TestConfig {
  override UserService {
    repo = new MockUserRepository();
  }
}

activate TestConfig;

const service = new UserService();
console.log(service.repo.findById("42"));

Transpile it:

npx dison user-service.dis
# 🎉 Success: user-service.dis ➔ user-service.ts

The generated user-service.ts is plain TypeScript — compile and run it with your normal toolchain (tsc, tsx, ts-node, ...).

Syntax reference

Dison only interprets six keywords. Everything else in a .dis file is ordinary TypeScript, passed through unchanged.

injectable

Declares a lazily-resolved, overridable property inside a class body.

class UserService {
  injectable repo: SqlUserRepository;
}

For types that can't be auto-constructed with new (interface, type alias, abstract class, array/union/function types), a default initializer is required:

class UserService {
  injectable repo: IUserRepository = new SqlUserRepository();
}

configuration / activate

Groups a set of override/bind statements. A named configuration is activated explicitly with activate. Naming and activate are a global-scope-only feature — see Scoped configuration below for local/class scopes, which use anonymous configurations instead:

configuration TestConfig {
  override UserService { repo = new MockUserRepository(); }
}

activate TestConfig;

To activate a configuration defined in another file:

activate TestConfig from "./configs";

Forward references (new in 1.5.0)

A configuration may reference classes declared later in the same file — key evaluation is deferred until the wiring is first consulted, so the natural "declarative header" style just works:

configuration { bind Repo = MockRepo; }   // at the top of the file

class Repo { ... }
class MockRepo extends Repo { ... }
class Service { injectable repo: Repo; }

This applies to bind, override, class-body configurations, and activate alike. Registration order is still respected: if the same type is bound twice, the registration that executes later wins, exactly as if everything had been applied in place.

Scoped configuration (new in 1.2.0)

Where you write a configuration decides how far it reaches. An anonymous configuration { ... } is auto-active (no activate needed) for the place it's written:

  • Top level → global, as before.
  • Inside a function/method body → a local scope, lexically limited to the rest of that block. Great for per-test or per-request isolation — the wiring is undone automatically when the block ends, and concurrent async requests don't interfere.
  • Directly inside a class body → a class scope: that class's declarative DI wiring, shared by all its instances and inherited by subclasses (which can override just the parts they change). If the same class body wires the same key twice, the last configuration wins (new in 1.6.0).
class UserService {
  injectable repo: IUserRepository = new SqlUserRepository();
  configuration { bind IUserRepository = CachedUserRepository; }  // class scope
}

function underTest(): void {
  configuration { bind IUserRepository = MockUserRepository; }    // local scope
  new UserService().repo.findById("1");   // uses the Mock here...
}
// ...and the real (or class-scoped) wiring everywhere else.

Resolution priority is local > class > global > default initializer. A dependency is wired according to the scope active where its root object was constructed, and the whole lazily-built object graph follows that same scope consistently.

In an async function body, entering a local scope is an implicit suspension point (one microtask; new in 1.4.0). The configuration and the code after it run in the function's own async context, so the scope can never leak into the caller's code while the function is suspended at an await. Everything else behaves as before: the scope stays active across awaits inside the function, concurrent async calls remain isolated, and instances constructed inside the scope keep it. Local configurations are rejected at transpile time inside generator function bodies (sync or async), because the scope cannot be kept across yield — put the configuration in the function that drives the generator instead.

Local scopes desugar to using + AsyncLocalStorage, so generated code that uses them needs Symbol.dispose (TypeScript 5.2+ / Node 20+) and node:async_hooks at runtime. See Requirements.

(Naming a configuration and activating it by name is a global-scope-only feature, by design — local and class scopes only take an anonymous configuration { ... }, which is auto-active for the scope it's written in. This isn't a gap to be filled later: the dynamic, explicit-activation model that named configurations need doesn't fit scopes that are already tied lexically to a block or class, so it's intentionally not offered there.)

override

Replaces a single property on a specific class:

override UserService {
  repo = new MockUserRepository();
}

An override targeting a base class also applies to instances of its subclasses (new in 1.3.0) — consistent with the fact that injectable properties are inherited. The most specific registration wins: if both override Service and override SubService set the same property, a SubService instance uses the latter. Across scopes the usual priority (local > class > global) is checked first, so a local override targeting a base class beats a global override targeting the subclass.

override can also be written standalone (not wrapped in a configuration), which desugars to an immediate assignment — useful inside a function to capture a lexical variable:

function createHarness(label: string) {
  class Tagged extends Base { tag = label; }
  override S { dep = new Tagged(); }
  return new S();
}

bind

Replaces every use of a type, wherever it's requested (not just one property):

bind SqlUserRepository = MockUserRepository;

bind supports generics on both sides (bind Repository<User> = MockRepository;) and can be written standalone or inside a configuration, same as override.

The replacement can take constructor arguments (new in 1.2.0), so you can bind types whose constructor needs values:

bind Repository = PostgresRepository("postgres://localhost/db");

The arguments are type-checked against the replacement's constructor, and a standalone bind's arguments can capture local variables.

When bind chains redirect the replacement (bind A = B("x"); bind B = C;), A resolves all the way to new C() and B's arguments are not used — arguments only matter on the final replacement of a chain. This is by design: a chain replaces B wholesale, constructor call included.

token

Most name clashes are resolved automatically (see Automatic collision resolution below), so you rarely need this. token is the explicit escape hatch for the one case Dison can't handle on its own: the same interface or type-alias name coming from two different external npm packages. Those types are declared outside your project, so Dison can't attach a companion to them.

token RepoToken;

class S {
  injectable dep: SomePkgRepository as RepoToken = new Impl();
}

configuration Cfg {
  bind SomePkgRepository as RepoToken = Mock;
}

When such an unresolvable clash is detected, the CLI reports a build error telling you to add a token / as <token>, rather than silently letting one type collide with the other. An explicit token also always wins if you want to override the automatic keying.

Automatic collision resolution

bind/injectable match types by identity, not by name, so same-named types in different files never collide — no tokens, no coordination:

  • Classes (concrete or abstract) are keyed by the class value itself, the same way override keys on the class. Two unrelated class Foo in two files are distinct runtime values.
  • Interfaces / type aliases have no runtime value, so Dison generates a companion Symbol per declaration and keys on that. When you use such a type from another file, Dison injects the matching companion import automatically (it emits companions only for types actually used in DI).

So two files can each declare their own IRepository and use it in bind/injectable with no ceremony; each module's bind only affects its own. See sample/multi-file-collision/. The only clash Dison can't resolve this way is between types from two different external packages — that's what token is for.

Multiple files

Pass more than one file to the CLI to transpile a project as a unit:

npx dison a.dis b.dis c.dis

Input files can live in different directories. Their generated output shares override/bind state through the @no22/dison/runtime package export, so an override/bind activated in one file is visible from classes defined in another — as long as they all resolve @no22/dison/runtime to the same installed copy (in practice: they're part of the same project and share a common node_modules ancestor, which is the normal case).

The CLI also fails the build with a specific fix suggestion when a bind/override in one file could never match its declaration in another — e.g. you bound a class without importing it, or imported it type-only where the identity key needs the runtime value (new in 1.6.0). Previously such mistakes were silent no-ops unless you ran tsc.

Requirements

The generated code targets a modern TypeScript/Node toolchain:

  • TypeScript 5.2+ and Node.js 20+ if you use local scopes (an anonymous configuration inside a function/method body). Those desugar to using + AsyncLocalStorage, which need Symbol.dispose and node:async_hooks. Your tsconfig.json needs a lib that includes Symbol.dispose (e.g. "lib": ["esnext"]) and @types/node.
  • Everything else (injectable/override/bind, global and class scopes) has no special requirement beyond a current TypeScript. Since 1.6.0, a single-file transpile that uses no local scopes emits no node:async_hooks import at all (a synchronous stub is inlined instead), so that output also runs outside Node — in a browser, for example. Multi-file output shares state through @no22/dison/runtime, which does import node:async_hooks, so multi-file projects remain Node-oriented.

The runtime module (@no22/dison/runtime) is shipped pre-compiled, so these requirements apply to your generated .ts files, not to the package itself.

Samples

See sample/ for runnable, self-contained examples covering injectable/override/activate, bind (including generics and chaining), and a multi-file project where two files declare their own same-named IRepository interface and Dison keeps them apart automatically — no tokens needed.

License

ISC