or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.md
tile.json

tessl/npm-di

A dependency injection framework for ES6/ECMAScript 2015 and Node.js applications with support for ES6 classes, decorators, and annotations

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/di@1.9.x

To install, run

npx @tessl/cli install tessl/npm-di@1.9.0

index.mddocs/

DI Framework

DI is a dependency injection framework for ES6/ECMAScript 2015 and Node.js applications. It provides modern dependency injection with support for ES6 classes, decorators, and annotations, leveraging advanced JavaScript features like Map collections and ES6 modules for automatic dependency resolution and injection.

Package Information

  • Package Name: di
  • Package Type: npm
  • Language: JavaScript (ES6)
  • Installation: npm install di

Core Imports

import { Injector, Inject, Provide, getInjectAnnotation, getProvideAnnotation } from "di";

For Node.js/CommonJS:

const { Injector, InjectAnnotation, ProvideAnnotation, annotate } = require("di");
// Note: InjectAnnotation and ProvideAnnotation are the class constructors
// Inject and Provide are the same classes, available as aliases in ES6 imports
// Helper functions getInjectAnnotation and getProvideAnnotation are not available in Node.js exports

Basic Usage

import { Injector, Inject, Provide, annotate, InjectAnnotation } from "di";

// Define dependencies using decorators (ES6)
class Engine {
  start() {
    console.log("Engine started");
  }
}

@Inject(Engine)
class Car {
  constructor(engine) {
    this.engine = engine;
  }

  start() {
    this.engine.start();
    console.log("Car is running");
  }
}

// Create injector and resolve dependencies
const injector = new Injector();
const car = injector.get(Car);
car.start();

// Alternative: programmatic annotations (ES5 compatible)
function MockEngine() {
  this.start = () => console.log("Mock engine started");
}

function MockCar(engine) {
  this.engine = engine;
  this.start = () => {
    this.engine.start();
    console.log("Mock car is running");
  };
}

annotate(MockCar, new InjectAnnotation(MockEngine));
const mockInjector = new Injector([MockEngine, MockCar]);
const mockCar = mockInjector.get(MockCar);
mockCar.start();

Architecture

The DI framework is built around several key components:

  • Injector Class: Main dependency injection container that manages providers and instances
  • Annotation System: Decorators and programmatic annotations for declaring dependencies
  • Hierarchical Injection: Parent-child injector relationships for scoped dependency resolution
  • Module System: Support for both individual providers and module objects
  • Cache Management: Singleton pattern with instance caching for resolved dependencies

Capabilities

Injector Container

The main dependency injection container that manages providers, resolves dependencies, and maintains instance cache.

class Injector {
  /**
   * Create a new injector instance
   * @param modules - Array of provider functions or module objects
   * @param parentInjector - Optional parent injector for hierarchical injection
   */
  constructor(modules = [], parentInjector = null);

  /**
   * Resolve and return instance for the given token
   * @param token - Token to resolve (string, function, or class)
   * @param resolving - Internal parameter for cycle detection
   * @returns Resolved instance
   * @throws Error if no provider found or cyclic dependency detected
   */
  get(token, resolving = []);

  /**
   * Create a child injector with additional modules
   * @param modules - Array of additional modules for child injector
   * @returns New Injector instance with current injector as parent
   */
  createChild(modules = []);

  /**
   * Return debug information about injector state
   * @returns Object with id, parent_id, and providers information
   * @note Current implementation has a bug - uses Object.keys() on Map which returns empty array
   */
  dump();

  /**
   * Invoke function with dependency injection
   * @param fn - Function to invoke
   * @param context - Execution context
   * @note This method is currently not implemented in the source code
   */
  invoke(fn, context);
}

Usage Examples:

// Basic injector usage
const injector = new Injector([]);
const instance = injector.get(SomeClass);

// Injector with modules
const injector = new Injector([MockEngine, {Car: MockCar}]);

// Hierarchical injectors
const parent = new Injector([Engine]);
const child = parent.createChild([MockEngine]); // MockEngine overrides Engine

Dependency Annotations

Annotations for declaring dependencies and provider tokens.

/**
 * Annotation class for specifying dependencies
 */
class InjectAnnotation {
  /**
   * Create inject annotation with dependency tokens
   * @param params - Variable number of dependency tokens
   */
  constructor(...params);
}

/**
 * Annotation class for specifying provider tokens
 */
class ProvideAnnotation {
  /**
   * Create provide annotation with provider token
   * @param id - Token that this provider satisfies
   */
  constructor(id);
}

// Convenient aliases
const Inject = InjectAnnotation;
const Provide = ProvideAnnotation;

Usage Examples:

// Using decorators (ES6)
@Inject(Engine, Radio)
@Provide('Car')
class Car {
  constructor(engine, radio) {
    this.engine = engine;
    this.radio = radio;
  }
}

// Using programmatic annotations (ES5)
function Car(engine, radio) {
  this.engine = engine;
  this.radio = radio;
}
annotate(Car, new InjectAnnotation(Engine, Radio));
annotate(Car, new ProvideAnnotation('Car'));

