Releases: SansarJs/dependency
v0.0.5
@sansar/dependency
A lightweight, TypeScript-native dependency injection (DI) library for modern
JavaScript environments. It supports inversion of control (IoC) with decorators,
scoped dependencies, and flexible registration strategies (values, resolvers,
generators). Built with Stage 3 ECMAScript decorators in mind, it cannot [yet]
rely on reflect-metadata for true IoC.
Features
- Decorator-Based Injection: Use
@Inject()and@Scope()for
declarative dependency management - Functional Style Providers: Register dependencies as static values,
lazy resolvers, or dynamic generators onContainers - Scoped Dependencies: Cache instances per scope (e.g., request-scoped
services) - Circular Dependency Detection: Automatically detects and throws errors
on cycles - Token-Based Abstractions: Use
Tokensubclasses for type-safe
abstract dependencies - Hierarchical Containers: Support nested containers for modular
applications - Error Handling: Descriptive errors for undefined keys, duplicate
registrations, missing dependencies, and more - No External Dependencies
-
Tokendefault values - Containers as resources, and dependencies with disposal hooks
- Context as parameters to
resolverandgeneratordefintion functions - Support a
Configurationabstraction for defining, importing & excluding
some sets of dependency definitions (think a full set of configuration for
Redis, Datadog, logging, etc) - Introduce properties, environments, conditional dependency definitions
Installation
# Deno
deno add jsr:@sansar/dependency
# PNPM
pnpm i jsr:@sansar/dependency
# YARN
yarn add jsr:@sansar/dependency
# VLT
vlt install jsr:@sansar/dependency
# NPM
npx jsr add @sansar/dependency
# Bun
bunx jsr add @sansar/dependencyQuick Start
Basic Injection
Define and inject dependencies:
import { Container, Inject } from "@sansar/dependency";
@Inject()
class Logger {
log(message: string) {
console.log( message );
}
}
@Inject( Logger )
class Service {
constructor(readonly logger: Logger) {}
}
const container = new Container();
const service = container.get( Service );
service.logger.log( "Hello, DI!" ); // Outputs: Hello, DI!Using Tokens for Abstractions
Tokens allow injecting interfaces or abstract types:
import { Container, Inject, Token } from "@sansar/dependency";
class LogLevel extends Token<"debug" | "info" | "error"> {}
@Inject( LogLevel )
class Logger {
constructor(readonly level: "debug" | "info" | "error") {}
}
const container = new Container()
.register( LogLevel, { value: "info" } );
const logger = container.get( Logger );
console.log( logger.level ); // Outputs: infoScoped Dependencies
Scope instances to specific containers (e.g., per HTTP request):
import { Container, Inject, Scope } from "@sansar/dependency";
const REQUEST_SCOPE = Symbol("REQUEST");
@Scope( REQUEST_SCOPE )
@Inject()
class RequestService {}
const root = new Container();
const requestContainer = new Container({ scope: REQUEST_SCOPE, parent: root });
const someOtherContainer = new Container( requestContainer );
const service1 = someOtherContainer.get( RequestService );
const service2 = requestContainer.get( RequestService );
console.log(service1 === service2); // true (cached in scoped container)Concepts
Container
The core class for registering and resolving dependencies.
- Registration:
register(key, { value }): Provides a static value.register(key, { resolver: () => value }): Lazily resolves and caches the
value.register(key, { generator: () => value }): Generates a new value on each
request.- Optional
scopefor scoped resolver/generator caching.
- Resolution:
get(key): Retrieves the dependency, falling back to parent containers if
needed.
NOTE:
Containers can be nested, forming a hierarchy for modular apps.
Token
A base class for type-safe tokens. Extend it to represent abstract dependencies:
import { Token } from "@sansar/dependency";
class DatabaseUrl extends Token<string> {}NOTE:
Tokens cannot be instantiated.
@Inject(...tokens)
A class decorator to declare constructor dependencies.
Supports forward references via arrow functions.
- Throws
InjectDuplicationErrorif applied multiple times. - Throws
InjectMissingDependencyErrororInjectCircularDependencyErroron
resolution issues.
@Scope(scope: symbol)
Marks a class for scoped caching. Must be used with @Inject() and applied before
it.
- Throws
ScopeDuplicationErrorif applied multiple times. - Throws
ScopeInjectUsageErrorif misused with@Inject(). - Resolution throws
ContainerUndefinedScopeErrorif no matching scope exists.
Advanced Usage
Hierarchical and Scoped Resolution
Dependencies resolve from the invoking container, caching at the appropriate
level:
import { Container } from "@sansar/dependency";
const APP_SCOPE = Symbol( "APP" );
const root = new Container({ scope: APP_SCOPE });
root.register( Date, { resolver: () => new Date(), scope: APP_SCOPE } );
const child = new Container( root );
console.log(child.get( Date ) === root.get(Date)); // trueForward References:
When a token is defined later than its usage, leverage forward reference:
import { Container, Inject } from "@sansar/dependency";
@Inject(() => B)
class A {
constructor(readonly b: B) {}
}
@Inject()
class B {}
console.log(new Container().get( A ) instanceof A); // true
console.log(new Container().get( A ).b instanceof B); // trueResources Management
The Container class is EcmaScript-compliant by implementing the
[Symbol.dispose] method. Futhermore, any dependency object, whether a static
value, injected, resolver- or generator-based, that implements the
[Symbol.dispose] method will be invoked when the container resource is cleaned
up.
I case you do control the dependent value implementation, you can:
- pass an
onDestroycallback to the dependency provider when registering it - call
onDestroy( hook )with your disposal hook yourself from theContext
object passed toresolvers andgenerators.
NOTE: A dependency lifecycle is tied to its related container lifetime.
This is relevant for scoped dependencies which are tied to the contained with
the matching scope.
NOTE: When a container is disposed, it propagates to descendant
containers. We hope this helps preventing some weird inconsistencies.
import { Container, Inject, Token } from "@sansar/dependency";
class A extends Token<number> {}
class B {}
@Inject(A, B)
class C {
[Symbol.dispose]() {/* 1 */}
}
const scope = Symbol();
const root = new Container()
.register(A, { value: Math.PI, onDestroy() {/* 2 */} })
.register(B, {
scope,
resolver: ctx => {
ctx.onDestroy(bValue => {/* 3 */})
return Math.random();
}});
const container = new Container({ scope, parent: root });
container.get( C );
(() => { using _ = container; });
// when container goes out of scope, its [Symbol.dispose] is called
// so it the B instance cached there, through the #3 onDestroy hook
// when root goes out of scope or is disposed of, so will A (#2) and C (#1).Contributing
Contributions are welcome! Fork the repository, make changes, and submit a pull
request. Ensure tests pass and follow the existing code style.
License
This project is licensed under the MIT License. See LICENSE for
details.
Copyright (c) 2025 SansarJs
v0.0.4
@sansar/dependency
A lightweight, TypeScript-native dependency injection (DI) library for modern
JavaScript environments. It supports inversion of control (IoC) with decorators,
scoped dependencies, and flexible registration strategies (values, resolvers,
generators). Built with Stage 3 ECMAScript decorators in mind, it cannot [yet]
rely on reflect-metadata for true IoC.
Features
- Decorator-Based Injection: Use
@Inject()and@Scope()for declarative
dependency management - Functional Style Providers: Register dependencies as static values, lazy
resolvers, or dynamic generators onContainers - Scoped Dependencies: Cache instances per scope (e.g., request-scoped
services) - Circular Dependency Detection: Automatically detects and throws errors
on cycles - Token-Based Abstractions: Use
Tokensubclasses for type-safe abstract
dependencies - Hierarchical Containers: Support nested containers for modular
applications - Error Handling: Descriptive errors for undefined keys, duplicate
registrations, missing dependencies, and more - No External Dependencies
Installation
# Deno
deno add jsr:@sansar/dependency
# PNPM
pnpm i jsr:@sansar/dependency
# YARN
yarn add jsr:@sansar/dependency
# VLT
vlt install jsr:@sansar/dependency
# NPM
npx jsr add @sansar/dependency
# Bun
bunx jsr add @sansar/dependencyQuick Start
Basic Injection
Define and inject dependencies:
import { Container, Inject } from "@sansar/dependency";
@Inject()
class Logger {
log(message: string) {
console.log( message );
}
}
@Inject( Logger )
class Service {
constructor(readonly logger: Logger) {}
}
const container = new Container();
const service = container.get( Service );
service.logger.log( "Hello, DI!" ); // Outputs: Hello, DI!Using Tokens for Abstractions
Tokens allow injecting interfaces or abstract types:
import { Container, Inject, Token } from "@sansar/dependency";
class LogLevel extends Token<"debug" | "info" | "error"> {}
@Inject( LogLevel )
class Logger {
constructor(readonly level: "debug" | "info" | "error") {}
}
const container = new Container()
.register( LogLevel, { value: "info" } );
const logger = container.get( Logger );
console.log( logger.level ); // Outputs: infoScoped Dependencies
Scope instances to specific containers (e.g., per HTTP request):
import { Container, Inject, Scope } from "@sansar/dependency";
const REQUEST_SCOPE = Symbol("REQUEST");
@Scope( REQUEST_SCOPE )
@Inject()
class RequestService {}
const root = new Container();
const requestContainer = new Container({ scope: REQUEST_SCOPE, parent: root });
const someOtherContainer = new Container( requestContainer );
const service1 = someOtherContainer.get( RequestService );
const service2 = requestContainer.get( RequestService );
console.log(service1 === service2); // true (cached in scoped container)Concepts
Container
The core class for registering and resolving dependencies.
- Registration:
register(key, { value }): Provides a static value.register(key, { resolver: () => value }): Lazily resolves and caches the
value.register(key, { generator: () => value }): Generates a new value on each
request.- Optional
scopefor scoped resolver/generator caching.
- Resolution:
get(key): Retrieves the dependency, falling back to parent containers if
needed.
NOTE:
Containers can be nested, forming a hierarchy for modular apps.
Token
A base class for type-safe tokens. Extend it to represent abstract dependencies:
import { Token } from "@sansar/dependency";
class DatabaseUrl extends Token<string> {}NOTE:
Tokens cannot be instantiated.
@Inject(...tokens)
A class decorator to declare constructor dependencies.
Supports forward references via arrow functions.
- Throws
InjectDuplicationErrorif applied multiple times. - Throws
InjectMissingDependencyErrororInjectCircularDependencyErroron
resolution issues.
@Scope(scope: symbol)
Marks a class for scoped caching. Must be used with @Inject() and applied before
it.
- Throws
ScopeDuplicationErrorif applied multiple times. - Throws
ScopeInjectUsageErrorif misused with@Inject(). - Resolution throws
ContainerUndefinedScopeErrorif no matching scope exists.
Advanced Usage
Hierarchical and Scoped Resolution
Dependencies resolve from the invoking container, caching at the appropriate
level:
import { Container } from "@sansar/dependency";
const APP_SCOPE = Symbol( "APP" );
const root = new Container({ scope: APP_SCOPE });
root.register( Date, { resolver: () => new Date(), scope: APP_SCOPE } );
const child = new Container( root );
console.log(child.get( Date ) === root.get(Date)); // trueForward References:
import { Container, Inject } from "@sansar/dependency";
@Inject(() => B)
class A {
constructor(readonly b: B) {}
}
@Inject()
class B {}
console.log(new Container().get( A ) instanceof A); // true
console.log(new Container().get( A ).b instanceof B); // trueContributing
Contributions are welcome! Fork the repository, make changes, and submit a pull
request. Ensure tests pass and follow the existing code style.
License
This project is licensed under the MIT License. See LICENSE for
details.
Copyright (c) 2025 SansarJs
v0.0.3
@sansar/dependency
A strait forward dependency container in JavaScript, implemented for
environment agnosis and compliance to web standards.
deno add @sansar/dependencyAPI Surface
Definitions & concepts:
- Key: a class they serves to store dependency definition and later
retrieve a value from a container. It could be a normal constructor like
Dateor a sub-class ofToken.Tokensub-classes are useful when multiple dependencies share a common
type (eg.:StartupDateToken,ShutdownDateToken) or, for types that
are not constructor-based (eg.:
class ConfigToken extends Token<{value: string, radix?: number}>). - Provider: a definition to resolve a dependency value. It could be:
- a static value:
{ value: T }
- a resolver to lazily compute the value, once, cached:
{resolver: () => T} - a generator to lazily compute a new value, each time:
{resolver: () => T}
- a static value:
- Container: a class which instances organise dependency definitions, value
retrieval and caching. - Parent container: A container can have a parent. Dependency definitions
on children hoist that of their ancestors. Similarly, at resolution time,
containers are travelled up the tree
- Scope: an optional, unique tag for some containers, that force them as the
layers to evaluate and eventually cache scoped resolver- and generator-based
dependency definitions.
Classes:
Token: a base class for identifying values in a containerContainer: the class to contain dependency definitions and cache
dependencies when applicable
Usages
Ordinary constructor key, with a generator dependency definition:
import { Container } from "@sansar/dependency";
const container = new Container()
.register(Date, { generator: () => new Date() });
// Later
container.get(Date) // get a date
container.get(Date) // get another date
container.get(Date) // get yet another dateContrustor key, with a resolver dependency definition:
import { Container } from "@sansar/dependency";
class TemplateEngine { /** heavy stuff in here **/ }
const container = new Container()
.register(TemplateEngine, { resolver: () => new TemplateEngine() });
// Later
container.get(TemplateEngine) // get the template engine (initialized lazily)
container.get(TemplateEngine) // get the same template engine
container.get(TemplateEngine) // still the sameToken-based key, with a value dependency definition:
import { Container, Token } from "@sansar/dependency";
class DbConfig extends Token<Record<'uri'|'username'|'password', string>> {
}
const container = new Container()
.register(DbConfig, {
value: {
username: '',
password: '',
uri: 'jdbc://localhost:5432/app'
}
});
// Later
container.get(DbConfig) // get the registered DbConfig
container.get(DbConfig) // get the registered DbConfig
container.get(DbConfig) // get the registered DbConfigNow with a ancestry:
import {Container} from "@sansar/dependency";
const root = new Container();
const parent = new Container(root);
const container = new Container(parent);
parent.register(Date, { resolver: () => new Date() });
container.get(Date) === parent.get(Date); // true
// root.get(Date) // throw: ContainerUndefinedKeyErrorA scope with downstream definition:
import {Container} from "@sansar/dependency";
const MATH = Symbol('MATH');
const root = new Container();
const parent = new Container({scope: MATH, parent: root});
const container = new Container(parent);
container.register(Number, { scope: MATH, resolver: () => -1 });
// root.get(Number); // throw: ContainerUndefinedKeyError --------|
// parent.get(Number); // throw: ContainerUndefinedKeyError ---| |
container.get(Number); // -1 | |
parent.get(Number); // -1 -------------------------------------| |
// root.get(Number); // throw: ContainerUndefinedKeyError --------|A scope with upstream definition:
import {Container} from "@sansar/dependency";
const MATH = Symbol('MATH');
const root = new Container();
const parent = new Container({scope: MATH, parent: root});
const container = new Container(parent);
const values = [-1, 0, 1]
root.register(Number, { scope: MATH, resolver: () => values.shift() });
// root.get(Number); // throw: ContainerUndefinedScopeError ----|
parent.get(Number); // -1 -----------------------------------| |
container.get(Number); // -1 | |
parent.get(Number); // -1 -----------------------------------| |
// root.get(Number); // throw: ContainerUndefinedScopeError ----|