Annotation Utilities

Helper functions for working with annotations programmatically.

/**
 * Add annotation to function or class
 * @param fn - Function or class to annotate
 * @param annotation - Annotation instance to add
 */
function annotate(fn, annotation);

/**
 * Extract provide annotation from a provider function or class
 * @param provider - Function or class to inspect
 * @returns Token that this provider satisfies, or null if none found
 */
function getProvideAnnotation(provider);

/**
 * Extract inject annotation from a provider function or class
 * @param provider - Function or class to inspect
 * @returns Array of dependency tokens, or null if none found
 */
function getInjectAnnotation(provider);

Usage Examples:

// Programmatic annotation (ES5 compatible)
function Engine() {}
annotate(Engine, new ProvideAnnotation('Engine'));

function Car(engine) {
  this.engine = engine;
}
annotate(Car, new InjectAnnotation('Engine'));

const injector = new Injector([Engine, Car]);
const car = injector.get(Car);

// Using helper functions to inspect annotations
console.log(getProvideAnnotation(Engine)); // 'Engine'
console.log(getInjectAnnotation(Car)); // ['Engine']

// Dynamic provider inspection
function inspectProvider(provider) {
  const provideToken = getProvideAnnotation(provider);
  const dependencies = getInjectAnnotation(provider);
  
  return {
    provides: provideToken,
    requires: dependencies || []
  };
}

console.log(inspectProvider(Engine)); // { provides: 'Engine', requires: [] }
console.log(inspectProvider(Car)); // { provides: null, requires: ['Engine'] }

Module Definition

Modules can be defined in multiple ways to organize and register providers.

Single Provider Registration:

// Register class directly
const injector = new Injector([Engine, Car]);

// Register with explicit token
@Provide('MockEngine')
class TestEngine {}
const injector = new Injector([TestEngine]);

Module Objects:

// Module as object with string keys
const carModule = {
  'Engine': Engine,
  'Car': Car,
  'Radio': Radio
};

const injector = new Injector([carModule]);
const car = injector.get('Car'); // resolves using string token

Dependency Resolution

The injector supports multiple dependency declaration methods that can be used together.

Class-level @Inject decorator:

@Inject(Engine, Radio)
class Car {
  constructor(engine, radio) {
    // dependencies injected in order
  }
}

TypeScript-style type annotations:

class Car {
  constructor(engine: Engine, radio: Radio) {
    // types used for dependency resolution
  }
}

Parameter-level @Inject decorators:

class Car {
  constructor(
    engine: Engine,
    @Inject(MockRadio) radio
  ) {
    // Mixed type annotations and explicit injection
  }
}

Programmatic annotation (ES5):

function Car(engine, radio) {
  this.engine = engine;
  this.radio = radio;
}
annotate(Car, new InjectAnnotation(Engine, Radio));

Error Handling

The framework provides detailed error messages for common dependency injection issues.

No Provider Error:

const injector = new Injector([]);
try {
  injector.get('NonExistentService');
} catch (error) {
  // Error: "No provider for NonExistentService!"
}

Cyclic Dependency Error:

@Inject('Car')
class Engine {
  constructor(car) {}
}

@Inject(Engine)
class Car {
  constructor(engine) {}
}

const injector = new Injector([Engine, Car]);
try {
  injector.get(Car);
} catch (error) {
  // Error: "Cannot instantiate cyclic dependency! (Car -> Engine -> Car)"
}

Full Resolution Path:

// When dependency chain fails deep in the hierarchy
try {
  injector.get('House');
} catch (error) {
  // Error: "No provider for Sink! (House -> Kitchen -> Sink)"
}

Hierarchical Injection

Child injectors inherit from parent injectors and can override providers.

// Parent injector with base providers
const parent = new Injector([RealEngine, RealRadio]);

// Child injector with test overrides
const child = parent.createChild([MockEngine]);

// Child gets MockEngine, but inherits RealRadio from parent
const testCar = child.get(Car);

Instance Sharing:

const parent = new Injector([SharedService]);
const child = parent.createChild([]);

// Same instance returned from both injectors
const service1 = parent.get(SharedService);
const service2 = child.get(SharedService);
console.log(service1 === service2); // true

Types

/**
 * Annotation instance containing dependency tokens
 */
interface InjectAnnotation {
  params: any[];
}

/**
 * Annotation instance containing provider token
 */
interface ProvideAnnotation {
  id: any;
}

/**
 * Debug information from injector dump
 */
interface InjectorDump {
  id: number;
  parent_id: number | null;
  providers: {
    [token: string]: {
      name: string;
      dependencies: any[];
    };
  };
}

Requirements

  • ES6 Map support (or es6-shim polyfill)
  • Traceur runtime for ES6 transpilation
  • Node.js with --harmony_collections flag (for Node.js usage)

Browser Usage

For browser usage, include the Traceur runtime and ensure Map support:

<script src="traceur-runtime.js"></script>
<script src="di.js"></script>

Node.js Usage

node --harmony_collections your-app.js

The framework automatically includes required polyfills when Map is not available.